SOLID مخفف چند مفهوم مختلف هست که فهم این مطالب میتواند شما را در هنر برنامه نویسی به سطح پیشرفتهتری برساند.
اصول SOLID در توسعه نرمافزار اصولی هستند که توسط رابرت مارتین معرفی شدهاند. در اوایل ۲۰۰۰ آقای مارتین ۵ اصل را تشریح کرد که توسعهدهندگان نرمافزار میتوانند از آنها برای ایجاد نرمافزارهای با طراحی خوب، کیفیت بالا و سهولت نگهداری استفاده کنند. این ۵ اصل ساده هستند، مروج شیوههای خوب در طراحی و تولید نرمافزارند و بسیار مهم در مدیریت وابستگی (Dependency Management) در توسعه ی برنامه های شی گرا می باشد و در TDD نیز به ما کمک میکنند.
یکی از مشکلاتی که طراحی نامناسب برنامه های شی گرا برای برنامه نویسان ایجاد می کند موضوع مدیریت وابستگی در اجزای برنامه می باشد. اگر این وابستگی به درستی مدیریت نشود مشکلاتی شبیه موارد زیر در برنامه ایجاد می شوند:
برنامه ی نوشته شده را نمی توان تغییر داد و یا قابلیت جدید اضافه کرد. دلیل آن هم این است که با ایجاد تغییر در قسمتی از برنامه، این تغییر به صورت آبشاری در بقیه ی قسمت ها منتشر می شود و مجبور خواهیم بود که قسمت های زیادی از برنامه را تغییر دهیم. یعنی برنامه به یک برنامه ی ثابت و غیر پیشرفت تبدیل می شود. (این مشکل را Rigidity می نامیم.)
تغییر دادن برنامه مشکل است و آن هم به این دلیل که با ایجاد تغییر در یک قسمت از برنامه، قسمت های دیگر برنامه از کار می افتند و دچار مشکل می شوند. (این مشکل را Fragility می نامیم.)
قابلیت استفاده مجدد از اجزای برنامه وجود ندارد. در واقع، قسمت های مجدد برنامه ی شی گرای شما آنچنان به هم وابستگی تو در تو دارند که به هیچ وجه نمی توانید یک قسمت را جدا کرده و در برنامه ی دیگری استفاده کنید. (این مشکل را Immobility می نامیم.(
اصول SOLID که قصد رفع کردن این مشکلات و بسیاری مسائل گوناگون را دارد عبارت اند از:
Single Responsibility Principle
Open-Closed Principle
Liskov Substitution Principle
Interface Segregation Principle
Dependency Inversion Principle
با کنار هم گذاشتن حرف اول هر کدام از این اصول کلمه ی SOLID ایجاد می شود. با در نظر گرفتن این پنج اصل و پیاده سازی آنها در برنامه های خود می توانید به یک طراحی شی گرا پاک و درست دست پیدا کنید.
(Single responsibility principle (SRP به معنی اینکه هر کلاس بایستی فقط یک کار انجام دهد نه بیشتر.
(Open-Closed principle (OCP به معنی اینکه کلاسها جوری نوشته بشن که قابل گسترش باشند اما نیاز به تغییر نداشته باشند.
(Liskov substitution principle (LSP به مفهوم اینکه هر کلاسی که از کلاس دیگر ارث بری میکند هرگز نباید رفتار کلاس والد را تغییر دهد.
(Interface segregation principle (ISP به مفهوم اینکه چند اینترفیس کوچک و خورد شده همیشه بهتر از یک اینترفیس کلی و بزرگ است.
(Dependency inversion principle (DIP به معنی اینکه از اینترفیسها به خوبی استفاده کن!
این اصول حاصل تجربیات تعداد زیادی از محققات و توسعه دهندگان نرم افزار است. تیم های چابک این اصول را برای رفع مشکلات طراحی به کار میبرند.
ما این اصول رو یاد میگیریم تا بتوانیم نرم افزار بهتری را ایجاد کنیم یا به عبارتی اصولیتر کد نویسی کنیم.
Single Responsibility Principle
"به دنبال ماژولهای تک مسئولیتی باش"
هر کلاس بایستی فقط و فقط یک وظیفه را برعهده داشته باشد. وقتی دو دلیل مختلف برای تغییر یک کلاس وجود داشته باشد بنابراین ممکن است دو تیم مختلف این کار را انجام دهند. در نهایت یک کلاس توسط دو تیم مختلف ویرایش می شود و این سبب می شود تا پروسه سوال و جواب برای هماهنگی و … طولانی شود.
هدف این قانون جدا سازی مسئولیتهای چسبیده به هم است. به عنوان مثال کلاسی که هم مسئول ذخیره سازی و هم مسئول ارتباط با واسط کاربر است، این اصل را نقض میکند و باید به دو کلاس مجزا تقسیم شود.
دلیل اینکه باید هر کلاس یک مسئولیت داشته باشد این است که هر مسئولیت یک بعد برای تغییر است. وقتی که یک نیازمندی تغییر میکند باید کلاسی که مسئول آن است، تغییر کند. این تغییر ممکن است باعث اختلال در مسئولیت دیگری شود که این کلاس بر عهده دارد. این نوع چسبیدگی مسئولیتها منجر به طراحی شکننده میشود به طوری که ممکن است ما را با خطاهای غیر منتظرهای مواجه سازد.
ایده Single Responsibility Principle یا به اختصار SRP این است که هر متد یا کلاس در برنامه شما باید تنها یک دلیل برای تغییر داشته باشد. به صورت منطقی، میتوان این ایده را اینطور گسترش داد که هر کلاس یا متد در برنامه باید دقیقاً فقط یک وظیفه (یا کار) داشته باشد. به عبارت بهتر، هر کلاس یا متد باید در برابر یک و فقط یک وظیفه مسئول باشد.
به عنوان مثال اجازه بدهید کلاسی را با توابعی برای سبد خرید در یک سایت فروشگاه الکترونیک را فرض کنیم. به عنوان یک سبد خرید مجازی منطقی است که کلاس یک مجموعه (collection) از مواردی که کاربر آنها را جهت خرید به سبد خود اضافه کرده و احتمالاً یک راه برای برقراری ارتباط با سرویسی جهت پرداخت داشته باشد. برای توسعه دادن این مثال فرض کنیم که این فروشگاه الکترونیک یک روال امتیاز وفاداری (loyalty reward) دارد که به مشتریان بر اساس خریدشان امتیاز جایزه میدهد.
هیچکدام از قابلیتهای برای جایزه دادن، رهگیری یا مدیریت امتیازات، مناسب اضافه شدن به کلاس سبد خرید نیستند. این قابلیت مربوط به امتیازات باید در سرویس جدایی ایجاد شود. سبد خرید نباید مسئول امتیازات باشد و در واقع حتی نباید از وجود برنامه امتیازات خبر داشته باشد. سبد خرید فقط یک کار دارد: ذخیره لیست مواردی که کاربر قصد خرید آنها را دارد. بر این اساس کلاس سبد خرید فقط یک دلیل برای تغییر دارد: زمانی که روش ذخیرهسازی آیتمهای لیست خرید مشتری تغییر کند. تغییر در برنامه امتیازدهی به مشتریان، نباید هیچ تاثیری در سبد خرید داشته باشد بنابراین وقتی برنامه امتیازدهی تغییر میکند نیازی به تغییر کلاس سبد خرید نیست.
با اطمینان از اینکه متدها و کلاسهایی مینویسیم که فقط یک وظیفه دارند، این متدها را راحتتر قابل آزمایش میکنیم. متدهایی که کارهای زیادی انجام میدهند، نیازمند تستهایی با Arrange پیچیدهتر هستند که باعث طولانی و مشکل شدن فهم و نگهداری تستها میشود.
همچنین میتوان SRP را به تستها و روشی که آنها را مینویسیم نیز تعمیم داد. در حال ایدهآل هر تستی که مینویسیم فقط باید یک چیز را مورد آزمایش قرار دهد. این باعث زیاد شدن تستها میشود اما مزایای خاص خود را دارد. اول اینکه خود تستها سادهتر نوشته شده و راحتتر فهمیده میشوند. نکته مهم دیگر این است که وقتی تست شما fail میشود، اطلاعات خوب و مشخصی راجع به اینکه کجا دنبال مشکل بگردید خواهید داشت. اگر تست fail شود و فقط یک کار تست شده باشد، فقط یک محل برای بررسی وجود خواهد داشت. زمانی که یک تست بیش از یک کار انجام میدهد، در هنگام fail شدن باید زمان بیشتری را صرف پیدا کردن مشکل کرد.
Open/Close Principle
"پذیرای توسعه و بازدارنده از تغییر هر آنچه که هست، باشید"
منظور از OCP این است که برنامه نویس بایستی کلاسها و اشیاء را طوری ایجاد نماید که همواره امکان گسترش (Extend) آن وجود داشته باشد اما برای گسترش نیازی به تغییر در کلاسهای اصلی نباشد. یعنی اینکه کدها و کلاسها بایستی همواره برای گسترش باز باشند اما برای ویرایش و تغییر بسته باشند. البته منظور از بسته بودن برای ویرایش این است که نیازی به تغییر کدها نباشد. در بهترین حالت این اصل در برنامه نویسی شی گرا باعث کاهش ارتباطات کلاسها و ماژولها خواهد شد. در نهایت امکان توسعه از طریق افزودن کلاسهای جدید و نه تغییر کلاسهای موجود میسر خواهد شد.
با کمی دقت متوجه میشویم که OCP و SRP مکمل همدیگر هستند. البته به این معنی نیست که هر جا SRP را رعایت کرده باشیم پس OCP را نیز رعایت کردهایم. اما با رعایت هر یک از اصول دستیابی به مورد دیگر راحت تر و ساده تر خواهد بود.
اصل Open/Close یا به اختصار OCP ارتباط نزدیکی با مباحث Encapsulation و Inheritance دارد. در حقیقت میتوان گفت OCP ایدهای است که این دو قانون OOP را با هم متحد میکند. OCP بیان میکند که در نرمافزار، خواه متد یا کلاس، باید راه برای توسعه (extension) باز و برای تغییر (modification) بسته باشد. برای اینکه بهتر متوجه دو بخش این عبارت شویم، هر کدام را به صورت جداگانه بررسی میکنیم.
وقتی برنامهنویسان یک نرمافزار مینویسند اغلب متکی به کتابخانههای نرمافزاری نوشته شده توسط سایر برنامهنویسان هستند. به منظور اینکه این اجزا (components) به صورت گسترده مورد استفاده قرار میگیرند، قابلیتهای آنها را در کلیترین (general) حالات در نظر میگیرند. اغلب اوقات که از قابلیتهای این component ها به همان صورتی که هست استفاده میکنیم، به معنی این است که نیاز ما در دایره حالات کلی تعریف شده در آن کتابخانه قرار دارد. اما بعضی اوقات ممکن است به نسخه خاصتری از این component ها نیاز داشته باشیم.
بر اساس OCP این component ها را باید بتوان گسترش داد (راه برای extension باز باشد). راههای مختلفی برای این کار وجود دارد: مشخصترین راه ایجاد یک کلاس جدید مشتق شده از کلاس پایه (base) مربوط به component است که یا متدهای موجود آن را override کند یا متدهای جدیدی را بر اساس نیاز به آن اضافه کند.
راه دیگری که نسبت به راه قبلی کمتر واضح است این است که از یک اصل دیگر SOLID به نام Dependency Inversion استفاده کنیم که در ادامه دربارهاش توضیح خواهم داد.
این دو راه به من کمک میکند که قابلیتهای یک کلاس را توسعه یا تغییر بدهم بدون اینکه داخل آن را دستکاری کنم چرا که “راه برای توسعه باز است”
دومین بخش OCP میگوید که راه برای تغییرات بسته است. این بخش با مفاهیم Encapsulation در ارتباط است و بیان میکند که با کارهای داخلی component ها باید به صورت خصوصی (private) برخورد شود. در این شرایط OCP میگوید که اگر میخواهید یک قابلیت اضافی به component اضافه کنید یا روش کار قابلیت موجود را تغییر دهید، گزینههای کمی در اختیار دارید و تغییر داخل یک component به نحوی که Public API (یا قانون Encapsulation) را تحت تاثیر قرار دهد یکی از آن گزینهها نیست!
بسته نگهداشتن base component ها تضمین میکند که دیگر componentهای وابسته به آنها از تغییرات غیرمنتظره مربوط به قابلیتهای جدید زجر نکشند. همچنین تضمین میکند که با آمدن آپدیت برای آن componentها شما میتوانید آن به روزرسانیها را با برنامه خود ترکیب (integrate) کنید.
وقتی در این نوشته و نوشته بعدی شروع به صحبت درباره Dependency Inversion کنیم OCP در TDD بیشتر با معنی میشود. اما ذات OCP در mocking (که در نوشتههای بعدی به آن خواهیم پرداخت) به ما کمک میکند که مطمئن شویم راه mocking در کلاسهای ما با Dependency Inversion باز است.
Liskov Substitution Principle
"ارث بری باید به صورتی باشد که زیر نوع را بتوان بجای ابر نوع استفاده کرد"
یکی از اصول دیگر برنامه نویسی شی گرا و اصل سوم SOLID مفهوم LSP یا Liskov Substitution Principle می باشد، به این معنی که هیچ کلاسی نباید رفتار کلاس والد را تغییر دهد. برای رعایت این اصل باید در نظر داشته باشیم که هر کلاسی میتواند از کلاس دیگر ارث بری کند به شرطی که رفتار کلاس والد را تغییر ندهند.
اصل Liskov Substitution یا به اختصار LSP بیان میکند که یک شی در برنامه شما باید قابلیت جایگزینی با شی از کلاسی که از آن مشتق شده است را بدون ایجاد مشکل در برنامه داشته باشد. به عنوان مثال در بحث قبلی که در خصوص Polymorphism در روز دوم داشتیم درباره ایده super class و Public API در آن صحبت کردیم. برای یادآوری اگر یک کلاس از کلاس پایه (base) ارث بری کرده باشد آن وقت کلاس پایه super class و کلاس ارث بری شده کلاس مشتق (derived) نامیده میشود. به عنوان مثال Animal یک super class برای Dog است در حالی که Dog یک کلاس مشتق شده از Animal است.
بر اساس LSP اگر برنامه من انتظار یک شی از نوع Animal داشته باشد، من باید بتوانم هر کلاسی که از Animal مشتق شده را به جای آن پاس بدهم (مثلاً Dog, Cat, Fish و…) بدون اینکه مشکل و ایرادی در برنامه ایجاد شود. برنامه با این شی مشتق شده به عنوان یک Animal کلی (Generic) برخورد میکند (یعنی فقط متدهای Public API مربوط به Animal را میتوان برای آن شی فراخوانی کرد) و لازم نیست بداند یا اهمیت بدهد که واقعاً چه نوع کلاسی پاس داده شده است.
مثل OCP قدرت اصلی LSP وقتی مشخص میشود که درباره Dependency Inversion صحبت کنیم. LSP به همراه OCP و Dependency Inversion امکان mocking را میدهد. به صورت خلاصه LSP به قابل آزمایش کردن کدهای ما از طریق ایجاد یک جایگزین برای کلاسهای وابسته در کد کمک میکند که خودش باعث ایزوله شدن تست از برنامه و وابستگیهایش میشود.
Interface Segregation Principle
"واسطهای کوچک بهتر از واسطهای حجیم است"
یکی از اصول دیگر برنامه نویسی شی گرا و اصول SOLID، مفهوم ISP یا Interface Segregation Principle میباشد، به این معنی که برای استفاده از اینترفیس ها آنها را باید به اجزای کوچکتری تقسیم کرد. وقتی یک کلاس از یک اینترفیس بزرگ استفاده میکند ممکن است برخی از این متدها در کلاس مورد نظر قابل استفاده نباشند. اما وقتی یک اینترفیس بزرگ به چند اینترفیس کوچک تقسیم میشود هر کلاس میتواند در صورتی که به اینترفیس خاصی نیاز داشت از آن استفاده نماید. با این امکان اگرچه تعداد اینترفیسها بیشتر میشوند و ممکن است تکرار رخ دهد اما به دلیل اینکه منطق برنامه ما در اینترفیس ها اجرا نمی شود میتوان این مسئله را نادیده گرفت. در نهایت با رعایت این اصل امکان دیباگ و بررسی کدها سرعت بیشتری خواهد داشت.
اصل Interface Segregation یا به اختصار ISP بیان میکند که مشتریها نباید مجبور به استفاده از interface هایی شوند که استفاده نمیکنند. در واقع شما باید interface های خوبی که به طور خاص برای نیاز و قابلیت مورد نظر مشتری هستند ایجاد کنید.
به عنوان مثال شما ممکن است سرویسی برای پذیرش تمام درخواستهای وام یک بانک داشته باشید. اما مشتری شما ممکن است بین وامهای امن (مثل وام مسکن و خودرو و …) و وامهای ناامن (کارت اعتباری و …) تفاوت قائل شوند. به علاوه، API سرویس شما ممکن است متدهای مختلفی برای انواع مختلف وام داشته باشد. بر اساس ISP ایجاد یک intreface کلی که تمام این حالات را پوشش دهد راه اشتباهی است، به جای این کار شما باید چندین interface کوچکتر داشته باشید که نیاز تجاری خاصی را هدف گرفتهاند.
مزیت این اصل، خیلی با اهمیت آن در TDD در ارتباط است. یک interface بزرگ که پر از متدها و property هایی است که مشتری به ندرت از آنها استفاده میکند، interface را سنگین و پیچیده میکند. در TDD اینترفیسهای کوچکتر را راحتتر میتوان mock کرد که باعث میشود نتایج تست ما کوچکتر و با پیچیدگی کمتر و فهم آنها راحتتر باشند.
Dependency Inversion Principle
"وابستگی بین ماژولها را به وابستگی آنها به انتزاع (واسط) تغییر بده"
DIP مفهومی است که وابستگی مستقیم کلاسهای سطح بالا را به کلاسهای سطح پایین منع میکند. به این منظور که اگر کلاس خاصی(high-level) که از کلاس های دیگر(low-level) استفاده میکند وابستگی مستقیمی با کلاس های low-level داشته باشد سبب بروز این مشکل خواهد شد که اگر کلاس low-level دیگری به مجموعه افزوده شود اجبارا کلاس high-level نیز بایستی تغییر کند. DIP برای حل این مشکل به وجود آمده است.
اتصال (Coupling) و انقیاد (binding) در نرمافزار یک حقیقت است. با همه تلاشهایی که میکنیم، در آخر کلاس ما برای اینکه قابل استفاده باشد باید بتواند چیزی را bind کند. بر این اساس، ما باید این اتصالات را تا حد ممکن شل کنیم. این جایی است که اصل Dependency Inversion یا به اختصار DIP وارد میشود.
DIP ایدهای است که میگوید کد باید به یک چیز انتزاعی (abstractions) وابسته باشد نه یک پیادهسازی (implementation) واقعی. به علاوه آن abstratcion ها نباید به جزئیات وابسته باشند و جزئیات نیز نباید به abstraction وابسته باشد.این یک راه پیچیده برای بیان یک ایده ساده است.
به عنوان مثال شما یک نرمافزار منابع انسانی فروختهاید، برنامه توسط سرویسهای دیتابیس مختلفی قابل استفاده است. برنامه یک بخش مدیریت کارمندان دارد که در کنار کارهای دیگر، اطلاعات کارمندان را در دیتابیس سازمان به روز میکند. مدیریت کارمندان، احتمالاً بخشی دارد که دسترسی به دیتابیس را کنترل میکند. شما نبایست بخش مدیریت کارمندان را طوری بنویسید که به MS SQL Server یا Oracle وابسته باشد، به جای این کار، بایستی مدیریت کارمندان را طوری بنویسید که به یک سرویس داده کلی (generic) وابسته باشد که MS SQL Server یا Oracle را بتوان از آن سرویس generic مشتق کرد. در این حالت موقع نصب میتوانم هر یک از این دیتابیسها را که از سرویس داده پایه من مشتق شده باشند را استفاده کنم. در مثال نرمافزار منابع انسانی، ما وابستگی را invert کردهایم، به جای اینکه به MS SQL Server یا Oracle و جزئیاتشان وابسته باشیم به یک abstraction که هر دو این دیتابیسها به آن وابسته هستند، وابستگی ایجاد میکنیم.
لطفاً توجه کنید که Dependency Inversion مشابه Dependency Injection نیست. Dependency Injection یک روش برای رسیدن به Dependency Inversion است، ولی این دو با هم یکی نیستند. Dependency Injection را در نوشته بعدی مورد بررسی قرار خواهیم داد. Dependency Injection یک بخش بسیار مهم در TDD است و در نوشته بعدی خواهید دید که چطور DIP و Dependency Injection به ما در استفاده از mocking در تستهایمان کمک میکند.