سری ++C برای توسعه دهندگان #C
برنامه نویسی شیء گرا
در این بخش میخواهیم به بررسی یکسری از ویژگیها و نکات ریز برنامه نویسی شیء گرا در جاوا اسکریپت بپردازیم که یک برنامه نویس حرفهای جاوا اسکریپت حتما باید بر آنها واقف باشد تا بتواند کتابخانهها و Framework های موثرتر و بهینهتری را ایجاد کند. لازم به ذکر است که در این مجموعه مقالات، پیادهسازی اشیاء و شیوهی کد نویسی، بر اساس استاندارد ECMAScript 5 یا ES5 انجام خواهد شد. بنابراین از قابلیتهای جدیدی که در ES6 اضافه شدهاست، صحبت نخواهیم کرد. پس از پایان این مجموعه مقالات و پس از آگاهی کامل از قابلیتهای جاوا اسکریپت، در مجموعه مقالاتی به بررسی قابلیتهای جدید ES6 خواهیم پرداخت که مرتبط به مقالات جاری است.
همانطور که قبلا اشاره شد، در زبانهای برنامه نویسی شیء گرا، مفهومی به نام کلاس وجود دارد که ساختاری را جهت ایجاد اشیاء معرفی میکند و میتوانیم اشیاء مختلفی را از این کلاسها ایجاد نماییم. اما در جاوا اسکریپت مفهوم کلاس وجود ندارد و فقط میتوانیم از اشیاء استفاده کنیم که نسبت به زبانهای مبتنی بر کلاس متفاوت میباشد.
بر اساس تعریفی که از اشیاء در استاندارد ECMAScript صورت گرفته است، هرشیء، شامل مجموعهای از ویژگیهاست، که هر یک از آنها میتواند حاوی یک مقدار پایه، شیء و یا تابع باشد. به عبارت دیگر هر شیء شامل آرایهای از مقادیر است. هر ویژگی ( Property ) یا تابع (که در برنامه نویسی شیء گرا متد نیز نامیده میشود) توسط نام خود شناسایی میشوند که به یک مقدار دادهای نگاشت یا Map شدهاند. به همین دلیل میتوان هر شیء را به عنوان یک Hash Table تصور کرد که دادهها را به صورت یک زوج کلید مقدار یا key-value pairs نگهداری مینماید. در اینصورت نام ویژگیها و متدها به عنوان key و مقدار آنها به عنوان value در نظر گرفته میشوند.
مفهوم شیء
همانطور که قبلا اشاره شد، جهت تعریف اشیاء میتوان از دو روش استفاده نمود. در روش اول، ایجاد شیء با استفاده از شیء Object و در روش دوم، با استفاده از Object Literal Notation انجام خواهد شد. روش دوم جدیدتر و بین برنامه نویسان جاوا اسکریپت محبوبتر است. مثال دیگری را جهت یادآوری در این مورد ذکر میکنم:
var person = new Object(); person.firstName = "Meysam"; person.birth = new Date(1982, 11, 8); person.getAge = function () { var now = new Date(); return now.getFullYear() - this.birth.getFullYear(); } alert(person.firstName + ": " + person.getAge()); // Meysam: 34
var person = { firstName: "Meysam", birth: new Date(1982, 11, 8), getAge: function () { var now = new Date(); return now.getFullYear() - this.birth.getFullYear(); } }; alert(person.firstName + ": " + person.getAge()); // Meysam: 34
انواع Property ها
در ECMAScript 5 ، صفاتی برای Property ها معرفی شده است که از طریق Attribute های داخلی به Property ها اختصاص مییابد. این Attribute ها توسط موتور جاوا اسکریپت بر روی Property ها پیاده سازی میشوند و به صورت مستقیم قابل دسترسی نمیباشند. در طی فرآیند آموزش این مطالب، Attribute های داخلی را در [[]] قرار میدهیم، مثل [[Enumarable]] ، تا از سایر دستورات تفکیک شوند. به صورت کلی دو نوع ویژگی داریم که شامل Data Properties و Accessor Properties میباشند که به شرح آنها میپردازیم.
Data Properties
Data Property ها، 4 صفت یا Attribute را توصیف میکنند که عبارتند از:
[[Configurable]]
مشخص میکند یک Property اجازه حذف، تعریف مجدد و یا تغییر نوع را دارد یا خیر. بصورت پیش فرض، زمانی که یک شیء بصورت مستقیم ساخته میشود، مقدار این ویژگی True میباشد.
[[Enumarable]]
مشخص میکند که آیا امکان پیمایش یک Property توسط حلقه for-in وجود دارد یا خیر. بصورت پیش فرض، زمانیکه یک شیء بصورت مستقیم ساخته میشود، مقدار این ویژگی True میباشد.
[[Writable]]
مشخص میکند که آیا مقدار یک Property قابل تغییر میباشد یا خیر. بصورت پیش فرض، زمانیکه یک شیء بصورت مستقیم ساخته میشود، مقدار این ویژگی True میباشد.
[[Value]]
شامل مقدار واقعی یک Property و محل مقداردهی یا برگرداندن مقدار Property ها میباشد. مقدار پیش فرض آن نیز undefined میباشد.
زمانیکه یک Property به صورت عادی به یک شیء اضافه میشود، مانند مثالهای قبلی، سه Attribute اول به true تنظیم میشوند و [[Value]] با مقدار اولیه Property تنظیم میگردد. در این حالت آن Property ، قابل بروزرسانی و پیمایش میباشد. جهت تغییر ساختار یک Property و تنظیم Attribute های آن، باید آن Property را با استفاده از متد defineProperty() تعریف نماییم . شکل کلی تعریف Property با استفاده از این متد به صورت زیر میباشد:
Object.defineProperty(obj, prop, descriptor)
var person = {}; Object.defineProperty(person, "name", { writable: false, value:"Meysam" }); alert(person.name); // Meysam person.name = "Arash"; alert(person.name); // Meysam
var person = {}; Object.defineProperty(person, "name", { configurable: false, value: "Meysam" }); alert(person.name); // Meysam delete person.name; alert(person.name); // Meysam
لازم به ذکر است که میتوانید متد defineProperty() را چندین بار برای یک Property فراخوانی نموده و در هر مرحله صفات متفاوتی را تنظیم و یا صفات قبلی را تغییر دهید.
علاوه بر متد فوق، متد دیگری به نام defineProperties() وجود دارد که میتوان چند Property را بصورت همزمان تعریف و صفات آن را تنظیم نمود. شکل کلی این متد به صورت زیر است:
Object.defineProperties(obj, props)
آرگومان props یک شیء میباشد که ویژگیهای آن، نام همان Property هایی هستند که باید به obj اضافه شوند. همچنین هر ویژگی خود یک شیء میباشد که میتوان صفات آن ویژگی را تنظیم نمود. به مثال زیر توجه کنید:
var person = {}; Object.defineProperties(person, { "name": { configurable: false, value: "Meysam" }, "age": { writable:false, value:34 } });
Accessor Properties
این صفات شامل توابع getter و setter میباشند که یک یا هر دوی آنها میتوانند برای یک Property تنظیم شوند. زمانی که مقداری را از یک Property میخوانید، تابع getter فراخوانی میشود و مقدار Property مربوطه را بر میگرداند. این تابع میتواند قبل از برگرداندن مقدار، پردازش هایی را بر روی آن Property انجام داده و یک نتیجهی معتبر را برگرداند. زمانیکه Property را مقداردهی مینمایید، تابع setter فراخوانی میشود و Property را با مقدار جدید تنظیم مینماید. این تابع میتواند قبل از مقداردهی به Property ، دادهی مورد نظر را اعتبارسنجی نماید تا از ورود مقادیر نامعتبر جلوگیری کند. Accessor Properties شامل 2 صفت زیر میباشد:
[[Get]]
یک تابع میباشد و زمانی فراخوانی میگردد که مقدار یک Property را بخوانیم و مقدار پیش فرض آن undefined میباشد.
[[Set]]
یک تابع میباشد و زمانی فراخوانی میگردد که یک Property را مقداردهی نماییم و مقدار پیش فرض آن undefined میباشد. این تابع شامل یک آرگومان ورودی است که حاوی مقدار ارسالی به Property است.
مثال زیر یک پیاده سازی ساده از شیء تاریخ شمسی میباشد که هنوز از لحاظ طراحی دارای نواقصی هست و در ادامه کارآیی و کد آن را بهبود میبخشیم.
var date = { _year: 1, _month: 1, _day: 1, isLeap: function () { switch (this.year % 33) { case 1: case 5: case 9: case 13: case 17: case 22: case 26: case 30: return true; default: return false; } } }; Object.defineProperties(date, { "year": { "get": function () { return this._year; }, "set": function (newValue) { if (newValue < 1 || newValue > 9999) throw new Error("Year must be between 1 and 9999"); this._year = newValue; } }, "month": { "get": function () { return this._month; }, "set": function (newValue) { if (newValue < 1 || newValue > 12) throw new Error("Month must be between 1 and 12"); this._month = newValue; } }, "day": { "get": function () { return this._day; }, "set": function (newValue) { if (newValue < 1 || newValue > 31) throw new Error("Day must be between 1 and 31"); if (this.month === 12 && !this.isLeap() && newValue > 29) throw new Error("Day must be between 1 and 29"); if (this.month > 6 && newValue > 30) throw new Error("Day must be between 1 and 30"); this._day = newValue; } } });
دقت داشته باشید که لازم نیست حتما accessor های getter و setter با هم برای یک Property تنظیم شوند و شما میتوانید فقط یکی از آنها را برای Property به کار ببرید. اگر فقط تابع getter به یک Property اختصاص یابد، آن Property فقط خواندنی میشود و امکان تغییر مقدار آن وجود ندارد. در این صورت هر دستوری که اقدام به تغییر Property نماید، بیتاثیر خواهد بود. همچنین اگر فقط تابع setter به یک Property اختصاص یابد، آن Property فقط نوشتنی میشود و امکان خواندن مقدار آن وجود ندارد. در این صورت هر دستوری که اقدام به خواندن Property نماید، مقدار undefined برای آن برگردانده میشود.
نکتهی دیگری که باید به آن توجه کنید این است که اگر یک Property با استفاده از متد defineProperty() تعریف گردد، Attribute هایی که مقداردهی نشدهاند، مثل [[Configurable]] ، [[Enumarable]] و [[Writable]] با false مقداردهی میگردند و [[Value]] ، [[Get]] و [[Set]] مقدار undefined را بر میگردانند. در مبحث بعدی، در مورد این نکته مثالی ارائه شده است.
خواندن Attribute های مربوط به یک Property
با استفاده از متد getOwnPropertyDescriptor() میتوان، Attribute های اختصاص داده شده به Property ها را خواند و از مقدار آنها مطلع شد. این متد شامل 2 آرگومان میباشد، که آرگومان اول، شیء ای است که میخواهیم Attribute آن را بخوانیم و آرگومان دوم، نام Attribute میباشد. خروجی متد getOwnPropertyDescriptor() یک شیء از نوع PropertyDescriptor میباشد که ویژگیهای آن، همان Attribute هایی هستند که برای یک Property تنظیم شدهاند. به مثال زیر جهت خواندن Attribute های شیء تاریخ شمسی توجه کنید:
var descriptor = Object.getOwnPropertyDescriptor(date, "_year"); alert(descriptor.value); // 1 alert(descriptor.configurable); // true alert(typeof descriptor.get); // undefined descriptor = Object.getOwnPropertyDescriptor(date, "year"); alert(descriptor.value); // undefined alert(descriptor.configurable); // false alert(typeof descriptor.get); // function
ما طراحی میکنیم تا علاوه بر نیازهای عملیاتی، به نیازهای غیر عملیاتی (Non Functional Requirements) نیز فکر کنیم؛ در حالیکه در زمان برنامه نویسی صرفا به Functionality فکر میکنیم.
کتاب Object Oriented Design Heuristics اولین کتاب در زمینه طراحی و توسعه شیء گرا میباشد. خواندن آن برای برنامه نویسان در هر رده ای که هستند، مفید خواهد بود و میتوانند از این Heuristicها (قواعد شهودی) به عنوان ابزاری برای تبدیل شدن به یک توسعه دهنده برتر، استفاده کنند.
در این کتاب بیشتر، بهبود طراحی شیء گرا هدف قرار داده شدهاست و در این راستا بیش از 60 دستورالعمل که هیچ وابستگی به زبان خاصی هم ندارند، ارائه شده است. قواعد شهودی در واقع قوانین سخت گیرانهای نیستند. بلکه میتوان آنها را به عنوان یک مکانیزم هشدار در نظر گرفت که در زمان نیاز حتی میتوان آنها را نقض کرد.
پیشنهاد میکنم حداقل برای اینکه ادبیات فنی خود را سامان ببخشید و با ادبیات یکسانی باهم صحبت کنیم، این کتاب را مطالعه کنید.
Introduction to Classes and Objects
پارادایم شیء گرا از مفاهیم کلاس و آبجکت، به عنوان بلوکهای ساختاری پایهای در شکل گیری یک مدل سازگار و استوار برای تحلیل، طراحی و پیاده سازی نرم افزار، استفاده میکند.
این مفاهیم را با یک مثال واقعی، بهتر میتوان شرح داد. یک اتاق پر از جمعیت را درنظر بگیرید؛ اگر شما میپرسیدید «چه تعداد از حاضرین در این اتاق میتوانند یک ساعت زنگدار(alarm clock ) را با در دست داشتن تمام قطعات آن، بسازند؟» در بهترین حالت یک یا دو نفر تمایل داشتند دست خود را بالا ببرند. اگر در همین اتاق میپرسیدید، «چه تعداد از حاضرین در این اتاق میتوانند یک ساعت زنگدار را برای ساعت 9 صبح تنظیم کنند؟» بدون شک بیشتر جمعیت تمایل داشتند دست خود را بالا ببرند.
آیا نامعقول نیست که این تعداد جمعیت زیاد، ادعا دارند که میتوانند از ساعت زنگدار استفاده کنند، درحالیکه حتی نمیتوانند یک ساعت زنگدار بسازند؟ پاسخ بی درنگ برای این سوال «البته که نه! سوال شما نامعقول است» میباشد.
در دنیای واقعی خیلی چیزها هستند که ما میتوانیم از آنها استفاده کنیم، بدون آنکه دانشی درباره پیاده سازی آنها داشته باشیم؛ مانند: یخچالها، اتومبیلها، دستگاههای فتوکپی، کامپیوترها و غیره. چون آنها برای استفاده شدن از طریق واسط عمومی خودشان، تعریف و طراحی شدهاند. لذا حتی بدون داشتن دانشی از پیاده سازی آنها، استفاده از آنها آسان میباشد. این واسط عمومی وابسته به دستگاه مورد نظر است. اما جزئیات پیاده سازی دستگاه را از دید کاربرانش پنهان میکند. این استراتژی طراحی، چیزی است که به سازنده اجازه میدهد بدون آنکه کاربران رنجیده شوند، با آزادی عمل، 60 مؤلفه کوچک استفاده شده در ساخت ساعت زنگدار را تعویض کند.
مثال دیگری از واسط عمومی در مقابل جزئیات پیاده سازی، میتواند در حوزه اتومبیلها دیده شود. زمانیکه تولید کنندگان اتومبیل از سیستمهای احتراق مکانیکی به سمت سیستمهای احتراق الکترونیکی کوچ کردند، تعداد خیلی کمی از کاربران اتومبیلها نگران این موضوع بودند. اما چرا؟ چون واسط عمومی آنها مانند سابق ماند و تنها پیاده سازی تغییر کرد. فرض کنید که شما به قصد خرید اتومبیل به یک فروشنده اتومبیل مراجعه میکنید و فروشنده یک سوئیچ را به شما داده و از شما میخواهد برای تست آن را برانید. بعد از تلاشی که برای استارت زدن داشتید، فروشنده اعلام میکند که در این مدل برای استارت زدن باید ابتدا کاپوت را بالا زده و دکمه قرمز را فشار دهید. در این حالت، بدلیل اینکه واسط عمومی اتومبیل دستخوش تغییر بوده است، باعث ناراحتی شما خواهد شد.
این فلسفه، دقیقا یکی از ایدههای پایهای در پارادایم شیءگرا میباشد. تمام جزئیات پیاده سازی در سیستم شما باید در پشت یک واسط عمومی مستحکم و سازگار، از کاربران آنها پنهان باشد. نیاز کاربران، دانستن درباره واسط عمومی میباشد؛ اما هرگز مجاز به دیدن جزئیات پیاده سازی آنها نیستند. با این روش، پیاده ساز میتواند به هرشکلی که مناسب است، پیاده سازی را تغییر دهد؛ درحالیکه واسط عمومی مانند سابق میباشد. به عنوان مسافری که مکرر سفر میکنم، به شما اطمینان میدهم که استفاده از ساعتهای زنگدار با وجود عدم اطلاع از پیاده سازی آنها، فواید عظیمی دارند. در هتلهای زیادی که از دسته بندیهای گستردهای از ساعتها مانند الکتریکی، قابل کوک (windup)، باتری خور، در هر دو مدل دیجیتال و آنالوگ استفاده میکنند، اقامت کردهام. یکبار هم اتفاق نیفتادهاست در حالیکه در هواپیما نشسته باشم، نگران این باشم که قادر نخواهم بود از ساعت زنگی اتاقم در هتل استفاده کنم.
بیشتر خوانندگان این کتاب، با وجود اینکه در نزدیکی آنها شاید ساعت زنگداری هم نباشد، ولی منظور بنده را با عبارت «ساعت زنگدار» متوجه شدند. به چه دلیل؟ شما در زندگی خودتان ساعتهای زنگدار زیادی را میبینید و متوجه میشوید که همه آنها از یکسری خصوصیات مشترک مانند زمان، یک زمان هشدار و طراحیای که مشخص میکند هشدار روشن یا خاموش است، بهره میبرند. همچنین متوجه میشوید که همه ساعتهای زنگداری که دیدهاید امکان تنظیم کردن زمان، تنظیم زمان هشدار و روشن و خاموش کردن هشدار را به شما میدهند. در نتیجه، شما الان مفهومی را به نام «ساعت زنگدار» دارید که مفهومی را از داده و رفتار، در یک بسته بندی مرتب برای همه ساعتهای زنگدار، تسخیر میکند. این مفهوم به عنوان یک Class (کلاس) شناخته میشود. یک ساعت زنگدار فیزیکی که شما در دست خود آن را نگه داشتهاید، یک Object (وهله، Instance) ای از کلاس ساعت زنگدار میباشد. رابطه بین مفهوم کلاس و وهله، Instantiation Relationship (وهله سازی) نام دارد. به یک object، ساعت زنگدار وهله سازی شده (Instantiated) از کلاس ساعت زنگدار گفته میشود؛ در حالیکه از کلاس ساعت زنگدار به عنوان تعمیم (Generalization) از همه objectهای کلاس ساعت زنگدار که شما با آنها روبرو شدهاید، یاد میشود.
اگر من به شما میگفتم که ساعت زنگدارم از روی پاتختی (میز کوچک کنار تخت که دارای کشو میباشد) من پرید، من را گاز گرفت، سپس گربهی همسایه را دنبال کرد، قطعا مرا دیوانه به حساب میآوردید. اگر به شما میگفتم که سگ من کارهای مشابهای را انجام میدهد، کاملا منطقی میبود. چون نام یک کلاس تنها به مجموعهای از خواص اشاره نمیکند، بلکه رفتارهای موجودیت (entity) را نیز مشخص میکند. این رابطه دوسویه بین داده و رفتار، اساس پارادایم شیء گرا میباشد.
- هویت خود (ممکن است آدرس آن در حافظه باشد) - its own identity
- خواص کلاس خود (معمولا استاتیک) و مقادیر این خواص (معمولا پویا) - attributes of its class
- رفتار کلاس خود (از دید پیاده ساز) - behavior of its class
- واسط منتشر شده کلاس خود (از دید استفاده کننده) - published interface of its class
یک کلاس را می توان با record definition (ساختار داده پایه، struct) و لیستی از عملیاتی که مجاز به کار بر روی این record definition هستند، پیاده سازی کرد. در زبانهای رویهای (Procedural) یافتن وابستگی دادهها در یک تابع معین، آسان میباشد. این کار را میتوان به سادگی با بررسی کردن جزئیات پیاده سازی تابع و مشاهده نوع داده پارامترهای آن، مقادیر بازگشتی و متغییرهای محلیای که تعریف شدهاند، انجام داد. اگر قصد شما پیدا کردن وابستگیهای تابعی بر روی یک داده میباشد، باید همه کد را بررسی کرده و به دنبال توابعی باشید که به داده شما وابسته هستند. در مدل شیء گرا، هر دو نوع وابستگی (داده به رفتار و رفتار به داده) به راحتی در دسترس میباشند. وهلهها، متغیرهایی از یک نوع داده کلاس هستند. جزئیات داخلی آنها باید فقط برای لیست توابع مرتبط با کلاسهایشان آشکار باشد. این محدودیت دسترسی به جزئیات داخلی وهلهها، Information Hiding نامیده میشود. اختیاری بودن این بحث در خیلی از زبانهای شیء گرا ما را به سمت اولین قاعده شهودی هدایت میکند.
همه دادهها باید در داخل کلاس خود پنهان شده باشند. (All data should be hidden within its class)
با نقض این قاعده، امکان نگهداری را هم از دست میدهید. اجبار به پنهان کردن اطلاعات در مراحل طراحی و پیاده سازی، بخش عظیمی از فواید پارادایم شیء گرا میباشد. اگر داده به صورت عمومی تعریف شده باشد، تشخیص اینکه کدام بخش از عملیات (functionality) سیستم به آن داده وابسته است، سخت و مشکل خواهد بود. در واقع، نگاشت تغییرات داده به عملیات سیستم، همانند طراحی و پیاده سازی در دنیای action-oriented میباشد. ما مجبور میشویم برای تشخیص اینکه کدام عملیات به داده مورد نظر ما وابسته است، تمام عملیات سیستم را بررسی کنیم، تا به این ترتیب متوجه شویم.
برخی اوقات، یک توسعه دهنده استدلال میکند «نیاز دارم این بخش از داده را عمومی تعریف کنم زیرا ....» در این وضعیت، توسعه دهنده باید از خود سوال کند «کاری که تلاش دارم با این داده انجام دهم چیست و چرا کلاس این عملیات را خودش برای من انجام نمیدهد؟» در همه موارد این کلاس است که به سادگی عملیات ضروری را فراموش کردهاست. کمی بر روی شکل 2.2 فکر کنید. توسعه دهنده به صورت تصادفی فکر کرده است که عضو byte_offset را برای مجاز ساختن دسترسی تصادفی I/O، به صورت عمومی تعریف کند. اما چیزی که واقعا برای انجام این کار به آن نیاز داشت، تعریف یک operation بود (در زبان سی، توابع fseek و ftell برای ممکن کردن دسترسی تصادفی I/O، موجود هستند).
مراقب توسعه دهندههایی که جسورانه میگویند: «ما میتوانیم این بخش از داده را تغییر دهیم، زیرا هیچوقت تغییر نخواهد کرد!» باشید. طبق قانون برنامه نویسی مورفی، اولین بخشی که نیاز به تغییر خواهد داشت همین بخش از داده است.
به عنوان مثال دیگر برای روشنتر شدن بحث، کلاس Point را که پیاده سازی آن به روش مختصات دکارتی میباشد، در نظر بگیرید. یک طراح بیتجربه ممکن است دلیل تراشی کند که ما میتوانیم دادههای X و Y را به صورت عمومی تعریف کنیم؛ چرا که هیچ موقع تغییر نخواهند کرد. فرض کنید نیاز جدیدی مبنی بر اینکه پیاده سازی Point به ناچار باید از دکارتی به قطبی تغییر کند، به دست شما برسد. به این صورت استفاده کنندگان از این کلاس Point نیز باید تغییر کنند. حال اگر دادهها پنهان بودند و عمومی نبودند، در نتیجه فقط لازم بود پیاده سازهای این کلاس، کد خود را تغییر دهند.
EF Core چیست؟
EF Core یک ORM یا object-relational mapper چندسکویی است که امکان کار با بانکهای اطلاعاتی مختلف را از طریق اشیاء دات نتی میسر میکند. توسط آن قسمت عمدهی کدهای مستقیم کار با بانکهای اطلاعاتی حذف شده و تبدیل به کدهای دات نتی میشوند. مزیت این لایهی Abstraction اضافی (لایهای بر روی کدهای مستقیم لایه ADO.NET زیرین)، امکان تعویض بانک اطلاعاتی مورد استفاده، تنها با تغییر کدهای آغازین برنامهاست؛ بدون نیاز به تغییری در سایر قسمتهای برنامه. همچنین کار با اشیاء دات نتی و LINQ، مزایایی مانند تحت نظر قرار گرفتن کدها توسط کامپایلر و برخورداری از ابزارهای Refactoring پیشرفته را میسر میکنند. به علاوه SQL خودکار تولیدی توسط آن نیز همواره پارامتری بوده و مشکلات حملات تزریق SQL در این حالت تقریبا به صفر میرسند (اگر مستقیما SQL نویسی نکنید و صرفا از LINQ استفاده کنید). مزیت دیگر همواره پارامتری بودن کوئریها، رفتار بسیاری از بانکهای اطلاعاتی با آنها همانند رویههای ذخیره شده است که به عدم تولید Query planهای مجزایی به ازای هر کوئری رسیده منجر میشود که در نهایت سبب بالا رفتن سرعت اجرای کوئریها و مصرف حافظهی کمتری در سمت سرور بانک اطلاعاتی میگردد.
تفاوت EF Core با نگارشهای دیگر Entity framework در چیست؟
سورس باز بودن
EF از نگارشهای آخر آن بود که سورس باز شد؛ اما EF Core از زمان نگارشهای پیش نمایش آن به صورت سورس باز در GitHub قابل دسترسی است.
چند سکویی بودن
EF Core برخلاف EF 6.x (آخرین نگارش مبتنی بر Full Framework آن)، نه تنها چندسکویی است و قابلیت اجرای بر روی Mac و لینوکس را نیز دارا است، به علاوه امکان استفادهی از آن در انواع و اقسام برنامههای دات نتی مانند UWP یا Universal Windows Platform و Windows phone که پیشتر با EF 6.x میسر نبود، وجود دارد. لیست این نوع سکوها و برنامههای مختلف به شرح زیر است:
• All .NET application (Console, ASP.NET 4, WinForms, WPF) • Mac and Linux applications (Mono) • UWP (Universal Windows Platform) • ASP.NET Core applications • Can use EF Core in Windows phone and Windows store app
افزایش تعداد بانکهای اطلاعاتی پشتیبانی شده
در EF Full یا EF 6.x، هدف اصلی، تنها کار با بانکهای اطلاعاتی رابطهای بود و همچنین مایکروسافت صرفا نگارشهای مختلف SQL Server را به صورت رسمی پشتیبانی میکرد و برای سایر بانکهای اطلاعاتی دیگر باید از پروایدرهای ثالث استفاده کرد.
در EF Core علاوه بر افزایش تعداد پروایدرهای رسمی بانکهای اطلاعاتی رابطهای، پشتیبانی از بانکهای اطلاعاتی NoSQL هم اضافه شدهاست؛ به همراه پروایدر ویژهای به نام In Memory جهت انجام سادهتر Unit testing. کاری که با نگارشهای پیشین EF به سادگی و از روز اول پشتیبانی نمیشد.
حذف و یا عدم پیاده سازی تعدادی از قابلیتهای EF 6.x
اگر موارد فوق جزو مهمترین مزایای کار با EF Core باشند، باید درنظر داشت که به علت حذف و یا تقلیل یافتن یک سری از ویژگیها در NET Core.، مانند Reflection (جهت پشتیبانی از دات نت در سکوهای مختلف کاری و خصوصا پشتیبانی از حالتی که کامپایلر مخصوص برنامههای UWP نیاز دارد تمام نوعها را همانند زبانهای C و ++C، در زمان کامپایل بداند)، یک سری از قابلیتهای EF 6.x مانند Lazy loading هنوز در EF Core پشتیبانی نمیشوند. لیست کامل و به روز شدهی این موارد را در اینجا میتوانید مطالعه کنید.
بنابراین امکان انتقال برنامههای EF 6.x به EF Core 1.0 عموما وجود نداشته و نیاز به بازنویسی کامل دارند. هرچند بسیاری از مفاهیم آن با EF Code First یکی است.
برپایی تنظیمات اولیهی EF Core 1.0 در یک برنامهی ASP.NET Core 1.0
برای نصب تنظیمات اولیهی EF Core 1.0 در یک برنامهی ASP.NET Core 1.0، جهت کار با مشتقات SQL Server (و SQL LocalDB) نیاز است سه بستهی ذیل را نصب کرد (از طریق منوی Tools -> NuGet Package Manager -> Package Manager Console):
PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer PM> Install-Package Microsoft.EntityFrameworkCore.Tools -Pre PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer.Design
پس از اجرای سه دستور فوق، تغییرات مداخل فایل project.json برنامه به صورت ذیل خواهند بود:
{ "dependencies": { // same as before "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0", "Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final", "Microsoft.EntityFrameworkCore.SqlServer.Design": "1.0.0" } }
{ "dependencies": { // same as before "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0", "Microsoft.EntityFrameworkCore.Tools": { "version": "1.0.0-preview2-final", "type": "build" }, "Microsoft.EntityFrameworkCore.SqlServer.Design": { "version": "1.0.0", "type": "build" } }, "tools": { // same as before "Microsoft.EntityFrameworkCore.Tools": { "version": "1.0.0-preview2-final", "imports": [ "portable-net45+win8" ] } } }
بنابراین از همین ابتدای کار، بدون مراجعهی به Package Manager Console، چهار تغییر فوق را به فایل project.json اعمال کرده و آنرا ذخیره کنید؛ تا کار به روز رسانی و نصب بستهها، به صورت خودکار و همچنین صحیحی انجام شود.
فعال سازی صفحات مخصوص توسعه دهندههای EF Core 1.0
در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 5 - فعال سازی صفحات مخصوص توسعه دهندهها» با تعدادی از اینگونه صفحات آشنا شدیم. برای EF Core نیز بستهی مخصوصی به نام Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore وجود دارد که امکان فعال سازی صفحهی نمایش خطاهای بانک اطلاعاتی را میسر میکند. بنابراین ابتدا به فایل project.json مراجعه کرده و این بسته را اضافه کنید:
{ "dependencies": { // same as before "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore": "1.0.0" } }
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDatabaseErrorPage(); }
تعریف اولین Context برنامه و مشخص سازی رشتهی اتصالی آن
در این تصویر، زیر ساخت نگاشتهای EF Core را مشاهده میکنید. در سمت چپ، ظرفی را داریم به نام DB Context که در برگیرندهی Db Setها است. در سمت راست که بیانگر ساختار کلی یک بانک اطلاعاتی است، معادل اینها را مشاهده میکنیم. هر Db Set به یک جدول بانک اطلاعاتی نگاشت خواهد شد و متشکل است از کلاسی به همراه یک سری خواص که اینها نیز به فیلدها و ستونهای آن جدول در سمت بانک اطلاعاتی نگاشت میشوند.
بنابراین برای شروع کار، پوشهای را به نام Entities به پروژه اضافه کرده و سپس کلاس ذیل را به آن اضافه میکنیم:
namespace Core1RtmEmptyTest.Entities { public class Person { public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } } }
using Microsoft.EntityFrameworkCore; namespace Core1RtmEmptyTest.Entities { public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } public DbSet<Person> Persons { get; set; } } }
سازندهی این کلاس نیز به نحو خاصی تعریف شدهاست. اگر به سورسهای EF Core مراجعه کنیم، کلاس پایهی DbContext دارای دو سازندهی با و بدون پارامتر است:
protected DbContext() : this((DbContextOptions) new DbContextOptions<DbContext>()) { } public DbContext([NotNull] DbContextOptions options) { // … }
using Microsoft.EntityFrameworkCore; namespace Core1RtmEmptyTest.Entities { public class ApplicationDbContext : DbContext { public DbSet<Person> Persons { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"... connection string ..."); } } }
اما چون در یک برنامهی ASP.NET Core، کار ثبت سرویس مربوط به EF Core، در کلاس آغازین برنامه انجام میشود و در آنجا به سادگی میتوان به خاصیت Configuration برنامه دسترسی یافت و توسط آن رشتهی اتصالی را دریافت کرد، مرسوم است از سازندهی با پارامتر DbContext به نحوی که در ابتدا عنوان شد، استفاده شود.
بنابراین در ادامه، پس از مطالعهی مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایلهای config» به فایل appsettings.json مراجعه کرده و تنظیمات رشتهی اتصالی برنامه را به صورت ذیل در آن مشخص میکنیم:
{ "ConnectionStrings": { "ApplicationDbContextConnection": "Data Source=(local);Initial Catalog=TestDbCore2016;Integrated Security = true" } }
در اینجا به وهلهی پیش فرض SQL Server اشاره شدهاست؛ از حالت اعتبارسنجی ویندوزی SQL Server استفاده میشود و بانک اطلاعاتی جدیدی به نام TestDbCore2016 در آن مشخص گردیدهاست.
پس از تعریف رشتهی اتصالی، متد OnConfiguring را از کلاس ApplicationDbContext حذف کرده و از همان نگارش دارای سازندهی با پارامتر آن استفاده میکنیم. برای اینکار به کلاس آغازین برنامه مراجعه کرده و توسط متد AddDbContext این Context را به سرویسهای ASP.NET Core معرفی میکنیم:
public class Startup { public IConfigurationRoot Configuration { set; get; } public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", reloadOnChange: true, optional: false) .AddJsonFile($"appsettings.{env}.json", optional: true); Configuration = builder.Build(); } public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IConfigurationRoot>(provider => { return Configuration; }); services.AddDbContext<ApplicationDbContext>(options => { options.UseSqlServer(Configuration["ConnectionStrings:ApplicationDbContextConnection"]); });
بنابراین قسمت options.UseSqlServer را یا در اینجا مقدار دهی میکنید و یا از طریق بازنویسی متد OnConfiguring کلاس Context برنامه.
یک نکته: امکان تزریق IConfigurationRoot به کلاس Context برنامه وجود دارد
با توجه به اینکه Context برنامه را به صورت یک سرویس به ASP.NET Core معرفی کردیم، امکان تزریق وابستگیها نیز در آن وجود دارد. یعنی بجای روش فوق، میتوان IConfigurationRoot را به سازندهی کلاس Context برنامه نیز تزریق کرد:
using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; namespace Core1RtmEmptyTest.Entities { public class ApplicationDbContext : DbContext { private readonly IConfigurationRoot _configuration; public ApplicationDbContext(IConfigurationRoot configuration) { _configuration = configuration; } //public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) //{ //} public DbSet<Person> Persons { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(_configuration["ConnectionStrings:ApplicationDbContextConnection"]); } } }
در این حالت متد ConfigureServices کلاس آغازین برنامه، چنین شکلی را پیدا میکند و ساده میشود:
public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ApplicationDbContext>();
یک نکته: امکان تزریق ApplicationDbContext به تمام کلاسهای برنامه وجود دارد
همینقدر که ApplicationDbContext را به عنوان سرویسی در ConfigureServices تعریف کردیم، امکان تزریق آن در اجزای مختلف یک برنامهی ASP.NET Core نیز وجود دارد:
using System.Linq; using Core1RtmEmptyTest.Entities; using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { public class TestDBController : Controller { private readonly ApplicationDbContext _ctx; public TestDBController(ApplicationDbContext ctx) { _ctx = ctx; } public IActionResult Index() { var name = _ctx.Persons.First().FirstName; return Json(new { firstName = name }); } } }
در این حالت پس از اجرای برنامه، خطای ذیل را مشاهده خواهیم کرد:
علت اینجا است که هنوز این بانک اطلاعاتی ایجاد نشدهاست و همچنین ساختار جداول را به آن منتقل نکردهایم که این موارد را در قسمتهای بعدی مرور خواهیم کرد.
OpenCVSharp #5
استفاده از OpenCVSharp در برنامههای WinForms به کمک PictureBoxIpl
یکی از اسمبلیهای کتابخانهی OpenCVSharp را که در پوشهی bin برنامه میتوان مشاهده کرد، OpenCvSharp.UserInterface.dll نام دارد. این اسمبلی حاوی یک picture box جدید به نام PictureBoxIpl است که میتواند تصاویری را با فرمت IplImage، دریافت کند.
میتوانید این picture box ویژه را از طریق منوی ToolBox -> Choose items و سپس صفحهی دیالوگ فوق، به نوار ابزار WinForms اضافه کرده و از آن استفاده کنید و یا میتوان با کدنویسی نیز به آن دسترسی یافت:
using (var iplImage = new IplImage(@"..\..\Images\Penguin.png", LoadMode.Color)) { Cv.Dilate(iplImage, iplImage); var pictureBoxIpl = new OpenCvSharp.UserInterface.PictureBoxIpl { ImageIpl = iplImage, AutoSize = true }; flowLayoutPanel1.Controls.Add(pictureBoxIpl); }
یک نکته
هر نوع تغییری به iplImage پس از انتساب آن به خاصیت ImageIpl، نمایش داده نخواهد شد. برای به حداقل رساندن سربار ایجاد اشیاء جدید (خصوصا برای نمایش اطلاعات رسیدهی از دوربین یا WebCam)، از متد RefreshIplImage استفاده کنید. این متد بجای ایجاد یک شیء جدید، تنها ناحیهی موجود را مجددا ترسیم خواهد کرد و بسیار سریع است:
pictureBoxIpl.RefreshIplImage(iplImage);
استفاده از OpenCVSharp در برنامههای WinForms به کمک PictureBox
اگر نخواهید از کنترل جدید PictureBoxIpl استفاده کنید، میتوان از همان Picture box استاندارد WinForms نیز کمک گرفت:
Bitmap bitmap; using (var iplImage = new IplImage(@"..\..\Images\Penguin.png", LoadMode.Color)) { bitmap = iplImage.ToBitmap(); // BitmapConverter.ToBitmap() } var pictureBox = new PictureBox { Image = bitmap, ClientSize = bitmap.Size }; flowLayoutPanel1.Controls.Add(pictureBox);
یک نکته
در اینجا نیز برای به حداقل رسانی به روز رسانیهای بعدی picture box بهتر است از متد ToBitmap به شکل زیر کمک گرفت:
iplImage.ToBitmap(dst: (Bitmap)pictureBox.Image);
استفاده از OpenCVSharp در برنامههای WPF
در WPF میتوان با استفاده از متد الحاقی ToWriteableBitmap کلاس BitmapConverter، فرمت IplImage را به منبع تصویر یک کنترل تصویر استاندارد، تبدیل کرد:
using System.Windows.Media; using OpenCvSharp; using OpenCvSharp.Extensions; namespace OpenCVSharpSample05Wpf { public partial class MainWindow { public MainWindow() { InitializeComponent(); loadImage(); } private void loadImage() { using (var iplImage = new IplImage(@"..\..\Images\Penguin.png", LoadMode.Color)) { Cv.Dilate(iplImage, iplImage); Image1.Source = iplImage.ToWriteableBitmap(PixelFormats.Bgr24); } } } }
کدهای کامل WPF و WinForms این مطلب برای دریافت.
Dependency Injection
در ادامه مباحث بهتر کد بنویسیم و الگوهایی که در این رابطه معرفی شدند، اخیرا کتابی از انتشارات manning منتشر شده تحت عنوان Dependency Injection . هر چند به ظاهر این کتاب برای جاوا کارها تهیه شده اما قسمت عمدهای از آن برای سایر زبانهای برنامه نویسی دیگر نیز قابل استفاده است.
In object-oriented programming, a central program normally controls other objects in a module, library, or framework. With dependency injection, this pattern is inverted—a reference to a service is placed directly into the object which eases testing and modularity. Spring or Google Guice use dependency injection so you can focus on your core application and let the framework handle infrastructural concerns.
Dependency Injection explores the DI idiom in fine detail, with numerous practical examples that show you the payoffs. You'll apply key techniques in Spring and Guice and learn important pitfalls, corner-cases, and design patterns. Readers need a working knowledge of Java but no prior experience with DI is assumed.
WHAT'S INSIDE:
◊ How to apply it (Understand it first!)
◊ Design patterns and nuances
◊ Spring, Google Guice, PicoContainer, and more
◊ How to integrate DI with Java frameworks
راستی، این کتاب تر و تازه رو میتونید از همین کتاب فروشیهای دور و اطراف نیز تهیه کنید! در سایت booktraining دات ارگ در قسمت graphics-and-design به تاریخ 4 آگوست.
Target Typing در C# 9.0
مشکلی که بعضیها با واژهی کلیدی var دارند، این است که اندکی خوانایی کدها را کاهش میدهد و در این حالت بلافاصله مشخص نیست که نوع شیء در حال استفاده چیست. در C# 9.0 برای این دسته از برنامه نویسها راه حل دیگری را پیشنهاد دادهاند: نوع ابتدایی را مشخص کنید، اما نیازی به ذکر نوع پس از واژهی کلیدی new نیست و همانند var، خود کامپایلر آنرا حدس خواهد زد! برای توضیح آن دو کلاس سادهی زیر را درنظر بگیرید:
public class Person { public string FirstName { get; set; } } public class PersonWithCtor { public PersonWithCtor(string firstName) { this.FirstName = firstName; } public string FirstName { get; set; } }
Person person = new Person();
var person = new Person();
Person person = new();
Target Typing و پارامترهای سازندهی کلاسها در C# 9.0
در مثال فوق، کلاس PersonWithCtor به همراه یک سازندهی پارامتردار تعریف شدهاست. در این حالت Target Typing آن به صورت زیر خواهد بود:
Person person = new("User 1");
var personList = new List<Person> { new ("User 1"), new ("User 2"), // ... };
public void Adopt(Person p) { //... } public void CallerMethod() { this.Adopt(new Person("User 1")); // C# 9.0 this.Adopt(new("User 1")); }
XmlReader.Create(reader, new XmlReaderSettings() { IgnoreWhitespace = true }); // C# 9.0 XmlReader.Create(reader, new() { IgnoreWhitespace = true });
Target Typing و استفاده از خواص کلاسها در C# 9.0
در همان مثال اول، اگر بخواهیم خاصیت FirstName را مقدار دهی کنیم و همچنین از Target Typing نیز استفاده کنیم ... روش زیر کامپایل نخواهد شد:
Person person = new { FirstName = "User 2" };
Person person = new() { FirstName = "User 2" };
امکان استفادهی از Target typing با فیلدها در C# 9.0
امکان تعریف var با فیلدهای یک کلاس در زبان #C وجود ندارد. به همین جهت مجبور هستیم یک چنین تعاریف طولانی را در سطح کلاسها داشته باشیم:
private ConcurrentDictionary<string, ObservableList<Cat>> _catsBefore = new ConcurrentDictionary<string, ObservableList<Cat>>();
private ConcurrentDictionary<string, ObservableList<Cat>> _cats = new(); // C# 9.0
public ObservableCollection<Friend> Friends { get; } = new();
امکان ترکیب null-coalescing operator با target typing در C# 9.0
null-coalescing operator یا همان ?? به این معنا است که اگر متغیر سمت چپ آن نال نبود، همان مقدار درنظر گرفته شود و اگر نال بود، متغیر سمت راست آن بازگشت داده شود. در این حالت مثال زیر را در نظر بگیرید که در آن سگ و گربه از نوع پایهی حیوان تعریف شدهاند:
public interface IAnimal { } public class Dog : IAnimal { } public class Cat : IAnimal { }
Cat cat = null; Dog dog = new(); IAnimal animal = cat ?? dog;
دانستنیهایی در مورد Target Typing
- نوشتن ()throw new مجاز است و نوع پیشفرض آن، System.Exception در نظر گرفته میشود.
- در حالت کار با tuples، نوشتن new اضافی است:
(int a, int b) t = new(1, 2); // "new" is redundant
(int a, int b) t = new(); // OK; same as (0, 0)
محدودیتهای Target Typing در C# 9.0
- امکان نوشتن ()var dog = new وجود ندارد؛ چون نوع سمت راست این انتساب دیگر قابل حدس زدن نیست. نمونهی دیگر آن anonymous type properties است؛ مانند new { Prop = new() } که در آن برای مثال نوع خاصیت Prop قابل حدس زدن نیست.
- target typing با binary operators قابل استفاده نیست.
- به عنوان ref قابل استفاده نیست.
اصل معکوس سازی وابستگیها
1) Dependency inversion principle یا DIP (اصل معکوس سازی وابستگیها)
DIP یکی از اصول طراحی نرم افزار است و D آن همان D معروف SOLID است (اصول پذیرفته شده شیءگرایی).
2) Inversion of Control یا IOC (معکوس سازی کنترل)
الگویی است که نحوه پیاده سازی DIP را بیان میکند.
3) Dependency injection یا DI (تزریق وابستگیها)
یکی از روشهای پیاده سازی IOC است.
4) IOC container
به فریم ورکهایی که کار DI را انجام میدهند گفته میشود.
Dependency inversion principle چیست؟
اصل معکوس سازی وابستگیها به این معنا است که بجای اینکه ماژولهای سطح پایین سیستم، رابطهای قابل استفادهای از خود را در اختیار سطوح بالاتر سیستم قرار دهند، ماژولهای قرار گرفته در سطوحی بالاتر، اینترفیسهایی را تعریف میکنند که توسط ماژولهای سطح پایین پیاده سازی خواهند شد.
همانطور که ملاحظه میکنید به این ترتیب وابستگیهای سیستم معکوس خواهند شد. نمونهای از عدم استفاده از این طراحی را در دنیای واقعی به صورت رومزه با آنها سر و کار داریم؛ مانند وسایل الکترونیکی قابل حملی که نیاز به شارژ مجدد دارند. برای مثال تلفنهای همراه، دوربینهای عکاسی دیجیتال و امثال آن.
هر کدام از اینها، رابطهای اتصالی متفاوتی دارند. یکی USB2، یکی USB3 دیگری Mini USB و بعضیها هم از پورتهای دیگری استفاده میکنند. چون هر کدام از لایههای زیرین سیستم (در اینجا وسایل قابل شارژ) رابطهای اتصالی مختلفی را ارائه دادهاند، برای اتصال آنها به منبع قدرت که در سطحی بالاتر قرار دارد، نیاز به تبدیلگرها و درگاههای مختلفی خواهد بود.
اگر در این نوع طراحیها، اصل معکوس سازی وابستگیها رعایت میشد، درگاه و رابط اتصال به منبع قدرت باید تعیین کننده نحوه طراحی اینترفیسهای لایههای زیرین میبود تا با این آشفتگی نیاز به انواع و اقسام تبدیلگرها، روبرو نمیشدیم.
اگر وابستگیها معکوس نشوند مطابق تصویر فوق، کلاس سطح بالایی را خواهیم داشت که به اینترفیس کلاسهای سطح پایین وابسته است. البته در اینجا اینترفیس یک کلمه عمومی است و بیشتر نحوه در معرض دید و استفاده قرار دادن اعضای یک کلاس مد نظر بوده است تا اینکه مثلا الزاما اینترفیسهای زبان خاصی مدنظر باشند.
مشکلی که در این حالت به زودی بروز خواهد کرد، افزایش کلاسهای سطح پایین و بیشتر شدن وابستگی کلاسهای سطح بالا به آنها است. به این ترتیب قابلیت استفاده مجدد خود را از دست خواهند داد.
در تصویر فوق حالتی را مشاهده میکنید که وابستگیها معکوس شدهاند. تغییر مهمی که در اینجا نسبت به حالت قبل رخ داده است، بالا بردن اینترفیس، به بالای خط میانی است که در تصویر مشخص گردیده است. این خط، معرف تعریف لایههای مختلف سیستم است. به عبارتی کلاسهای سطح بالا در لایه دیگری نسبت به کلاسهای سطح پایین قرار دارند. در اینجا اجازه دادهایم تا کلاس لایه بالایی اینترفیس مورد نیاز خود را تعریف کند. این نوع اینترفیسها در زبان سی شارپ میتوانند یک کلاس Abstract و یا حتی یک Interface متداول باشند.
با معکوس شدن وابستگیها، لایه سطح بالا است که به لایه زیرین عنوان میکند: تو باید این امکانات را در اختیار من قرار دهی تا بتوانم کارم را انجام دهم.
اکنون اگر در یک سیستم واقعی تعداد کلاسهای سطح پایین افزایش پیدا کنند، نیازی نیست تا کلاس سطح بالا تغییری کند. کلاسهای سطح پایین تنها باید عملکردهای تعیین شده در اینترفیس را پیاده سازی کنند. و این برخلاف حالتی است که وابستگیها معکوس نشدهاند:
تاریخچه اصل معکوس سازی وابستگیها
اصل معکوس سازی وابستگیها در نشریه C++ Report سال 1996 توسط شخصی به نام Bob Martin (معروف به Uncle Bob!) برای اولین بار مطرح گردید. ایشان همچنین یکی از آغاز کنندگان گروهی بود که مباحث Agile را ارائه کردند. به علاوه ایشان برای اولین بار مباحث SOLID را در دنیای شیءگرایی معرفی کردند (همان مباحث معروف هر کلاس باید تک مسئولیتی باشد، باز باشد برای توسعه، بسته برای تغییر و امثال آن که ما در این سری مباحث قسمت D آنرا در حالت بررسی هستیم).
مطابق تعاریف Uncle Bob:
الف) ماژولهای سطح بالا نباید به ماژولهای سطح پایین وابسته باشند. هر دوی اینها باید به Abstraction وابسته باشند.
ب) Abstraction نباید وابسته به جزئیات باشد. جزئیات (پیاده سازیها) باید وابسته به Abstraction باشند.
مثال برنامه کپی
اگر به مقاله Uncle Bob مراجعه کنید، یکی از مواردی را که عنوان کردهاند، یک برنامه کپی است که میتواند اطلاعات را از صفحه کلید دریافت و در یک چاپگر، چاپ کند.
حال اگر به این مجموعه، ذخیره سازی اطلاعات بر روی دیسک سخت را اضافه کنیم چطور؟ به این ترتیب سیستم با افزایش وابستگیها، پیچیدگی و if و elseهای بیشتری را خواهد یافت؛ از این جهت که سطح بالایی سیستم به صورت مستقیم وابسته خواهد بود به ماژولهای سطح پایین آن.
روشی را که ایشان برای حل این مشکل ارائه دادهاند، معکوس کردن وابستگیها است:
در اینجا سطح بالایی سیستم وابسته است به یک سری تعاریف Abstract خواندن و یا نوشتن؛ بجای وابستگی مستقیم به پیاده سازیهای سطح پایین آنها.
در این حالت اگر تعداد Readers و یا Writers افزایش یابند، باز هم سطح بالایی سیستم نیازی نیست تغییر کند زیرا وابسته است به یک اینترفیس و نه پیاده سازی آن که محول شده است به لایههای زیرین سیستم.
این مساله بر روی لایه بندی سیستم نیز تاثیرگذار است. در روش متداول برنامه نویسی، لایه بالایی به صورت مستقیم متدهای لایههای زیرین را صدا زده و مورد استفاده قرار میدهد. به این ترتیب هر تغییری در لایههای مختلف، بر روی سایر لایهها به شدت تاثیرگذار خواهد بود. اما در حالت معکوس سازی وابستگیها، هر کدام از لایههای بالاتر، از طریق اینترفیس از لایه زیرین خود استفاده خواهد کرد. در این حالت هرگونه تغییری در لایههای زیرین برنامه تا زمانیکه اینترفیس تعریف شده را پیاده سازی کنند، اهمیتی نخواهد داشت.
مثال برنامه دکمه و لامپ
مثال دیگری که در مقاله Uncle Bob ارائه شده، مثال برنامه دکمه و لامپ است. در حالت متداول، یک دکمه داریم که وابسته است به لامپ. برای مثال وهلهای از لامپ به دکمه ارسال شده و سپس دکمه آنرا کنترل خواهد کرد (خاموش یا روشن). مشکلی که در اینجا وجود دارد وابستگی دکمه به نوعی خاص از لامپ است و تعویض یا استفاده مجدد از آن به سادگی میسر نیست.
راه حلی که برای این مساله ارائه شده، ارائه یک اینترفیس بین دکمه و لامپ است که خاموش و روشن کردن در آن تعریف شدهاند. اکنون هر لامپی (یا هر وسیله الکتریکی دیگری) که بتواند این متدها را ارائه دهد، در سیستم قابل استفاده خواهد بود.