منبع اصلی نوشتار زیر در این لینک قرار دارد

طراحی شیءگرا

طراحی شیءگرا

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

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

نشانه های طراحی ضعیف

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

۱- Rigidity (انعطاف ناپذیری): یک ماژول انعطاف ناپذیر است، اگر یک تغییر در آن، منجر به تغییرات در سایر ماژولها گردد. هر چه میزان تغییرات آبشاری بیشتر باشد، نرم افزار خشک تر و غیر منعطف تر است.

۲- Fragility (شکنندگی): وقتی که تغییر در قسمتی از نرم افزار باعث به بروز اشکال در بخش­های دیگر شود.

۳- Immobility (تحرک ناپذیری): وقتی نتوان قسمت هایی از نرم افزار را در جاهای دیگر استفاده نمود و یا به کار گیری آن هزینه و ریسک بالایی داشته باشد.

۴- Viscosity (لزجی): وقتی حفظ طراحی اصولی پروژه مشکل باشد، می­گوییم پروژه لزج شده است. به عنوان مثال وقتی تغییری در پروژه به دو صورت اصولی و غیر اصولی قابل انجام باشد و روش غیر اصولی راحت تر باشد، می گوییم لزج شده است. البته لزجی محیط هم وجود دارد مثلا انجام کار به صورت اصولی نیاز به Build کل پروژه دارد که زیاد طول می­کشد.

۵- Needless Complexity (پیچیدگی اضافی): زمانی که امکانات بدون استفاده در نرم افزار قرار گیرند.

۶- Needless Repetition (تکرارهای اضافی): وقتی که کدهایی با منطق یکسان در جاهای مختلف برنامه کپی می­شوند، این مشکلات رخ می دهند.

۷- Opacity (ابهام): وقتی که فهمیدن یک ماژول سخت شود، رخ می دهد و کد برنامه مبهم بوده و قابل فهم نباشد.

چرا نرم افزار تمایل به پوسیدگی دارد؟

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

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

اصول کلی طراحی

رعایت این اصول باعث می­شوند کد برنامه ساده­تر، انعطاف پذیرتر و قابل نگهداری­تر باشد. در ادامه این اصول بیان می­گردند.

 

KISS

قاعده Keep it simple stupid بیان می کند که به ساده ترین شکل ممکن کد بزنید. در حقیقت یکی از مشکلات رایج در طراحی نرم افزار، حل مسائل از روش های پیچیده است در حالی که می توان با روش ساده تر آن کار را انجام داد.

Dry

قاعده Don’t Repeat Yourself بیان کننده دوری کردن از تکرار کد در برنامه است. برای این کار کدهای تکراری با انتزاع به کلاس های واحدی انتقال می یابند. تکرار نه فقط در کد که در منطق و دانش برنامه هم باید حذف شده و به یک جا منتقل گردد.

Tell, Don’t Ask

این قاعده بیان کننده اصل بسته بندی در شیءگرایی است. بیان به کلاس های بگوییم که چه چیزی از آنها می خواهیم نه اینکه از وضعیت درونی آن سوال کنیم و سپس تصمیم بگیریم که چه چکاری را انجام دهند. این اصل باعث تنظیم مسئولیت های هر کلاس می گردد و از چسبیدگی کلاس ها به یکدیگر جلوگیری می کند.

YAGNI

قاعده You ain’t gonna need it می­گوید که آنقدر کد بزنید که لازم است نه بیشتر. این قاعده در توسعه تست محور[۱] نقش بسیار مهمی دارد. نباید بر اساس تصورات در آینده و هر دلیلی غیر از ضرورت در حال حاضر، کد اضافه به برنامه وارد گردد.

SoC

قاعده Seperation of Concern عبارت است از دسته بندی کردن کد برنامه بر اساس کارکردشان. معماری لایه­ای یک نمونه عالی از اعمال SoC است. جداسازی برنامه به مسئولیت­های جدا از هم نقش مهمی در استفاده مجدد، نگهداری و تست پذیری سیستم دارد.


[۱] Test Driven Development

اصول طراحی SOLID

اصول طراحی SOLID

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

اصل مسئولیت واحد (SRP[1])

هر کلاس باید تنها دارای یک مسئولیت باشد و در نتیجه فقط یک دلیل برای تغییر داشته باشد.

هدف این قانون جدا سازی مسئولیت­های چسبیده به هم است. به عنوان مثال کلاسی که هم مسئول ذخیره سازی و هم مسئول ارتباط با واسط کاربر است، این اصل را نقض می­کند و باید به دو کلاس مجزا تقسیم شود.

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

مثال: مسئولیت واحد در کلاس Rectangle

