اولین تمرین درس طراحی شیگرای سیستمها
هدف این پروژه، آشنایی عمیقتر با اصول شیگرایی (OOP) و تمرین عملی اصول SOLID از طریق تحلیل و اصلاح یک سیستم ساده رزرو هتل است. این پروژه یک پروژه تمرینی است و ساختار آن عمداً ساده طراحی شده است.
در این تمرین، یک سیستم ساده رزرو اتاق هتل قرار دارد. این سیستم شامل کلاسهای زیر است:
-
Customer→ نمایش اطلاعات مشتری -
Room→ اطلاعات اتاق -
Reservation→ ذخیره اطلاعات رزرو -
ReservationService→ مدیریت فرآیند رزرو (متدmakeReservation) -
EmailSender→ ارسال تأییدیه رزرو -
PaymentProcessor→ انتخاب و اجرای روش پرداخت
در نسخه اولیهی پروژه، متد رزرو یک مشتری را ایجاد میکند، یک اتاق تخصیص میدهد و با توجه به روش پرداخت انتخابی، عملیات رزرو را انجام داده و در پایان، تاییدیه را از طریق ایمیل ارسال میکند.
کد اولیه پروژه از لینک زیر قابل دریافت است:
https://github.com/yahyaPoursoltani/base-project-for-solid/tree/master
در این مرحله لازم است یک نسخه از پروژه تهیه کرده و در پوشهی
Step-01-without-OOD/
ذخیره کنید. سپس دو قابلیت جدید زیر را به سیستم اضافه نمایید:
افزودن ارسال پیامک (SMS) با ایجاد کلاسی مشابه EmailSender با نام:
SmsSender
افزودن روش جدید پرداخت حضوری توسط اصلاح کلاس:
PaymentProcessor
و افزودن متدی مانند:
onSitePayment()
| ردیف | تغییرات مرتبط با ارسال پیام جدید | کلاس تغییر یافته | توضیح تغییر | تغییرات مرتبط با پرداخت جدید | کلاس تغییر یافته | توضیح تغییر |
|---|---|---|---|---|---|---|
| 1 | تغییر نام تابع sendEmail به send |
MessageSender |
قابل استفاده بودن تابع برای هم ارسال پیامک و هم ایمیل | افزودن INPERSON به enum |
PaymentMethods |
اضافهکردن گزینه پرداخت حضوری |
| 2 | افزودن SMS به switch case |
ReservationService |
چاپ پیام تایید رزرو با پیامک | افزودن متود payInPerson |
PaymentProcessor |
متود جدید برای چاپ کردن روش پرداخت |
| 3 | افزودن this.notifier=notifier به makeReservation |
ReservationService |
جهت گرفتن notifier از ورودی و بررسی در switch case | اضافه کردن INPERSON به switch case |
ReservationService |
این قسمت payment processor را از قسمت قبلی صدا می زند |
همچنین کلاس
PoorRoomبه برنامه اضافه شد تا گزینه ای مناسب برای افرادی که تمکن مالی بالایی ندارند باشد.همچنین کلاس
SmsSenderبه برنامه اضافه شد تا یک کلاس برای ارسال پیامک داشته باشیم.
در ادامه این تغییرات را از نظر رعایت اصول شیگرایی تحلیل خواهیم کرد.
این گام مقدمهای است برای بررسی:
-
نقضهای احتمالی اصول SOLID
-
شناسایی وابستگیهای نامناسب
-
بهبود طراحی سیستم
| اصل | کلاس | علت برقراری / نقض |
|---|---|---|
| اصل SRP مورد برقراری | LuxuryRoom و PoorRoom |
می توان گفت که توابع AddFreeDinner و AddFreeRommate به عنوان رفتار طبیعی برای هر نوع اتاق تعریف شده اند و single responsibility در این کلاس ها برای صرفا نمونه سازی اشیا همچنان برقرار است. |
| اصل SRP مورد نقض | LuxuryRoom و PoorRoom |
میتوان گفت توابع AddFreeDinner و AddfreeRommate ارتباطی به مسئولیت این کلاس ها ندارند و می توانند در قالب یک کلاس جدا مثلا RoomBenefits تعریف بشوند. |
| اصل OCP مورد برقراری | MessageSender |
این کلاس با دارا بودن چارچوب اصلی یک ارسال کننده پیام برای اضافه کردن روش ها و ارث بری کلاس های جدید باز و دربرابر تغییرات آنها بسته است. |
| اصل OCP مورد نقض | PaymentProcessor و ReservationService |
PaymentProcessor تمامی متد های پرداخت را دربر گرفته است که باعث می شود قابلیت باز بودن در برابر تغییرات را از دست بدهد و در اضافه شدن هر متد نیازمند ویرایش باشد. همین وضعیت به شکل بدتری در ReservationService است و حالت بندی های مختلفی برای نوع پرداخت و روش اطلاع رسانی وجود دارد. |
| اصل LSP مورد برقراری | MessageSender و فرزندانش |
هر کدام از کلاس های SmsSenderوEmailSenderمی توانند بدون مشکل جای پدر بنشینند. |
| اصل LSP مورد نقض 1 | Room |
امکان تعیین type باعث می شود که در صورت استفاده کردن از کلاس فرزند بتوان نوع آن را تغییردهد در حالیکه شی استفاده شده از کلاس فرزند با آن یکی نباشد! پس نوع اتاق نباید یک رشته در کلاس پایه باشد. |
| اصل LSP مورد نقض 2 | PaymentProcessor |
رابط های (متود ها) بیخودی فراوان هر بار که بخواهیم پرداخت کنیم ناگزیر اضافه می شوند! |
| اصل ISP مورد برقراری | MessageSender |
از آنجا که هر کدام از روش های اطلاع رسانی توابع خاص خود را دارند پس ارث بری از MessageSender در EmailSender و SmsSender محل درست رعایت ISP است. |
| اصل ISP مورد نقض | PaymentProcessor |
در این کلاس چهار متد پرداخت متفاوت گذاشته شده است و اگر نیاز به فقط یکی از آن ها داشته باشیم مجبوریم به سه متد دیگر هم وابسته باشیم. |
| اصل DIP مورد برقراری | SmsSender |
این کلاس فقط به اینترفیس MessageSender وابستگی دارد نه کلاس concrete دیگری. |
| اصل DIP مورد نقض | ReservationService |
کلاس در متد makeReservation هم paymentType و هم notifier را از ورودی می گیرد که این باعث می شود وابستگی آن به سمت کلاسهای جزئی که تغییر میکنند برود. (باید صرفا متد را از شی مربوطه فراخوانی کند) کلاس PaymentProcessor خودش زمینه این مشکل را فراهم کرده است.(مشکلات امنیتی که بماند.) |
| اصل PLK مورد برقراری | Reservation |
دسترسی به فیلد ها به صورت ساده و کاملا درست داده شده است. |
| صل PLK مورد نقض | ReservationService |
زنجیره طولانی و بی فایده res.customer.city و res.room.price وابستگی(coupling) کد را بالا برده. |
| اصل CRP مورد برقراری | Reservation |
اینکه یک شی از کلاس های Room و Customer را در خودش دارد یعنی فقط از آن ها استفاده می کند و ارث بری بیخودی برای استفاده مجدد ندارد. |
| اصل CRP مورد نقض | ReservationService |
همه چیز مانند پرداخت ، رزرو و ارسال پیام در پکیج services گذاشته شده است و اگر کلاسی مانند ReservationService به یکی از آن ها فقط نیاز داشته باشد باید بقیه را هم ارث ببرد. |
نقض اصل SRP با تقسیم کلاس ReservationService به چهار سرویس تکمسئولیتی شامل DiscountService، InvoicePrinter، NotificationService و ReservationService صرفا هماهنگکننده رفع شد. همچنین کلاس PaymentProcessor حذف و هر روش پرداخت به یک کلاس مستقل تبدیل شد.
نقض اصل OCP با پیادهسازی پترن Strategy همراه با یک متد factory برای پرداخت و نوتیفیکیشن برطرف شد. به این ترتیب افزودن روش پرداخت یا پیام جدید تنها با ایجاد یک کلاس جدید انجام میشود و هیچیک از کلاسهای موجود نیاز به تغییر ندارند.
نقض اصل DIP با تعریف اینترفیسهای جدا برای هر سرویس و انجام پیادهسازیهای مربوطه از طریق constructor کلاس برطرف شد. در نتیجه ماژولهای سطح بالا تنها به abstraction وابسته هستند.
نقض اصل PLK با کپسولهسازی تمامی دسترسیها به فیلدهای آبجکت های Customer و Room در داخل کلاس Reservation و ارائه متدهای کمکی مناسب رفع شد. اکنون هیچ کلاس دیگری به ساختار داخلی آبجکت های داخلی دسترسی مستقیم ندارد و تمام تعاملات تنها از طریق ابجکت Reservation انجام میشود.
نقض اصل LSP با تنظیم فیلد type به کمک private final و مقدار دهی فقط در constructor انجام شد. همچنین متود های get برای هر یک از فیلد ها تعریف شد و مشکل تداخل تغییر نوع غیر مجاز از این طریق برطرف شد.
نقض اصل ISP توابع از paymentProcessor خارج شدند و paymentstrategy به عنوان تنها interface برای پیاده سازی متود pay پیاده سازی شد. الان اضافه کردن هر تعداد روش
با کمترین تغییر امکان پذیر است.
نقض اصل CRP تعیین قیمت نهایی به اینترفیس جدای IDiscountService واگذاری شد و چاپ رسید رزرو و ارسال نوتیفیکیشن هم به IInvoicePrinter و INotificationService واگذاری شدند. همچنین پکیج های notification, payment و reservation از services مشتق شدند و import ها کاهش پیدا کردند.
اگر از همان ابتدا اصول شی گرایی بهدرستی در این پروژه رعایت شده بود، تقریبا تمام دردسرها و تغییراتی که در مرحله اول برای افزودن پیامک و یک روش پرداخت جدید متحمل شدیم، وجود نمیداشت. در حالت عادی مجبور شدیم چندین فایل را باز کنیم، switchها را دستکاری کنیم، نام متدها را تغییر دهیم، باگهای مربوط به استفاده نکردن از پارامترها را رفع کنیم. کاری که در مجموع ۷ یا ۸ تغییر بود.
اما اگر پروژه از روز اول بر پایه اصول درست طراحی شده بود، برای اضافه کردن هر دو قابلیت (پیامک و روش پرداخت جدید) فقط دو کار بسیار ساده کافی بود: ۱. یک کلاس جدید بسازیم (مثلاً SmsSender و InPersonPayment). ۲. در صورت نیاز، فقط یک خط به بخش factory اضافه کنیم یا حتی همان را هم لازم نباشد دست بزنیم. اینطوری نه سوئیچی تغییر میکرد، نه کلاس قدیمی بازنویسی میشد، نه ریسک خراب شدن بقیه قسمتها وجود داشت. به بیان دیگر، رعایت اصول شی گرایی از ابتدا باعث میشد بهجای چندین تغییر پراکنده و حساس، هر قابلیت جدید فقط با یک یا حداکثر دو تغییر کوچک و ایمن اضافه شود. این دقیقا همان چیزی است که در پروژههای واقعی، نگهداری و توسعه را آسان، سریع و کمهزینه میکند.
رعایت اصول شی گرایی در این پروژه باعث شد کد از یک ساختار درهم و ضعیف به کدی منظم، خوانا و انعطاف پذیر تبدیل شود. هر بخش اکنون تنها یک وظیفه مشخص دارد، افزودن قابلیت های جدید بدون تغییر کدهای قبلی ممکن شده و وابستگیها نیز به صورت کنترل شده و قابل جایگزینی مدیریت میشوند.
در نهایت، برنامه اکنون به مراتب آسان تر نگهداری، توسعه و رفع اشکال می شود، ریسک ایجاد خطا در تغییرات آینده به حداقل رسیده و هزینه و زمان لازم برای به روزرسانیهای بعدی کاهش یافته است و این همان هدفی است که در طول پروژه با رعایت اصول شی گرایی ، سعی در رسیدن به آنها داشتیم.
دل گرچه درین بادیه بسیار شتافت
یک موی ندانست و بسی موی شکافت
گرچه ز دلم هزار خورشید بتافت
آخر به کمال ذرهای راه نیافت