ویرگول
ورودثبت نام
سارا رضائی
سارا رضائیlinkedin.com/in/sara-rez
سارا رضائی
سارا رضائی
خواندن ۷ دقیقه·۵ سال پیش

لایه ی متفاوت، انتزاع متفاوت

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

انتزاع، یعنی حذف جزئیاتِ اشیاء یا سیستم ها و تمرکز روی اجزایی که برای یک هدف خاص، مهم هستند.

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

مثال:

در یک سیستمِ فایل، سه لایه می توانیم داشته باشیم:

1. بالاترین لایه، یک انتزاع از فایل فراهم می آورد. انتزاعی شامل یک آرایه از بایت ها، که قابلیت نوشتن و خواندن دارد.

2. لایه ی بعدی (پایین تر)، مسئول مدیریت یک حافظه ی موقت برای نگهداری بایت های فایل است. لایه های دیگر می توانند مطمئن باشند، بلاک های حافظه که بیشتر استفاده شده، در این حافظه ی موقت نگهداری می شود تا دسترسی دوباره به آن ها سریع باشد.

3. پایین ترین لایه، مسئولیت مدیریت جا به جایی بلاک های حافظه، بین memory و disk را بر عهده دارد.

نشانه هایی وجود دارد که از طریق آن می توانیم بفهمیم انتزاع لایه ها شبیه به هم است و سیستم درست طراحی نشده است:

نشانه ی اول: وجود متدهایی، که هیچ عملکردی ندارند و فقط یک متد دیگر را صدا می زنند.

مثلا کلاس زیر را در نظر بگیرید:

public class TextDocument { private TextArea textArea; public Character getLastTypedCharacter() { return textArea.getLastTypedCharacter(); } public int getCursorOffset() { return textArea.getCursorOffset(); } }

این کلاس دو متد دارد، که هر دو متد، هیچ کاری به جز صدا زدن متدهای شی textArea انجام نمی دهند. در نتیجه، می توان گفت که انتزاع این کلاس، هیچ تفاوتی با انتزاع کلاس TextArea ندارد و مسئولیت ها به درستی تقسیم نشده است.

این متدها، واسط کلاس را پیچیده می کنند و به پیچیدگی کل سیستم می افزایند، ولی برای عملکرد کلی سیستم هیچ فایده ای ندارند. همچنین این متدها، وابستگی بین کلاس ها را بیشتر می کنند و به coupling سیستم می افزایند.

در مثال بالا، متد getLastTypedCharacter را در کلاس TextDocument در نظر بگیرید. این متد، قرار است کاری را انجام دهد، که پیاده سازیِ عملکرد آن، به صورت کامل در کلاس TextArea است. یعنی پیاده سازی در یک کلاس، و واسط در یک کلاس دیگر قرار دارد. این یک نشانه ی خیلی بد از طراحی نامناسب است. به عنوان یک قانون کلی:

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

هر زمان متدهای شبیه به این مثال را در کد دیدید، از خودتان بپرسید: هر کدام از این کلاس ها، دقیقا مسئول چه عملکردی است؟ به احتمال خیلی زیاد، متوجه خواهید شد که مسئولیت ها به درستی تقسیم نشده و بین مسئولیت های دو کلاس، همپوشانی وجود دارد.

راه حل این است، که کلاس ها را طوری تغییر دهیم، که هر کلاس، تعدادی مسئولیتِ مرتبط به هم داشته باشد. این کار را از چند طریق می توان انجام داد:

به تصویر بالا دقت کنید، در حالت (a) کلاس C1 تعدادی از متدهای کلاس C2 را کال کرده و نتیجه را از طریق واسط خودش به بیرون منتقل کرده است. می توانیم این مشکل را به سه طریق حل کنیم:

روش (b): هرکس که متدهای C1 را صدا می زند، به جای آن، متدهای مشابه در C2 را صدا بزند.

روش (c): متدها (در واقع مسئولیت ها) بین دو کلاس تقسیم شده است

روش (d): دو کلاس با هم ادغام شده اند

