سارا رضائی
سارا رضائی
خواندن ۶ دقیقه·۳ سال پیش

انتخاب سختِ درست

فرض کنید در حال توسعه ی یک ماژول جدید هستید و به یک پیچیدگی غیر قابل اجتناب بر می خورید.

کدام را انتخاب می کنید؟

1. مدیریت این پیچیدگی را بر عهده ی استفاده کنندگان از ماژول می گذارید.

2. این پیچیدگی را درون ماژول، حل می کنید.

اگر این پیچیدگی، مربوط به business داخلی ماژول است، بهتر است راه حل دوم را انتخاب کنید. به عنوان یک برنامه نویس، شما باید به این فکر کنید، که برای استفاده کنندگان از ماژولی که در حال نوشتن آن هستید، چگونه می توانید زندگی را ساده تر کنید، حتی اگر این تصمیم باعث شود که کار خودتان بیشتر گردد. جمله ی اخیر را به یک روش دیگر هم می توان بیان کرد:

برای یک ماژول، داشتن یک واسط ساده، مهم تر از یک پیاده سازی ساده است.

یک برنامه نویس، همیشه وسوسه دارد که کار ساده تر را انجام دهد و کار سخت تر را به عهده ی دیگران بگذارد. مثلا اگر در کد شرایطی پیش آید که نداند با آن چه کار کند، ساده ترین راه، تولید یک exception است، که مدیریت آن بر عهده ی استفاده کننده از آن ماژول است. مثال دیگر زمانی است که در مورد یک موضوع، مثلا بخشی از یک فرمول، نمی توانیم تصمیم بگیریم، ساده ترین راه، قرار دادن آن بخش از فرمول در تنظیمات (configuration)، و سپردن بقیه ی کار (یعنی فراهم آوردن مقدار مناسب برای آن configuration) به مدیر سیستم است.

رویکردهای این چنینی، در کوتاه مدت، کمک می کنند که آسان تر زندگی کنیم، ولی در نهایت باعث افزایش پیچیدگی می شوند، چرا که با این روش، انسان های بیشتری باید با یک مشکل دست و پنجه نرم کنند، نه فقط یک نفر. مثلا اگر یک متد، یک exception بدهد، هر صدا زننده ی آن، باید بداند با آن چه کار کند. یا در مورد مثالِ configuration، به ازای هر استقرار جدید نرم افزار، مدیر سیستم باید بداند چه مقادیری برای چه تنظیماتی فراهم کند. این ها همه بر پیچیدگی سیستم می افزایند.

بیایید دو مثال را با هم بررسی کنیم:

مثال اول:

فرض کنید یک کلاس برای مدیریت متن داریم. این کلاس قرار است متدهایی برای خواندن یک فایل متنی، ویرایش آن و ذخیره ی آن روی دیسک داشته باشد. دو رویکرد می توان برای نوشتن این کلاس در نظر گرفت:

  1. رویکرد اول این است که متدهای کلاس، بر اساس خط (line) کار کنند و استفاده کنندگان از این متدها، وظیفه ی مدیریت خطوط و دانشِ چگونگی استفاده از آن ها را داشته باشند. این رویکرد، باعث می شود پیاده سازی این ماژول ساده تر شود، ولی برای لایه های دیگر، پیچیدگی ایجاد می کند. برای مثال اگر یک لایه ی دیگر که از این ماژول استفاده می کند، بخواهد بخشی از متن را حذف کند، باید بداند آن بخش از متن، در کدام خطوط قرار گرفته، محاسباتی انجام دهد و سپس دستور حذف آن خطوط را اجرا کند.
  2. رویکرد دوم کاراکتر محور است، یعنی لایه های دیگر هیچ نیازی به دانستن اطلاعات در مورد خطوط متن ندارند، آن ها فقط می دانند که کدام کاراکترها (بر اساس position) باید حذف شوند. در این صورت، پیاده سازی کلاس Text سخت تر است، چون خودش باید خطوط را مدیریت کند، در عوض واسط ساده تر است و پیچیدگی به بیرون از ماژول منتقل نشده است.

مثال دوم:

یک متد را در نظر بگیرید که در بخشی از جریان کاری خودش، نیاز دارد عملیاتی را retry کند. این retry کردن، وابسته به یک تعداد است، مثلا 4 بار retry و سپس در صورت انجام نشدن کار، تولید خطا. دو رویکرد می توان برای پیاده سازی این متد در نظر گرفت:

  1. رویکرد اول این است که، این متد، تعداد retry را از configuration بخواند و یا در پارامتر های ورودی خودش دریافت کند. در هر دو حالت، تعداد retry، در واسط متد قرار گرفته، و تصمیم گیری درباره ی مقدار آن، بر عهده ی استفاده کنندگان است. در شرایطی، مثلا زمانی که در domain های زیادی، قرار است از این متد استفاده شود، ممکن است این رویکرد مناسب باشد، چرا که برنامه نویسی که در آن domain بخصوص کد می نویسد، احتمالا بهتر از هر شخص دیگری می تواند در مورد نیازهای آن domain فکر کند و تصمیم درستی در مورد تعداد retry این متد بگیرد.مثلا ممکن است جایی که performance مهم است تعداد کمتری در نظر بگیرد و جایی که انجام شدن کار مهم است، فارغ از این که چقدر آن عملیات طول می کشد، تعداد retry بیشتری در نظر بگیرد.
  2. رویکرد دوم به این صورت است که، در پیاده سازی این متد، مکانیزمی در نظر بگیریم، که به صورت خودکار، تعداد retry محاسبه شود. در واقع، هر پارامتری که توقع داریم، استفاده کننده از این متد در نظر بگیرد و با استفاده از آن تعداد retry را محاسبه کند، در خودِ متد، برای محاسبه ی retry، در نظر گرفته شود. مثلا یک فرمول شامل تعداد دفعاتِ اجرای موفقیت آمیز، که در یک ضریب ثابت ضرب می شود و تعداد retry را به دست می آورد. خوبی این رویکرد این است که، در صورت تغییر پارامترهای محاسبه، یک بار این پارامترها را در همین متد تغییر می دهیم، و نیازی نیست که تمام استفاده کنندگان از این متد تغییر کنند. همچنین، این روش، پیچیدگی را به درون ماژول منتقل می کند و از انتشار آن در کل سیستم جلوگیری می کند.

قبل از انتخاب یکی از این دو رویکرد باید به یک سوال پاسخ دهیم:

آیا ماژول های دیگر (هر کسی که قرار است از این متد استفاده کند)، در محاسبه و انتخابِ یک مقدار مناسب برای تعداد retry، بهتر از خود این متد عمل خواهند کرد؟

اگر پاسخ به این سوال بلی است، یعنی ما نمی توانیم یک روش بهینه و مناسب برای محاسبه تعداد retry، در این ماژول پیاده سازی کنیم، پس بهتر است مسئولیت محاسبه ی آن را به استفاده کننده واگذار کنیم. در غیر این صورت، باید خودِ ماژول، مقادیری که نیاز دارد را محاسبه کند و اطلاعات مربوط به retry کردن را (که در این مثال، تعداد است) از دید دنیای بیرون پنهان سازد (پنهان سازی اطلاعات).

یک روش میانه هم این است که، در پیاده سازی متد، یک مقدار پیش فرض محاسبه شود، و در موقعیت های خاص، به استفاده کننده اجازه دهیم که این مقدار پیش فرض را با مقدار مورد نظر خودش جایگزین کند، که در این صورت باز هم اطلاعات ماژول به دنیای بیرون نشت کرده است.

ایده آل این است که، هر ماژول، مسائل خودش را به صورت کامل حل کند و آن را به عهده ی استفاده کنندگان نگذارد.


خلاصه

تصمیم گیری در این مورد که کدام پیچیدگی ها در پیاده سازی ماژول مدیریت شوند و کدام ها را باید به بیرون از ماژول منتقل کرد، نیازمند بصیرت است. مانند یک بندباز، باید بتوانیم تعادل را در این زمینه حفظ کنیم. به طول کلی، می توان معیارهای زیر را، برای زمان هایی که پیچیدگی باید درون ماژول مدیریت شود در نظر گرفت:

  1. زمانی که پیچیدگی مورد نظر، به عملکرد بیزنسی آن ماژول مرتبط است.
  2. زمانی که مدیریت پیچیدگی درون ماژول، باعث می شود که بخش های دیگر نرم افزار بسیار ساده شوند.
  3. زمانی که با مدیریت پیچیدگی درون ماژول، واسط ماژول بسیار ساده می شود.

زمانی که در حال توسعه ی یک ماژول هستید، به دنبال فرصت هایی باشید، که با تحمل سختی بیشتر، بتوانید زحمتِ استفاده کنندگان از آن ماژول را کم کنید.


منابع

A philosophy of software design

طراحی نرم افزاربرنامه نویسیتوسعه نرم افزاربرنامه نویسی ماژولار
linkedin.com/in/sara-rez
شاید از این پست‌ها خوشتان بیاید