در شکل زیر کلاس Rectangle دارای دو متد است. متد Draw() یک مستطیل را روی صفحه نمایش رسم می­کند و متد Area() مساحت مستطیل را محاسبه می­کند.

\"image024

همچنین دو برنامه متفاوت از این کلاس استفاده می کنند. برنامه Computational Geometry Application محاسبات هندسی انجام می­دهد، اما هرگز مستطیل رسم نمی کند. برنامه GeographicalApplication هم محاسبات هندسی انجام دهد و هم مستطیل رسم کند.

این طراحی اصل SRP را زیر پا می گذارد زیرا کلاس Rectangle دو مسئولیت دارد. این کار باعث بروز مشکلاتی می­گردد. اولاً باید اسمبلی GUI در برنامه محاسبات هندسی بارگذاری گردد که اصلا چنین کاری منطقی نیست، زیرا این برنامه هیچ استفاده­ای از این اسمبلی نمی­کند. ثانیاً اگر تغییری در GraphicalApplication رخ دهد که باعث تغییر در کلاس Rectangle شود، آنگاه ComputationalGeometryApplication که در این بین هیچ نقشی ندارد، تحت تاثیر قرار می­گیرد. راه حل این مشکل جداسازی این دو مسئولیت در دو کلاس مجزا است.

\"image025

قاعده باز بسته (OCP[2])

یک موجودیت نرم افزاری (کلاس، متد و غیره) باید برای توسعه باز باشد ولی برای تغییر بسته.

چگونه ممکن است که رفتار یک برنامه تغییر کند بدون اینکه کد آن ویرایش شود؟ چگونه می توانیم بدون تغییر یک موجودیت نرم افزاری کارکرد آن را تغییر دهیم؟

پاسخ این سوال در انتزاع است. می­توان یک انتزاع ایجاد کرد که ثابت بوده و گروه نامحدودی از رفتارهای هم خانواده را در بر بگیرد. انتزاع در OOP به صورت کلاس­های انتزاعی[۳] و یا واسط ها[۴] بوده و گروه نامحدودی از رفتارهای ممکن توسط کلاس­های مشتق شده[۵] از آنها بیان می­گردند.

بنابراین یک ماژول می­تواند دارای یک انتزاع باشد. این ماژول برای ویرایش بسته است زیرا وابسته به انتزاعی است که ثابت است، اما رفتار آن می تواند با ایجاد اشتقاق های جدید، تغییر یا توسعه یابد.

مثال: وابستگی Client و Server

شکل زیر یک مثال ساده از نقض اصل OCP را نشان می­دهد. هر دو کلاس Client و Server به صورت واقعی[۶] بوده و انتزاعی نیستند. اگر بخواهیم Client از یک Server دیگری استفاده کند، کلاس Client باید تغییر کند تا نام کلاس Server جدید در آن بنشیند.

\"image026

طراحی زیر بر اساس اصل OCP بنا نهاده شده است. در اینجا کلاس Client به واسط ClientInterface (که یک انتزاع از سرویسی است که باید ارائه شود) وابسته است. بنابراین کلاس­های مشتق شده از ClientInterface می­توانند آن را به هر شکلی که می­خواهند پیاده سازی کنند و کلاس Client از تغییرات در امان خواهد بود.

\"image027

طراحی فوق از الگوی طراحی Strategy بهره می­برد که در پارت ۳ معرفی می­گردد.

علاوه بر الگوی Strategy از الگوی Template Method نیز می­توان برای ایجاد انتزاع و رعایت اصل OCP استفاده کرد. در طراحی زیر کلاس Policy دارای تعدادی توابع سیاست گذاری (PolicyFunction) است که بر اجرای عملیات سیاست گذاری می­کنند.

\"image028

در توابع سیاست گذاری، توابع عملیاتی (SerivceFunction) فراخوانی می­شوند تا سرویس­های درخواستی را انجام دهند. توابع عملیاتی توسط کلاس Policy پیاده سازی نمی­شوند، بلکه کلاس­های مشتق شده از آن (Implementation) این سرویس­ها را فراهم می­کنند. بنابراین عملکرد کلاس Policy می­تواند با ایجاد کلاس­های مشتق شده از آن و باز نویسی توابع عملیاتی تغییر یابد.

قاعده جایگزینی لیسکو (LSP[7])

زیر نوع ها باید بتوانند جایگزین نوع پایه خود باشند.

مکانیزم اساسی در پشت OCP انتزاع و چند شکلی است که با استفاده از وراثت محقق می­شود. به کمک وراثت می­توانیم کلاس­های مشتق شده ایجاد کنیم تا متدهای انتزاعی کلاس پایه را پیاده سازی ­کنند. سوالی که پیش می­آید این است که چگونه باید وراثت را انجام دهیم؟ قواعد طراحی در وراثت چیست؟ بهترین سلسله مراتب کلاس ها به چه شکل است؟ پاسخ این سوالات در اصل Liskov نهفته است. این قاعده می­گوید که اشیاء کلاس­های مشتق شده (زیر نوع­ها) باید بتوانند جایگزین اشیاء کلاس پایه خود (نوع پایه) شوند.