نشانه ی دوم: نمایان شدن جزئیات پیاده سازی، در واسط

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

یک کلاس را در نظر بگیرید، که قرار است عملکردهایی برای مدیریت متن را فراهم کند (text editor). فرض کنید این کلاس را طوری طراحی کنیم، که بر اساس خطوط متن کار کند، مثلا متدهایی مانند GetLine و PutLine داشته باشد. در این صورت کدهای لایه های دیگر، اگر بخواهند که متنی را در میان یک خط کد حذف کنند، و یا متنی را در میان خط های یک متن دیگر اضافه کنند، باید بدانند که چگونه خطوط را از هم جدا کرده و استفاده از آن ها را مدیریت کنند. در واقع، کار با خطوط، که باید درون کلاس text پنهان می شد، از طریق واسط های نامناسب مانند GetLine، به بیرون نشت داده شده و جزئیات درونی کلاس را آشکار کرده است.

در این مثال، اگر بخواهیم طراحی را اصلاح کنیم، کلاس Text باید در واسط خودش، متدهایی مانند Insert و Delete را ارائه دهد، که یک متن را در یک موقعیت مناسب از متن وارد می کند یا حذف می کند. قطعا این متن، دارای خطوطی است، ولی چون کلاس به درستی طراحی شده، پیچیدگی های مدیریت خطوط متن را در خودش پنهان می کند و انتزاع درستی را در لایه ی خودش برای لایه های دیگر فراهم می کند.

نشانه ی سوم: متغیرهای آواره

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

تصویر زیر را در نظر بگیرید:

همانطور که در (a) می بینیم، یک متغیر به نام cert داریم، که فقط در متد m3 مورد نیاز است. ولی چون این متغیر در main تعریف شده، بین چهار متد main و m1 و m2 و m3 جا به جا شده، بدون این که سه متد دیگر به آن نیازی داشته باشند. بنابراین، واسط متدهای m1 و m2، بیهوده پیچیده شده است و شامل پارامتری است که هیچ نیازی به آن نیست. این مساله به پیچیدگی سیستم می افزاید و تغییرات آینده روی این بیزنس را سخت می کند.

برای حل این مشکل، راه حل (b)، یک object مشترک بین main و m3 در نظر می گیرد. هر اطلاعاتی که m3 به آن ها نیاز دارد را، main در این شی مشترک قرار می دهد تا بعدا m3 از آن استفاده کند. ولی یه مشکل وجود دارد! همین object خودش تبدیل به یک متغیر خواهد شد، که باید بین متدها دست به دست شود، در غیر این صورت چگونه به دست m3 برسد؟

راه حل (c)، یک global variable تعریف می کند و هر اطلاعاتی که m3 نیاز دارد را در آن قرار می دهد. ولی این راه هم مشکلاتی دارد، مثلا اگر دو instance از یک کلاس بخواهیم در یک process داشته باشیم، دسترسی به متغیرهای global چالش هایی خواهد داشت.

راه حل (d)، استفاده از یک متغیر context است، که state کل application را در خودش ذخیره می کند. معمولا اطلاعاتی مثل تنظیمات (configurations) و یا اطلاعاتی که باید بین بخش های مختلف سیستم share شود را در این متغیر ها نگهداری می کنیم. مشکل این راه حل این است که context ممکن است تبدیل به یک شی خیلی سنگین شود و خودش به پیچیدگی و وابستگی در سیستم اضافه کند.

هیچ کدام از این راه حل ها، ایده آل نیستند. اگر راه حل بهتری می شناسید، در بخش نظرات بیان کنید.

خلاصه

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

مواردی که در این نوشته بررسی کردیم، نشانه هایی هستند، که مشخص می کنند ماژولی که طراحی کرده ایم، به اندازه ی کافی، جزئیات را از دنیای بیرون پنهان نکرده است. بنابراین، شاید هزینه ای که وجودش بر سیستم تحمیل می کند، از هزینه ی نبودنش بیشتر باشد، پس نیاز به اصلاح، یا در صورت نیاز، حذف آن ماژول، قابل بررسی است.

منبع

A philosophy of software design

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