مقدمه
تصمیم دارم در مجموعهای از پستهای بهم پیوسته، مهمترین اصول برنامه نویسی را بررسی کرده و با آنها آشنا شویم. اصولی مانند SOLID، DesignPattern، تست نویسی و غیره. تمامی این اصول و مفاهیم تا حدود زیادی انتزاعی و گاهی حوصله سر بر هستند، تمام سعیام را کرده ام که مفاهیم تا جای ممکن قابل لمس و درک باشند.
بخش بزرگی از مطالبی که در این سلسه پستها خواهم نوشت، برگرفته از کتاب و ویدئوهای Bob C. Martin درباره Clean Code خواهد بود، باقی مطالب از تجربههای شخصی و مطالعات جسته گریختهای است که در طی سالیان گذشته داشتهام.
میخواهم در اولین گام، به یکی از مهمترین اصول برنامه نویسی یعنی SOLID بپردازیم.
برای دسترسی سریعتر به هریک از عناوین این پست، میتوانید از فهرست زیر استفاده کنید.
- SOLID چیست؟
- مدیریت وابستگی یا Dependency Management
- پیچیدگی های غیر ضروری
- مدیریت وابستگیها با SOLID
- جمع بندی
SOLID چیست؟
اصول ۵ گانه طراحی شی گرا برای اولین بار توسط Uncle Bob تعریف شد؛ سپس در سال ۲۰۰۰ آقای Michael Feathers با استفاده از اولین حروف هریک از نامهای این اصول ۵ گانه، نام SOLID را برای آنها انتخاب کرد.
وقتی این اصول با هم در طراحی و پیادهسازی یک برنامه اعمال میشوند، به احتمال زیاد آن سیستم قابلیت این را خواهد داشت که به آسانی قابل توسعه و نگهداری باشد.
در حقیقت اصول سالید، دستورالعملهایی هستند که میتوان هنگام کار بر روی یک نرمافزار، آنها را برای از بین بردن، عوامل نامطلوب در کد، اعمال کرد. اینکار از طریق فراهم آوردن چارچوبی انجام میگیرد که با استفاده از آن، برنامهنویس میتواند کد برنامه را اصلاح و بازسازی کند تا کدها توسعهپذیر و خواناتر شوند.
مدیریت وابستگی یا Dependency Management
قبل از اینکه شیرجهای عمیق در SOLID بزنیم، لازم است تا با مدیریت وابستگی و مفاهیم مرتبط با آن آشنا بشویم.
مدیریت وابستگی چیست؟
کمتر دیده میشود که پروژههای نرم افزاری را بصورت ایزوله توسعه داده باشند. معمولا، یک پروژه وابستگی زیادی به فانکشنهایی با قابلیت استفاده مجدد دارد که بصورت لایبرری یا کامپوننتهای جدا شده از سیستم مورد استفاده قرار میگیرند. مدیریت وابستگی، یک تکنیک برای شفاف سازی، حلِ پیچیدگی و استفاده از وابستگیهای مورد نیاز یک پروژه است.
در توسعه نرم افزار، بزرگترین نگرانی، پایداری و انسجام همیشگی سیستم است؛ به بیانی دیگر، با افزایش وابستگی یک سیستم ویژگیهایی مانند استفاده مجدد، انعطاف پذیری و قابلیت نگهداری آن کاهش مییابد. مدیریت وابستگی (یا DM) به ما کمک میکند تا این وابستگی ها را کنترل و مدیریت کنیم.
مفاهیم و اصولی مانند SOLID یا Object-Oriented از ابزارها و تکنیکهای پر کاربرد در مدیریت وابستگی اند.
فاجعه در مدیریت وابستگی!
بقول Uncle Bob، سیستمهایی که مدیریت وابستگی در آن رعایت نمیشود این ۴ بو (Smell) را همراه خود خواهند داشت:
- سختی یا Rigidity
- شکنندگی یا Fragility
- عدم تحرک یا Immobility
- چسبناکی یا Viscosity
اما این ۴ بو چه هستند و چه تاثیری در سیستم ما دارند. در ادامه هرکدام از این ۴ مورد را به تفضیل بررسی خواهیم کرد.
سختی یا Rigidity
به سادهترین زبان ممکن، سختی یا Rigidity یعنی ناتوانی در تغییر.
اما چه میشود که تغییر دادن یک سیستم سخت میشود؟ تغییر دادن یک سیستم زمانی سخت میشود که هزینه تغییر بالا رود! فرض کنید برای ساختن و تست گرفتن از یک سیستم باید ۲ ساعت وقت صرف کنید. اگر بعد از یک تغییر کوچک، مجبور شوید دوباره ۲ ساعت دیگر برای ساختن و تست گرفتن از آن سیستم وقت بگذارید، ریجیدیتی رخ داده است.
در نتیجه اگر برای تغییر دادن بخش کوچکی از کد مجبور شویم کل سیستم را مجددا rebuild کنیم، آنوقت آن سیستم سخت شده یا دچار Rigidty شده است.
چطور بفهمیم دچار Rigidty شده ایم؟
- وقتی به ازای هر تغییر کوچک مجبور شویم کل سیستم را rebuild کنیم.
- وقتی به ازای هر تغییر، زمان زیادی برای تست و ساخت یک سیستم صرف شود.
درصورتی که بتوانیم راهی پیدا کنیم تا به ازای هر تغییر مجبور نشویم کل سیستم را دوباره بازسازی کنیم یا دستکم بتوانیم زمان تست و ساخت سیستم را به حداقل ممکن برسانیم، آنوقت از Rigidty جلوگیری کردهایم.
اگر سیستم شما به ازای هر تغییر، وقت زیادی را جهت test و rebuild صرف میکند، نشانه این است که توسعه دهندگان کم حوصله و بی دقتی را در تیم خود دارید!
دلایل ابتلا به Rigidity
امیدوارم هیچگاه به Rigidity مبتلا نشوید! برای اینکه بتوانید از آن پیشگیری کنید، بهتر است دلایلی که سیستمِ شما را مبتلا به Rigidity میکند خوب بشناسید:
- کدها بصورت رویهای یا Procedural نوشته شده اند: برای مثال تمام کد در یک فایل و بصورت تو در تو با if-else های مختلف و زیاد و بصورت به هم پیوسته و وابسته نوشته شده است.
- هیچ مفهوم انتزاعی یا Abstraction در کد دیده نمیشود: این مورد وقتی اتفاق میافتد کدها در پایینترین سطح ممکن نوشته شده باشد. در واقع بیش از حد به جزئیات توجه شده تا اینکه روی مفاهیم و خصوصیات شئ تمرکز شود. برای مثال بهجای اینکه به خصوصیات یک شئ از طریق یک method یا property دسترسی داشته باشیم، با استفاده از یک پرچم ۱ بیتی در خانه nام حافظه، خصوصیت آن شئ را دریافت میکنیم.
- کدها بصورت یک مفهوم کلی در سیستم تعبیه شدهاند در حالی که با خصوصیاتی بسیار جزئی جهت استفاده در یک مورد خاص تعریف شده باشند: برای مثال یک کد HTML را در نظر بگیرید که قرار است یک table را با استفاده از دادههای یک ماتریس چاپ کند درحالی که در کد تعریف کرده ایم تا Headerها پیش زمینهای مشکی با فونت Bold و رنگ سفید داشته باشند.
- کامپوننتها اطلاعات زیادی درباره جزئیات یکدیگر دارند، وقتی که قرار است باهم ارتباط داشته باشند: برای مثال کلاسی داریم که یک Shape از مربع برای ما میسازد؛ از آن میخواهیم که مساحت مربع را بطور هاشور زده نمایش دهد؛ پس در آن فانکشنی مینویسیم که ضلع مربع را گرفته، آن را ضربدر خودش کند تا مساحت مربع بدست آمده و حدود origin های شکل چند ضعلی روی اسکرین بدست آید. سپس توسط فانکشن دیگری آن را بصورت هاشور زده نمایش میدهیم. مشکل جایی رخ میدهد که از همان کامپوننت - که مساحت اشکال را هاشور زده نمایش میدهد - بخواهیم یک Shape دیگر از دایره ساخته و مساحت آن را هاشور زده نمایش دهد!
شکنندگی یا Fragility
مفهوم Fragility و Rigidity بسیار بهم نزدیک اند. درواقع شکنندگی و سختی، علت و معلول یکدیگر هستند. شکنندگی یا Fragility اشاره دارد به اینکه هر موقع تغییری در سیستم ایجاد میکنید در بخش (یا بخشهای) دیگری از سیستم - که حتی هیچ ربطی با آن قسمت ندارد - با خطا و مشکل مواجه میشوید.
فرض کنید یک ماشین داریم که رادیوی آن مشکل دارد، رادیو را درست میکنیم و پس از روشن کردن ماشین متوجه میشویم که شیشه برقی کار نمیکند!
همانطور که تغییرات افزایش پیدا میکنند، نگهداری سیستم نیز سخت تر میشود؛ زیرا هربار که تغییری در سیستم ایجاد میکنیم، در چندین نقطه دیگر خطاهایی پیدا میشود، این مورد تا آنجا ادامه پیدا میکند که هر تغییری در سیستم باعث سکتههای ناقص نرم افزار میگردد!
این مشکل باعث میشود که تیم اعتبار خود را در نزد مدیران و مشتریان از دست دهد! چون به ازای هر تغییری یا فیچر جدیدی که برای تیم ارسال میشود نیاز به صرف زمان و هزینه زیادی جهت ایجاد تغییر، تست و ساختن محصول میشود و حتی باعث میگردد که پروژه ها دیرتر از موعو به مرحله تحویل برسند. در نهایت این امر موجب کاهش کیفیت در محصول میشود.
چطور بفهمیم دچار Fragility شده ایم؟
- مجبور شوید به ازای هر تغییر، تغییرات متوالی و زیادی در دیگر بخشها ایجاد کنید.
- خطاهای جدیدی در بخشهایی رخ دهد که هیچ ربطی با تغییرات ندارند.
- جدا کردن بخشی از سیستم، مارا مجبور کند تا بعضی بخشها را دوباره از نو باز نویسی کنیم.
دلایل ابتلا به Fragility
ابتلا به Fragility ممکن است دلایل مختلفی داشته باشد، اما مهمترین و اصلیترین دلیل ابتلا به Fragility وابستگی و ایزوله نبودن کامپوننتها و بخشهای مختلف سیستم است، که با تغییر یک بخش، نیاز به تغییر بخشهای دیگر داریم.
عدم تحرک یا Immobility
خاصیت عدم تحرک یا Immobility اشاره به این موضوع دارد که نتوانیم آن قسمت از کد یا کامپوننت را در دیگر بخشهای سیستم استفاده کنیم. اگر در بخشی از سیستم احتیاج به ماژولی داشتیم که مشابه آن در قسمت دیگری از سیستم وجود دارد اما امکان استفاده مجدد در بخش دیگر به دلایل مختلف (مثل وجود برخی خصوصیات جزئی مثلا همان رنگ بک گراند با فونت فلان) وجود نداشت آنگاه دچار immobility شدهایم.
فرض کنید در بخشی از نرم افزار یک ماژول login با استفاده از username و passowrd ساخته ایم. حال تصمیم داریم آن ماژولِ لاگین را از سیستم جدا کرده و در جایی دیگر استفاده کنیم. اگر نتوانید به سرعت و آسانی آن تکه کد را جدا کرده و در سیستم یا بخش دیگر (بدون تغییر) مجددا استفاده کنید، آنگاه ماژول immobile است. درواقع نمیتواند حرکت کند!
وقتی سیستم immobile باشد، روزی که بخواهیم بخشی از سیستم را که به آن احتیاج داریم جدا کرده و در جای دیگری به کار بگیریم، این قضیه آنقدر سخت و ریسکی میشود که نهایتا ترجیح میدهیم بجای اینکه کد را مجددا استفاده کنیم، بشینیم و از ابتدا کدها را برای بخش جدید بازنویسی کنیم.
هنگامی که قصد آن را داشته باشیم تا بخشی از سیستم را جدا کرده و در بخش دیگری استفاده کنیم، در حالتی که کدها immobile باشد، دچار مشکل خواهیم شد؛ از آنجا که این عمل (بدلیل وجود جزئیات و وابستگیهای بیش از حد) سخت و ریسکی است، توسعه دهنده ترجیح میدهد بجای استفاده مجدد کدها در جای مورد نظر، وقت زیادی گذاشته و از پایه دوباره کدها را بازنویسی کند. این عمل سبب دوباره کاری و داپلیکیت شدن کدها میشود. امیدوارم متوجه باشید که داپلیکیت شدن کدها چه عواقبی ممکن است داشته باشد.
چطور بفهمیم دچار Immobility شده ایم؟
-
ماژول آنقدر پیچیده و وابسته به دیگر بخشها شده که قابل جدا شدن نیست.
-
ماژول قابل جدا شدن باشد، اما جدا کردن و استفاده کردن از آن در محیطی غیر از محیط اصلیِ آن، بسیار سخت و ریسکی باشد.
-
نتوانید ماژول یا بخشی از کد را از سیستم جدا کرده و فورا بدون تغییر در آن، در جای دیگری استفاده کنید.
دلایل ابتلا به Immobility
- اصلی ترین دلیل ابتلا به immobility یا عدم تحرک، پرداختن به جزئیات بیش از حد و غیر ضروری در آن بخش از کد است.
- ماژول بیش از حد به محیط و envirement خودش وابسته باشد.
- ماژول بیش از یک وظیفه را بر دوش داشته باشد.
برای مثال، همان ماژول لاگین را در نظر داشته باشید. این ماژول برای احراز هویت کاربر از یک database schema خاص در دیتابیس Redis استفاده میکند. این ماژول ابتدا به دیتابیس Redis وصل شده و سپس با توجه به آن schema خاص، کاربر را احراز هویت میکند. حال قصد داریم آن را در سرویسی دیگری استفاده کنیم که از MySQL استفاده میکند و از قضا schema آن سیستم نیز با schema سیستم قبلی متفاوت است. حال، تو خود حدیث مفصل بخوان ازین مجمل!
چسبناکی یا Viscosity
چسبناکی، ویسکوزیته یا Viscosity مقاومت در مقابل تغییر است. وقتی که ساخت مجدد و تست سیستم برای ما سخت میشود و ترجیح بدهیم از خیر تغییرات آن قسمت بگذریم، آنگاه کد ما viscous یا چسبناک است.
در هنگام طراحی هرموقع که نیاز به یک تغییر بود، یا آن را در نظر میگیریم و با پیدا کردن یک راه درست تغییر را ایجاد میکنیم، یا اینکه میتوانیم یک راه سریع، کوتاه و البته کثیف برای آن پیدا کنیم. شرایط و محیطهایی که اغلب در آن کار میکنیم، مارا به سمت آن رویکرد سریع و کثیف هدایت میکنند.
وقتی زمانِ کامپایل شدن بیش از حد طول میکشد، بجای آنکه آن قسمت از کد که موجب این مشکل شده است را تصحیح کنیم، تصمیم میگریم جایی را تغییر دهیم که نیازی ندارد مجددا کامل کامپایل شود.
وقتی که check-in یا check-outهای دیتا یا فایلها زمان زیادی از سیستم بگیرد، تصمیم به تغییر جایی میگیریم که احتیاج به دسترسی به فایلهای کمتری را دارد.
اگر مستقر کردن محیط آنقدرها هم برای ما سخت نباشد، تصمیم میگیریم بجای تغییر در کد، دیتابیس را تغییر دهیم. زیرا اجرا کردن یک اسکریپت دیتابیس برای تغییرات، آسانتر و امنتر از دوباره deploy کردن کل اپلیکیشن است.
بنابراین کدی که خاصیت Viscosity آن بالا باشد، مارا مجبور میکند تا بجای ماژول یا آن بخش از سیستم، تغییرات را روی بخشهایی اجرا کنیم که کمترین مقاومت ممکن را دارند.
چطور بفهمیم دچار Viscosity شده ایم؟
- تغییر دادن آن بخش از سیستم آنقدر سخت و هزینهبر باشد که ترجیح دهیم جهت کاهش بار سیستم، بخش دیگری که کمتر مارا درگیر میکند را تغییر دهیم.
دلایل ابتلا به Viscosity
- عدم توجه توسعه دهندگان به کد یا حواله کردن بهبود و refactoring کد به زمان دیگر
- وابستگی بیش از حد ماژولها یا بخشهای سیستم به یکدیگر
پیچیدگی های غیر ضروری
یکی از مباحثی که معمولا در هنگام توسعه نرم افزار مطرح میشود این است که درباره آینده نرم افزار چه تصمیمی باید بگیرید؟ آیا فقط نیازهای کنونی خود را باید در طراحی لحاظ کنیم؟ یا اینکه باید یک دید باند مدت داشته و تمامی نیازهای آینده نرم افزار را در نظر داشته و در طراحی اعمال کنیم؟
به بیانی دیگر، آیا لازم است دستگیرههایی در سیستم جهت استفاده در آینده (دور یا نزدیک) قرار دهیم؟
به ازای هر دستگیرهای که در طراحی لحاظ میکنید که (شاید) در آینده مورد استفاده قرار گیرد، آن را پیچیدهتر کرده و بار اضافی جهت توسعه و نگهداری به سیستم و تیم توسعه خود افزوده اید. اگر تنها نیازهای کنونی خود را در نظر داشته و سیستم را با استفاده از تکنیکهایی مدیریت وابستگی - که در آینده به آنها خواهم پرداخت - ایزوله و ماژولار تهیه کنید، آنگاه هم میتوانید از پیچیدگی های غیر ضروری جلوگیری کرده و هم هیچکدام از آن چهار خصیصه نامطبوع را در سیستم خود تجربه نخواهید کرد.
هیچگاه هیچ سیستمی از همان ابتدا بد نیست. بدیها یا آن خواص نامطبوع در طول زمان پدیدار میشوند. تصمیمهای بدی که در مقاطع گوناگون میگیریم که حاصل بیتوجهی و تجربههای غلط ما هستند، دلایل اصلی بوجود آمدن این خصایص نامطبوع و پیچیدگیهای غیر ضروری اند.
گسترده شدن پیچیدگیها، پیشرفت را سخت تر میکند و ما را بیشتر وسوسه میکند تا راههای میانبری برای کاهش این سختی پیدا کنیم که همانها نیز به افزایش وابستگی و پیچیدهتر شدن سیستم در آینده میانجامد. “َUncle Bob”
مدیریت وابستگی ها با SOLID
مدیریت وابستگی موضوعی است که بیشترِ ما با آن سروکار داریم. هرموقع صفحهی اسکرین را که پوشیده از کدهای پیچیده و بهم ریخته تماشا میکنیم، در حال تجربهی ناخوشایندِ نتایجِ حاصل از مدیریت ضعیف در وابستگی هستیم. مدیریت ضعیف وابستگی منجر به کدهایی میشود که تغییر دادن آن سخت است، شکننده است و قابل استفاده مجدد نیست. ما داریم راجع به آن ۴ بوی نامطبوع حرف میزنیم. از سویی دیگر، زمانی که وابستگیها به خوبی مدیریت شوند کد انعطاف پذیر، قویتر و قابل استفاده مجدد میشوند.
اصول ۵ گانه SOLID یکی از راههاییست که به ما در مدیریت وابستگیها کمک میکند. بهتر است نگاهی اجمالی به این اصول بیاندازیم:
حرف اول | مخفف | مفهوم |
---|---|---|
S | Single responsibility principle | اصل تکوظیفهای یک کلاس باید تنها یک وظیفه داشته باشد (یک کلاس باید تنها یک دلیل برای تغییر داشته باشد و نه بیشتر) |
O | Open–closed principle | اصل باز ـ بسته اجزای نرمافزار باید نسبت به توسعه باز (یعنی پذیرای توسعه باشد) و نسبت به اصلاح بسته باشند (یعنی پذیرای اصلاح نباشد). (یعنی مثلاً برای افزودن یک ویژگی جدید به نرمافزار نیاز نباشد که بعضی از قسمتهای کد را بازنویسی کرد، بلکه بتوان آن ویژگی را مانند پلاگین به راحتی به نرمافزار افزود) |
L | Liskov substitution principle | اصل جانشینی لیسکو اشیاءِ یک برنامه که از یک کلاس والد هستند، باید به راحتی و بدون نیاز به تغییر در برنامه، قابل جایگزینی با کلاس والد باشند. |
I | Interface segregation principle | اصل تجزیهٔ (تفکیک) رابط استفاده از چند رابط که هر کدام، فقط یک وظیفه را بر عهده دارد بهتر از استفاده از یک رابط چند منظوره است. |
D | Dependency inversion principle | اصل وارونگی وابستگی بهتر است که برنامه به تجرید(abstraction) وابسته باشد نه پیادهسازی |
جمع بندی
در این پست با اصول ۵گانه سالید آشنایی ابتدایی پیدا کردیم، سپس به مفهوم مدیریت وابستگیها یا همان DM پرداختیم. با ۴ بوی نامطبوع کدهایی که مدیریت وابستگی در آن رعایت نشده آشنا شدیم، که عبارت بودند از:
- سختی یا Rigidity
- شکنندگی یا Fragility
- عدم تحرک یا Immobility
- چسبناکی یا Viscosity
در ادامه هرکدام را مفصلا بررسی کردیم. در نهایت به این نتیجه رسیدیم، برای اینکه کدهای ما بوی هیچکدام از این ۴تا را نگیرد، بهتر است از اصول و قوائد SOLID و دیگر ابزارهای DM استفاده کنیم. کلی حرف زدیم. اجازه بدید در پستهای بعدی درباره هر یک از ۵ اصل SOLID صحبت کنیم.
اگه سوال یا موردی بود، مثل همیشه در توییتر یا در تلگرام میتونیم باهم در ارتباط باشیم. 😉🍷