مثال: کلاس Shape

یک نشانه آشکار نقض LSP استفاده از دستورات شرطی برای بررسی نوع اشیاء است. معمولا برای فراخوانی متد مناسب بر اساس نوع شیء این بررسی انجام می­گیرد. کد زیر را در نظر بگیرید:

۱
۲
۳
۴
۵
۶
۷
۸
۹
۱۰
۱۱
۱۲
۱۳
۱۴
۱۵
۱۶
۱۷
۱۸
۱۹
۲۰
۲۱
۲۲
۲۳
۲۴
۲۵
۲۶
۲۷
۲۸
public class Shape
{
private ShapeType type;
public Shape(ShapeType t) { type = t; }
public static void DrawShape(Shape s)
{
if (s.type == ShapeType.square)
(s as Square).Draw();
else if (s.type == ShapeType.circle)
(s as Circle).Draw();
}
}
 
public class Circle : Shape
{
private Point center;
private double radius;
public Circle() : base(ShapeType.circle) { }
public void Draw() {/* draws the circle */}
}
 
public class Square : Shape
{
private Point topLeft;
private double side;
public Square() : base(ShapeType.square) { }
public void Draw() {/* draws the square */}
}

کلاس­های Square و Circle از کلاس Shape مشتق شده اند و دارای متد Draw() هستند، ولی هیچ متدی در Shape را پنهان نمی­کنند. از آنجایی که این دو کلاس قابل تعویض با Shape نیستند، متد DrawShape باید نوع Shape ورودی را بررسی کند و بر اساس نوع آن متد Draw مناسب را فراخوانی نماید.

به این خاطر که کلاس­های Square و Circle نمی­توانند جایگزین Shape شوند، اصل LSP نقض می­شود. از طرف دیگر متد DrawShape() قاعده OCP را زیر پا می­گذارد، زیرا این متد درباره هر کلاس مشتق شده از کلاس پایه Shape باید آگاه بوده و تغییر کند. نقض LSP نتیجه مستقیم نقض اصل OCP است.

قاعده معکوس کردن وابستگی (DIP[8])

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

در طراحی ساخت یافته، ماژولهای سطح بالا به ماژولهای سطح پایین وابسته بودند. این مسئله دو مشکل ایجاد می­کرد:

۱- ماژول­های سطح بالا (سیاست گذار) به ماژول­های سطح پایین (مجری) وابسته هستند. در نتیجه هر تغییری در ماژول­های سطح پایین ممکن است باعث اشکال در ماژول­های سطح بالا گردد.

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

برای رفع این مشکل باید وابستگی به انتزاعات داشته باشیم، یعنی تا جایی که می­توانیم قراردادهای زیر را رعایت کنیم:

۱- هیچ متغیری نباید از نوع یک کلاس واقعی باشد.

۲- هیچ کلاسی نباید از یک کلاس واقعی مشتق شود.

۳- هیچ متدی نباید متد پیاده سازی شده در کلاس پایه خود را پنهان (Override) کند.

مثال: وابستگی کلید به لامپ

مطابق شکل زیر دو کلاس کلید و لامپ را داریم که در آن کلید (Button) به لامپ (Lamp) وابسته است.

\"image029

اشکال طراحی فوق چیست و چرا اصل DIP زیر پا گذاشته می­شود؟ دقت کنید که کلاس Button مستقیما به کلاس Lamp وابسته است. این وابستگی یعنی اینکه هر تغییر در کلاس Lamp ممکن است بر کلاس Button تاثیر بگذارد. همچنین از کلاس Button نمی­توان در جاهای دیگر مانند یک موتور، پنکه و غیره استفاده کرد. در اینجا یک نمونه از Button فقط و فقط یک نمونه از Lamp را کنترل می­کند. این راهکار DIP را نقض می­کند زیرا راهکارهای سطح بالا (روش و خاموش شدن) از پیاده سازی­های سطح پایین (یک لامپ) جدا نیستند و در نتیجه انتزاعات به جزئیات وابسته هستند.

پیدا کردن انتزاع نهفته

