مقدمه
1. اصول SOLID چیست؟
اصول SOLID مجموعه ای از اصول طراحی برای نوشتن نرم افزار قابل نگهداری و توسعه پذیر است. هر اصل بر جنبه خاصی از طراحی نرم افزار تمرکز دارد و هدف آن ارتقای ماژولار بودن، انعطاف پذیری و استحکام است. این اصول به شرح زیر است:
- اصل مسئولیت واحد (SRP): یک کلاس باید تنها یک دلیل برای تغییر داشته باشد. به این معنی که یک کلاس باید فقط یک وظیفه داشته باشد.
- اصل باز/بسته (OCP): نهادهای نرم افزار باید برای توسعه باز باشند اما برای اصلاح بسته شوند.
- اصل جایگزینی لیسکوف (LSP): اشیاء یک سوپرکلاس باید با اشیاء زیر کلاسهای آن بدون تأثیر بر صحت برنامه قابل تعویض باشند.
- اصل جداسازی رابط (ISP): کلاینت ها نباید مجبور شوند به واسط هایی که استفاده نمی کنند وابسته باشند.
- اصل وارونگی وابستگی (DIP): ماژول های سطح بالا نباید به ماژول های سطح پایین وابسته باشند. هر دو باید به انتزاعات بستگی داشته باشند.
2. اصول SOLID چه کاربردهایی دارد؟
اصول SOLID به عنوان دستورالعملی برای طراحی و ساختار سیستم های نرم افزاری به گونه ای استفاده می شود که قابلیت نگهداری، قابلیت استفاده مجدد و آزمایش پذیری را ارتقا دهد. با رعایت این اصول، توسعهدهندگان میتوانند کدی ایجاد کنند که درک، اصلاح و گسترش آن آسانتر باشد. اصول SOLIDدر دستیابی به تفکیک نگرانی ها، کاهش تکرار کد، بهبود انعطاف پذیری کد، و امکان تست واحد آسان تر کمک می کند. به کارگیری اصول SOLIDهمچنین منجر به کدهای ماژولارتر و جفتشدهتر میشود و مدیریت وابستگیها و ایجاد تغییرات را بدون ایجاد اثرات موجدار گستردهتر آسانتر میکند.
3. چرا باید از اصول SOLIDاستفاده کنیم؟
استفاده از اصول SOLID در توسعه نرم افزار مزایای متعددی را به همراه دارد. اولاً، اصول SOLIDکدی را ترویج میکنند که درک، اصلاح و نگهداری آن در طول زمان آسانتر است. آنها چارچوبی را برای طراحی سیستم هایی ارائه می دهند که کمتر مستعد اشکال هستند و در هنگام ایجاد تغییرات خطر کمتری برای ایجاد عوارض جانبی ناخواسته دارند. اصول SOLIDهمچنین قابلیت استفاده مجدد کد را تسهیل میکند، زیرا آنها ایجاد مؤلفههایی را تشویق میکنند که به راحتی میتوانند به زمینههای مختلف متصل شوند. علاوه بر این، با پیروی از اصول SOLID، نرمافزار با نیازهای در حال تکامل سازگارتر میشود و معرفی ویژگیهای جدید یا اصلاح ویژگیهای موجود را بدون تعمیرات اساسی کد آسانتر میکند. در نهایت، استفاده از اصول SOLIDبه توسعه دهندگان کمک می کند تا سیستم های نرم افزاری با کیفیت بالا، قوی و مقیاس پذیر بسازند.
اصل مسئولیت واحد (Single Responsibility Principle - SRP)
اولین اصل از اصول SOLID، اصل مسئولیت واحد (SRP) است که بیان می کند که یک کلاس باید تنها یک دلیل برای تغییر داشته باشد. به عبارت دیگر، یک کلاس باید یک مسئولیت یا هدف واحد داشته باشد. با پایبندی به این اصل، ما اطمینان حاصل میکنیم که هر کلاس یا ماژول در سیستم نرمافزاری ما بر روی یک کار خاص متمرکز است و یک مسئولیت واحد و کاملاً تعریف شده را در بر میگیرد. این کدی را ترویج میکند که درک، نگهداری و اصلاح آن آسانتر است. هنگامی که یک کلاس چندین مسئولیت دارد، تغییرات در یک جنبه ممکن است به طور ناخواسته بر سایر قسمتهای کد تأثیر بگذارد و منجر به اشکالات غیرمنتظره و مشکلاتی در مدیریت نرم افزار شود. با پیروی از این اصل، هدف ما برای اجزای بسیار منسجم و با اتصال ضعیف است که به یک سورس کد قابل نگهداری و انعطاف پذیرتر کمک میکند.
مثال:
class UserManager { void authenticateUser(String username, String password) { // Authentication logic goes here } void saveUserData(String username, UserData userData) { // Data storage logic goes here } }
در مثال بالا، ما یک کلاس UserManager داریم که دو مسئولیت دارد: احراز هویت کاربر و ذخیره داده های کاربر در پایگاه داده. با این حال، این مثال اصل مسئولیت واحد را نقض می کند زیرا کلاس دو مسئولیت متفاوت دارد. برای پایبندی به SRP، میتوانیم کد را به دو کلاس جداگانه تبدیل کنیم که هر کدام یک مسئولیت دارند.
class UserAuthenticator { void authenticateUser(String username, String password) { // Authentication logic goes here } } class UserDataManager { void saveUserData(String username, UserData userData) { // Data storage logic goes here } }
در کد بازسازی شده، دو کلاس UserAuthenticatorو UserDataManagerایجاد کردهایم. کلاس UserAuthenticator تنها مسئول احراز هویت کاربران است، در حالی که کلاس UserDataManager مسئول ذخیره دادههای کاربر است. اکنون هر کلاس یک مسئولیت دارد که باعث میشود کد قابل نگهداری تر، توسعه پذیرتر و پایبند به اصل SRP باشد.
اصل باز/بسته (Open/Closed Principle - OCP)
دومین اصل از اصول SOLID، اصل باز/بسته (OCP) است که بیان میکند که موجودیتهای نرمافزار (کلاسها، ماژولها، توابع و غیره) باید برای توسعه باز باشند اما برای اصلاح بسته باشند. به عبارت دیگر، هنگامی که یک جزء نرم افزاری به درستی پیاده سازی شد و به درستی کار کرد، نباید مستقیماً برای تطبیق با نیازهای جدید یا در حال تغییر اصلاح شود. در عوض، کامپوننت باید به گونهای طراحی شود که امکان گسترش و سفارشی سازی از طریق کد اضافی را بدون تغییر سورس کد موجود فراهم کند. با پایبندی به اصل باز/بسته، ما در تلاش هستیم تا سیستمهای نرمافزاری را ایجاد کنیم که انعطافپذیرتر، قابل استفادهتر و راحتتر در طول زمان نگهداری شوند.
مثال:
interface Shape { } class Rectangle implements Shape { double width; double height; Rectangle(this.width, this.height); } class Circle implements Shape { double radius; Circle(this.radius); } class AreaCalculator { double calculateTotalArea(List<Shape> shapes) { double totalArea = 0.0; for (var shape in shapes) { if (shape is Rectangle) { totalArea += (shape as Rectangle).width * (shape as Rectangle).height; } else if (shape is Circle) { totalArea += 3.14 * (shape as Circle).radius * (shape as Circle).radius; } } return totalArea; } }
در مثال بالا سه کلاس Shape، Rectangle و Circle داریم. کلاسهای Rectangleو Circle رابط Shape را پیاده سازی میکنند.
کلاس AreaCalculator مسئول محاسبه مساحت کل لیست اشکال است که پایبندی به اصل Open/Closed را نقض میکند. توجه داشته باشید که کلاس AreaCalculator هنگام معرفی یک شکل جدید نیاز به اصلاح دارد. به عنوان مثال، اگر یک شکل جدید مانند مثلث اضافه کنیم، کلاس AreaCalculator باید اصلاح شود و یک شرط جدید به آن برای محاسبه مساحت مثلث اضافه کنیم.
مثال بالا را طوری پیاده سازی میکنیم که پایبندی به اصل دوم را نشان دهد:
abstract class Shape { double area(); } class Rectangle implements Shape { double width; double height; Rectangle(this.width, this.height); @override double area() { return width * height; } } class Circle implements Shape { double radius; Circle(this.radius); @override double area() { return 3.14 * radius * radius; } } class AreaCalculator { double calculateTotalArea(List<Shape> shapes) { double totalArea = 0.0; for (var shape in shapes) { totalArea += shape.area(); } return totalArea; } }
در این مثال به رابط Shape متد area را اضافه میکنیم دو کلاس Rectangle و Circle نیز باید تغییر پیدا کنند و متد area را پیاده سازی کنند چرا که از رابط (Interface) Shape ارثبری کردهاند و میدانیم هر وقت کلاسی از یک رابط ارث بری کند باید تمامی متدهای آن رابط را پیاده سازی کند. هر کدام از کلاسهای Rectangle و Circle پیاده سازی مختص خود را برای متد area ارائه میکنند.
الان کلاس AreaCalculator پایبندی به اصل Open/Closed را نشان می دهد. این کلاس هنگام معرفی یک شکل جدید نیازی به اصلاح ندارد. مانند مثال قبل، اگر یک شکل جدید مانند مثلث اضافه کنیم، میتوانیم به سادگی یک کلاس جدید ایجاد کنیم که رابط Shape را پیادهسازی کرده و پیادهسازی خود را برای متد area ارائه دهد. کلاس AreaCalculatorبرای اصلاح بسته باقی می ماند، اما برای گسترش باز است، و اجازه می دهد تا اشکال جدید بدون تغییر کد موجود به آن اضافه شود.
این مثال نشان میدهد که چگونه پایبندی به اصل باز/بسته سیستم را قادر میسازد تا به راحتی با اشکال جدید بدون نیاز به اصلاح کلاسهای موجود گسترش یابد، قابلیت استفاده مجدد کد را ارتقاء میدهد و خطر ایجاد اشکال در کدهای قبلی را به حداقل میرساند.
اصل جایگزینی لیسکوف (Liskov Substitution Principle - LSP)
سومین اصل از اصول SOLID، اصل جایگزینی لیسکوف (LSP) است که بیان میکند که اشیاء یک سوپرکلاس باید با اشیاء زیر کلاسهای آن بدون تأثیر بر صحت برنامه قابل تعویض باشند. به عبارت دیگر، اگر برنامهای برای استفاده از یک نوع شیء خاص نوشته شده باشد، باید بتواند با هر نوع فرعی از آن شی نیز به درستی کار کند. LSP تضمین میکند که کلاسها یا زیر کلاسهای مشتق شده به قراردادها و رفتارهای تعریف شده توسط کلاسهای پایه خود احترام میگذارند. با پایبندی به این اصل، کدی را توسعه میدهیم که قابل نگهداری، توسعهپذیرتر و قویتر است، زیرا امکان تعویض آسان اشیاء را فراهم میکند و رفتاری سازگار و قابل پیشبینی را در سلسله مراتب وراثت رعایت میکند.
مثال:
class Bird { void fly() { // Bird is flying } } class Duck extends Bird { @override void fly() { // Duck is flying; } } class Ostrich extends Bird { @override void fly() { throw UnsupportedError("Ostrich cannot fly"); } } void main() { List<Bird> birds = [Bird(), Duck(), Ostrich()]; for (var bird in birds) { bird.fly(); } }
در این مثال، ما یک کلاس Bird داریم که به عنوان یک کلاس پایه برای انواع مختلف پرندگان عمل میکند. کلاس Bird یک متد fly دارد که رفتار پرواز پرندگان را نشان می دهد.
کلاس Duck کلاس Bird را گسترش می دهد و متد fly را دارد تا پیاده سازی خاص خود را برای اردک ها ارائه دهد. وقتی متد fly را روی یک شی Duck فراخوانی می کنیم، "Duck is flying" را روی کنسول چاپ می کند.
کلاس Ostrich نیز کلاس Bird را گسترش می دهد، اما در این مورد، متد fly خطای UnsupportedErrorرا پرتاب می کند زیرا شترمرغ ها نمی توانند پرواز کنند. این موضوع نشان میدهد که کلاس شترمرغ متفاوت از سایر پرندگان رفتار می کند و نمی تواند رفتار پرواز را انجام دهد.
در تابع mian، لیستی از اشیاء Bird ایجاد میکنیم که شامل نمونه هایی از Bird، Duck و Ostrich می باشد. روی لیست پرندگان یک حلقه تکرار می نویسیم و متد fly را روی هر پرنده فراخوانی میکنیم. با وجود پیاده سازیهای مختلف fly در کلاس های Duck و Ostrich، ما میتوانیم با همه پرندگان به صورت یکنواخت رفتار کنیم و متد fly را بدون ایجاد مشکلی فراخوانی کنیم اما این امر LSP را نقض میکند زیرا اشیاء کلاس مشتق شده (Ostrich) را نمیتوان بدون ایجاد خطاهای غیرمنتظره جایگزین اشیاء کلاس پایه (Bird) کرد.
برای پایبندی به LSP، کلاس شترمرغ نباید رفتاری را معرفی کند که با رفتار مورد انتظار تعریف شده توسط کلاس پایه آن در تضاد باشد.
اصل جداسازی رابطها (Interface Segregation Principle - ISP)
چهارمین اصل از اصول SOLID، اصل جداسازی رابط (ISP) است که بیان میکند که کلاینت ها نباید مجبور شوند به واسط هایی که استفاده نمی کنند وابسته شوند. به عبارت دیگر، ایده ایجاد رابطهای خاص و ریزدانه متناسب با نیازهای مشتریان را به جای داشتن رابطهای بزرگ و همه منظوره ترویج میکند. با انجام این کار، از اتصال کلاینتها به وابستگیهای غیر ضروری جلوگیری میکند و به ماژولار بودن و انعطاف پذیری بهتر در سیستم اجازه میدهد. ISPجداسازی واسطها را به واحدهای کوچکتر و منسجم ترغیب میکند و انسجام بالا و جفت کم بین اجزا را ارتقا میدهد.
مثال:
interface IClick{ void (Object obj); void onLongClick(Object obj); void onTouch(Object obj); }
در مثال یک رابط IClick وجود دارد که سه متد را اعلان می کند: ، onLongClick و onTouch. این متدها انواع مختلفی از تعاملات کاربر را نشان میدهند. این مثال اصل چهارم را نقض میکند چرا که کلاسی که فقط نیاز به پیاده سازی عمل کلیک کردن دارد حتما باید ویژگی لمس کردن را هم پیاده سازی کند در صورتی که اصلا نیاز به این ویژگی ندارد ولی مجبور است آن را پیاده سازی کند. کد بالا را به شکل زیر اصلاح میکنیم
interface IClick{ void (Object obj); void onLongClick(Object obj); } interface ITouch{ void (Object obj); void onTouch(Object obj); }
در کد اصلاح شده، رابط IClick به دو رابط جداگانه تقسیم میشود: IClick و ITouch. رابط IClick اکنون فقط شامل متدهای و onLongClick است که مستقیماً با رویدادهای کلیک مرتبط هستند. از سوی دیگر، رابط ITouch شامل متدهای و onTouch است که به طور خاص به رویدادهای لمسی مربوط میشوند.
با تقسیم رابط اصلی، کد به اصل جداسازی رابط (ISP) پایبند است. این به کلاینتها اجازه میدهد فقط به رابطهایی که نیاز دارند وابسته باشند و از مجبور شدن آنها به پیادهسازی روشهایی که با عملکردشان مرتبط نیستند، جلوگیری میکند. این امر سازماندهی بهتر کد را ارتقا میدهد، اتصال را کاهش میدهد، و انعطافپذیری و قابلیت نگهداری پایگاه کد را افزایش میدهد.
اصل وارونگی وابستگی (Dependency Inversion Principle - DIP)
پنجمین اصل از اصول SOLID، اصل وارونگی وابستگی (DIP) است و بیان میکند که ماژولها یا کلاسهای سطح بالا نباید مستقیماً به ماژولها/کلاسهای سطح پایین وابسته باشند، اما هر دو باید به انتزاعها وابسته باشند. به عبارت دیگر، هیچ شیئی نباید در داخل یک کلاس ایجاد شود. آنها باید از خارج منتقل یا تزریق شوند. شیءای دریافت میشود، به جای یک کلاس، یک رابط خواهد بود. این اصل بر جداسازی مؤلفهها تأکید میکند و استفاده از رابطها یا کلاسهای انتزاعی را بهعنوان انتزاع برای تسهیل اتصال آزاد و انعطافپذیری ترویج میکند. DIPبا وابستگی به انتزاعات به جای اجرای واقعی، اصلاح، گسترش و آزمایش سیستم های نرم افزاری را آسان تر می کند. همچنین امکان تعویض پیادهسازیهای مختلف را بدون تأثیرگذاری بر ماژولهای سطح بالاتر که به آنها وابسته هستند، میدهد.
مثال:
class PaypalPaymentService { void processPayment(double amount) { // Logic to process payment via Paypal } } class StripePaymentService { void processPayment(double amount) { // Logic to process payment via Stripe } } class PaymentService { PaypalPaymentService _paypalPaymentService; StripePaymentService _stripePaymentService; PaymentService() { _paypalPaymentService = PaypalPaymentService(); _stripePaymentService = StripePaymentService(); } void processPayment(double amount, String provider) { if (provider == 'paypal') { _paypalPaymentService.processPayment(amount); } else if (provider == 'stripe') { _stripePaymentService.processPayment(amount); } } } void main() { PaymentService paymentService = PaymentService(); paymentService.processPayment(100.0, 'paypal'); paymentService.processPayment(50.0, 'stripe'); }
در این مثال، دو کلاس خدمات پرداخت مشخص داریم، PaypalPaymentService و StripePaymentService، که به طور مستقیم پردازش پرداخت را برای ارائه دهندگان پرداخت مربوطه انجام میدهند.
کلاس PaymentService به جای وابستگی به انتزاعات، مستقیماً نمونههایی از PaypalPaymentService و StripePaymentService را در سازنده خود ایجاد میکند. همچنین حاوی یک عبارت شرطی (if-else) برای تعیین اینکه از کدام سرویس پرداخت بر اساس پارامتر ارائه دهنده استفاده شود، میباشد.
این مثال با وابستگی مستقیم به پیاده سازیهای مشخص به جای انتزاعها، اصل وارونگی وابستگی (DIP) را نقض میکند. کلاس PaymentServiceبه شدت خود را با کلاسهای خدمات پرداخت خاص مرتبط میکند و تغییر یا گسترش ارائهدهنده پرداخت بدون تغییر کلاس PaymentService را دشوار میکند.
در یک طراحی سازگار، کلاس PaymentServiceباید به یک رابط انتزاعی یا کلاس پایه که یک ارائه دهنده پرداخت عمومی را نشان می دهد، بستگی داشته باشد. این امر به انعطاف پذیری، توسعه پذیری و قابلیت تعویض بهتر ارائه دهندگان پرداخت بدون تغییر کلاس PaymentService سطح بالاتر اجازه میدهد.
کد مثال بالا را به شکل زیر اصلاح می کنیم:
abstract class PaymentProvider { void processPayment(double amount); } class PaypalPaymentProvider implements PaymentProvider { @override void processPayment(double amount) { // Logic to process payment via Paypal } } class StripePaymentProvider implements PaymentProvider { @override void processPayment(double amount) { // Logic to process payment via Stripe } } class PaymentService { PaymentProvider _paymentProvider; PaymentService(this._paymentProvider); void processPayment(double amount) { _paymentProvider.processPayment(amount); } } void main() { PaymentProvider paypalProvider = PaypalPaymentProvider(); PaymentProvider stripeProvider = StripePaymentProvider(); PaymentService paymentService = PaymentService(paypalProvider); paymentService.processPayment(100.0); paymentService = PaymentService(stripeProvider); paymentService.processPayment(50.0); }
کلاس انتزاعی PaymentProvider با متد processPayment را به کد اضافه می کنیم که کلاسهای PaypalPaymentProviderو StripePaymentProviderآن را پیاده سازی (implements) میکنند. کلاس PaymentService بجای اینکه مستقیماً نمونههایی از PaypalPaymentServiceو StripePaymentServiceرا در سازنده خود ایجاد کند یک نمونه از PaymentProvider تعریف میکند و متد processPayment را صدا میزند. مثال بالا اصل پنجم را رعایت میکند.
خلاصه
اصول SOLID، مجموعهای از اصول طراحی نرم افزار است که شامل اصول مسئولیت واحد، باز/بسته، جایگزینی لیسکوف، جداسازی رابط و وارونگی وابستگی است. استفاده از این اصول در طراحی نرم افزار مزایایی مانند قابلیت نگهداری، استفاده مجدد، اصلاح آسان و تست واحد را به همراه دارد. اصل مسئولیت واحد بیان میکند که هر کلاس باید تنها یک دلیل برای تغییر داشته باشد. اصل باز/بسته از کامپوننتهای نرم افزاری میخواهد برای توسعه باز باشند اما برای اصلاح بسته. اصل جایگزینی لیسکوف میگوید که اشیاء یک سوپرکلاس باید قابل جایگزینی با اشیاء زیر کلاسهای آن باشند. اصل جداسازی رابط توصیه میکند که کلاینتها به رابطهایی که استفاده نمیکنند وابسته نشوند. و اصل وارونگی وابستگی بیان میکند که ماژولها یا کلاسهای سطح بالا نباید مستقیماً به ماژولها/کلاسهای سطح پایین وابسته باشند. استفاده از این اصول باعث میشود نرم افزار قابل نگهداری، توسعهپذیر و قابل استفاده مجدد باشد.