ویرگول
ورودثبت نام
سارا رضائی
سارا رضائی
خواندن ۵ دقیقه·۳ سال پیش

با هم یا جدا از هم؟

یکی از سوالات بنیادی در طراحی نرم افزار این است:

اگر بخواهیم دو عملکرد را در نرم افزار پیاده سازی کنیم، بهتر است آن ها را یک جا قرار دهیم، یا در دو جای متفاوت؟

این سوال در همه ی سطوح سیستم ممکن است مطرح شود، در متدها، کلاس ها و ...

برای مثال، اگر یک کلاس داریم که مسئولیت مدیریت فایل را بر عهده دارد، آیا buffering را در همان کلاس بگذاریم یا در یک کلاس مجزا؟

آیا همه ی عملکردِ مربوط به پردازش کردن یک HTTP request، در یک متد پیاده سازی شود؟ یا آن را بین چند متد مختلف پخش کنیم؟

هدف از این تصمیم گیری، کاهش پیچیدگی سیستم و افزایش modularity است. شاید به نظر برسد بهترین کار این است که تا حد ممکن سیستم را به بخش های کوچکتر بشکنیم ولی این کار می تواند بر پیچیدگی سیستم اضافه کند. چرا؟

  • تعداد ماژول های بیشتر، بر پیچیدگی سیستم اضافه می کند. هر چقدر تعداد ماژول ها بیشتر باشد، نگهداری از آن ها سخت تر، و پیدا کردن یک ماژول مشخص بین این همه، دشوار است. همچنین به تعداد ماژول ها، واسط به سیستم اضافه می شود و هر واسط، بر پیچیدگی سیستم می افزاید (توضیح بیشتر درباره ی واسط، در مطلب برنامه نویسی ماژولار)
  • شکستن سیستم به بخش های بیشتر، باعث می شود مدیریت آن بخش ها سخت تر باشد و نیاز به کد بیشتری داشته باشیم. برای مثال بخشی از کد که قبلا فقط با یک object کار می کرد، الان باید با چندین object کار کند و آن ها را مدیریت کند.
  • شکستن باعث جدایی می شود. بخش های مختلف کد، قبل از شکستن، نزدیک هم بودند، ولی حالا از هم دور هستند. مثلا متدهایی که در یک کلاس بودند حالا در کلاس های متفاوت و احتمالا فایل های مجزایی قرار دارند. این باعث می شود برنامه نویس ها، همه ی component هایی که نیاز دارند را یکجا نبینند و شاید حتی از وجود بعضی از آن ها آگاهی نداشته باشند. اگر component ها واقعا مستقل هستند، شکستن خوب است. باعث می شود برنامه نویس روی یک component، در یک زمان تمرکز کند و توسط تعداد زیادی component گیج نشود. ولی اگر بین component ها وابستگی وجود دارد، جداسازی خوب نیست. در این صورت، جداسازی باعث می شود برنامه نویس ناچار شود بین فایل ها و بخش های مختلف مدام جا به جا شود و حتی بدتر از آن ممکن است که اصلا از بعضی وابستگی ها اطلاع نداشته باشد.
  • جداسازی می تواند باعث duplication شود. ممکن است مجبور شویم کدی را که در یک component وجود دارد، در دیگری هم بنویسیم.

به عنوان یک قانون کلی، اگر دو قطعه کد به هم مرتبط هستند، بهتر است در کنار هم باشند، در غیر این صورت آن ها را جدا کنید.

از کجا بفهمیم دو قطعه کد به هم مرتبط هستند؟

  • اگر اطلاعاتی را به اشتراک می گذارند و هر دو وابسته به یک دانش خاص هستند.

مثلا فرض کنید بخواهیم یک Http Request را پردازش و بخش های مختلف آن را از هم جدا کنیم. اگر این بیزنس را در چند متد پخش کنیم، احتمالا همه ی آن ها باید فرمت Http Request را بشناسند و بتوانند بخش های مختلف آن را از هم جدا کنند. بنابراین دانشی که اینجا مشترک است، نحوه ی کار با Http Request است و بهتر است آن را فقط در یک ماژول قرار دهیم.

  • اگر با همدیگر استفاده می شوند.

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

  • اگر از نظر مفهوم، همپوشانی دارند.

یعنی می توانیم یک دسته بندی سطح بالاتر در نظر بگیریم، که این دو قطعه کد هر دو به آن تعلق دارند. مثلا اگر دو قطعه کد داریم، یکی برای جستجوی یک string و دیگری برای تبدیل حروف کوچک و بزرگ به هم، هر دوی آن ها در دسته بندی ویرایش string قرار می گیرند.

  • اگر فهمیدن یک قطعه از کد، بدون نگاه کردن به دیگری، سخت است.

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


بعضی از برنامه نویس ها اعتقاد دارند، متدی که بیشتر از 20 خط کد دارد، باید حتما به چند متد دیگر تقسیم شود!
این جمله همیشه درست نیست. طولانی بودن یک متد، اصلا دلیل خوبی برای شکستن آن نیست. اگر بدون دلیل کافی، یک متد را بشکنیم، تمامی آن متدها واسط خود را به سیستم تحمیل می کنند و این یعنی افزودن پیچیدگی. تنها زمانی مجاز به شکستن یک متد هستیم، که پیچیدگی سیستم را کمتر کند.
متدهای طولانی همیشه بد نیستند. فرض کنید یک متد داریم که 4 قطعه کد دارد، هر قطعه کد هم 10 خط است. کسی که این متد را می خواند، می تواند از قطعه ی بالایی شروع کند و هر قطعه را بفهمد و به آخر متد برسد. اگر این قطعات به هم وابستگی زیاد داشته باشند و هر کدام از آن ها را تبدیل به یک متد دیگر کنیم، برای فهمیدن متد parent، باید مدام بین آن متد و متدهای child رفت و آمد داشته باشیم.
در طراحی متدها، مساله ی مهم این است که متد، فقط و فقط یک کار را، به صورت کامل انجام دهد. همچنین باید یک انتزاع تمیز و ساده فراهم کند و شامل یک واسط ساده باشد که بخش های دیگر کد به راحتی از آن استفاده کند. اگر متد این ویژگی ها را داشته باشد، تعداد خطوط آن، اصلا مهم نیست.

شکستن متد به دو روش قابل انجام است:
1. در کد آن متد، یک بخش از کد را به عنوان یک sub-task تشخصی بدهیم و آن را به یک متد child انتقال دهیم که توسط متد parent استفاده می شود.

اگر از این روش استفاده می کنیم، باید این دو شرط رعایت شود:

  • کسی که متد child را می خواند نیازی نداشته باشد هیچ چیزی درباره ی متد parent بداند.
  • کسی که متد parent را می خواند نیاز نداشته باشد پیاده سازی متد child را نگاه کند تا کد متد parent را بفهمد.

2. یک متد را به دو متد کاملا مجزا بشکنیم که هیچ کدام از دیگری استفاده نمی کنند.

اگر از این روش استفاده می کنیم، باید این شرط رعایت شود:

  • استفاده کننده از این متدها، مجبور نباشد که همیشه هر دوی آن ها را با هم صدا بزند.

خلاصه

تصمیم گیری در مورد جدا کردن ماژول ها، یا یکی کردن آن ها، باید بر اساس پیچیدگی سیستم باشد. ساختاری را انتخاب کنید که نتیجه ی آن پنهان سازیِ بهترِ اطلاعات، کمترین وابستگی و عمیق ترین ماژول ها باشد.

منابع

A philosophy of software design

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