در این مثال سیاست سطح بالا کدام است؟ انتزاعی که در پشت این برنامه نهفته است، حقیقتی است که تغییر نمی­کند حتی اگر جزئیات تغییر کنند. در مثال کلید/لامپ انتزاع نهفته عبارت است از اشاره روش/خاموش کردن کاربر و ربط دادن آن اشاره به شیء هدف. چه مکانیزمی برای شناسایی اشاره کاربر وجود دارد؟ (ربطی ندارد!) شیء مقصد چیست؟ (بی ربط است!). اینها جزئیاتی هستند که نباید انتزاع را تحت تاثیر قرار دهند. بنابراین طراحی قبلی می­تواند با طراحی زیر جایگزین شود. در این مدل وابستگی کلید به لامپ معکوس شده است.

\"image030

در این طراحی کلاس Button به واسط ButtonServer وابسته است. این واسط، قراردادی برای روشن و خاموش کردن چیزی تعریف می­کند و کلاس Button به این انتزاع وابسته می­شود. اکنون کلاس Lamp واسط ButtonServer را پیاده سازی می­کند. در اینجا وابستگی معکوس شده است زیرا در طراحی قبل Button به کلاس Lamp وابسته بود ولی در اینجا کلاس Lamp است که به واسط ButtonServer وابسته است. طراحی فوق باعث می­شود که کلاس Button با هر شیء­ی که ButtonServer را پیاده سازی می­کند، کار کند مانند یک موتور، پنکه و غیره.

قاعده تفکیک واسط ها (ISP[9])

کلاینت ها نباید وابسته به متدهایی باشند که آنها را پیاده سازی نمی­کنند.

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

برای رفع این مشکل، آن واسط را به واسط­های کوچکتر تقسیم می­کنید. این تقسیم بندی باید بر اساس استفاده کنندگان از واسط­ها صورت گیرد.

مثال: آلودگی واسط در کلاس­های Door و TimedDoor

در سیستم امنیتی یک سازمان همه درب­ها از واسط زیر پیروی می­کنند.

۱
۲
۳
۴
۵
۶
public interface Door
{
void Lock();
void Unlock();
bool IsDoorOpen();
}

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

۱
۲
۳
۴
۵
۶
۷
۸
۹
۱۰
public class Timer
{
public void Register(int timeout, TimerClient client)
{/*code*/}
}
 
public interface TimerClient
{
void TimeOut();
}

اگر شیءی بخواهد از پایان مهلت (Timeout) آگاه شوند، باید از متد Register استفاده کند تا مهلت زمانی و شیء آگاه شونده را مشخص نماید. در شکل زیر این موضوع بیان شده است.

\"image031

در این طراحی Door واسط TimerClient را پیاده سازی می­کند در حالی که Door به زمانبندی کاری ندارد و فقط به نقش یک درب را دارد. این نقض اصل ISP است زیرا کلاس­های مشتق شده از Door موظف هستند که واسط TimerClient را پیاده سازی کنند، در حالی که نیازی به آن ندارند.

جدا سازی کلاینت­ها به معنی جدا سازی واسطها است

Door و TimedDoor واسطهایی هستند که توسط دو کلاینت متفاوت استفاده می­شوند. چون کلاینت­ها جدا هستند در نتیجه واسطها هم باید جدا باشند. زیرا کلاینت­ها روی واسطهای خود اعمال قدرت می­کنند.

واسطهای کلاس در مقابل واسطهای شیء

دوباره کلاس TimedDoor را در نظر بگیرید. در اینجا یک شیء داریم که دارای دو واسط است و توسط دو کلاینت مجزا استفاده می­گردد (Timer و کلاینت­های Door). این دو واسط باید در یک شیء واحد قرار گیرند زیرا پیاده سازی­های هر دو واسط، داده­های یکسانی را نگهداری می­کنند. در این شرایط چگونه می­توانیم قاعده ISP را اعمال کنیم؟ چگونه می­توانیم واسط ها را جدا کنیم در حالی که آنها باید با یکدیگر بمانند. پاسخ سوال این است که کلاینت­های یک شیء لازم نیست که از طریق واسط به آن دسترسی یابد، بلکه از طریق کلاس پایه­اش (وراثت) نیز می­توانند این کار را انجام دهند.

جداسازی واسط­ها از طریق وراثت

شکل زیر نشان میدهد که چگونه وراثت چندگانه می­تواند منجر به رعایت اصل ISP شود. در این طراحی، TimedDoor از Door و TimerClient ارث بری می­کند. با اینکه کلاینت­های هر دو واسط می­توانند از TimedDoor استفاده کنند، هیچ یک از آنها به TimedDoor وابسته نیستند. در واقع آنها به یک شیء از طریق دو واسط مجزا دسترسی دارند.

\"image032


[۱] The Single-Responsibility Principle

[2] Open Close Principle

[3] Abstract Class

[4] Interface

[5] Derived Class

[6] Concrete

[7] Lisko Substantial Principle

[8] Dependency Inversion Principle

[9] Interface Segregation Principle

 

منبع :agiledevelopment

 



برچسب ها : , , ,