اشتراک‌ها
روش‌هایی برای حذف کلاس‌های utility

- Killing the utility class with premeditation
- Killing the utility class with relocation
- Killing the utility class with consolidation
- Killing the utility class with semantic types
- Killing the utility class with extension methods

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

ایجاد متد بیرونی  

در این روش در کلاس استفاده کننده، متدی برای نیاز جدید ساخته می‌شود. به طور مثال به تکه کد زیر توجه نمایید. در این مثال نیاز داریم در شیء نشان دهنده تاریخ، به روز بعد برسیم. برای این منظور می‌توانیم تکه کدی به صورت زیر داشته باشیم:  
DateTime newStart = new DateTime(previousEnd.Year, previousEnd.Month, previousEnd.Day + 1);
زمانیکه بدست آوردن روز بعدی در جاهای زیادی از کد نیاز باشد، باید این روال را در متدی پیاده سازی کرد. این متد می‌تواند بدنه‌ای به صورت زیر داشته باشد:  
private static DateTime NextDay(DateTime now) 
{ 
      return new DateTime(now.Year, now.Month, now.Day + 1); 
}

ایجاد کلاس توسعه دهنده برای کلاس غریبه 

زمانیکه تعداد متدهای جدید مورد نیاز روی یک کلاس غریبه کم است، روش اول روش قابل قبولی است. اما در مواردی تعداد متدهای جدید مورد نیاز زیاد می‌شوند. در چنین شرایطی بهتر است کلاسی را برای توسعه امکانات کلاس غریبه ایجاد کنیم و متدهای مورد نظر را در آن بنویسیم.
برای ایجاد این کلاس نیز دو روش وجود دارد:
روش اول: ارث بری از کلاس غریبه. این روش در شرایطی که کلاس غریبه به صورت sealed باشد، جواب نخواهد داد. به طور مثال تکه کد زیر را در نظر بگیرید. در این تکه کد با فرض این که کد کلاس Person در دست نیست و نیاز است متدی برای دریافت سن فرد، بر اساس سال تولد او به کلاس اضافه شود.  
public class MyPerson : Person 
{ 
    public int GetAge() 
    { 
        return 0; 
    } 
}
یکی از اشکالات این روش این است که برای استفاده از این کلاس و متد تعریف شده در آن، باید تمامی موارد مورد نیاز متد جدید را از کلاس Person، به نوع کلاس جدید تغییر داد. این تغییر در مقاطعی نیاز به Cast یا ایجاد شیء جدیدی با نوع جدیدی منجر خواهد شد.
روش دوم: ساختن wrapper به دور شیء کلاس غریبه. در این روش کلاسی ایجاد می‌شود و شیء‌ای از کلاس غریبه در آن ساخت شده و تمامی متدهای کلاس غریبه در آن تعریف می‌شوند. این متدها صرفا امر هدایت فرخوانی به متدهای کلاس اصلی را انجام می‌دهند. سپس متدهای جدیدی در این کلاس تعریف و پیاده سازی می‌شوند. برای ایجاد امکان محاسبه سن فرد می‌توان کلاسی را مانند کلاس زیر نوشت:  
public class PersonWrapper 
{ 
    private readonly Person _person; 
    public PersonWrapper(Person person) 
    { 
        _person = person; 
    } 
    public int GetAge() 
    { 
        return 0; 
    } 
}
دقت کنید که در این روش پیاده سازی نیز تمامی خصوصیات و متدهای کلاس اصلی در کلاس wrapper وجود خواهند داشت. استفاده از این کلاس به صورت زیر خواهد بود:  
var person = new Person(); 
var wrapper = new PersonWrapper(person); 
wrapper.GetAge();
در زبان برنامه نویسی سی شارپ امکانی به نام extension method وجود دارد که هدف آن پیاده سازی همین طراحی از طریق امکانات زبان است. برای مطالعه بیشتر این مبحث در زبان سی شارپ می‌توانید به اینجا مراجعه نمایید.  
مطالب
طراحی شیء گرا: OO Design Heuristics - قسمت پنجم

(The God Class Problem (Behavioral Form 

یکی از مخاطراتی که ممکن است موجب عدم بروز مزایای شیء گرایی در طرح شما شود، بحث God Class می‌باشد. شکل رفتاری آن (Behavioral Form) بیشتر در اثر یک خطای مشترک بین توسعه دهندگان پارادایم action-oriented و در جریان مهاجرت به سمت پارادایم شیء گرا، رخ می‌دهد.

این توسعه دهندگان بیشتر سعی در تسخیر و دستیابی به یک مکانیزم کنترل مرکزی شبیه به آنچه در پارادایم action-oriented داشته‌اند، در طراحی شیء گرای خود دارند. حاصل این کار تشکیل کلاسی خواهد بود که همه کارها را انجام می‌دهد، درحالیکه جزئیات ناچیزی هم به عهده مجموعه‌ای از کلاس‌ها سپرده شده است.

قاعده شهودی 3.1

تا حد ممکن هوشمندی سیستم را به صورت افقی و به طور یکنواخت توزیع کنید. به این معنی که کلاس‌های سطح بالای موجود در طراحی، باید کار را به طور یکسان مابین خود به اشتراک بگذارند. (Distribute system intelligence horizontally as uniformly as possible, that is, the top-level classes in a design should share the work uniformly)
منظور اینکه Businessای را که سیستم قرار است پیاده سازی کند، بین کلاس‌های سطح بالا تقسیم کنید. در حالت vertical یا عمودی می‌توان در نظر گرفت که کلاسی این Business را توسط یکسری متد در دل خود پیاده سازی کند و این متدها یکدیگر را فراخوانی خواهند کرد. 
قاعده شهودی 3.2
در سیستم خود God Class ایجاد نکنید. به کلاس هایی که نام آنها شامل Driver، Manager و یا Subsystem می‌باشد، مشکوک باشید. (Do not create god classes/objects in your system. Be very suspicious of a class whose name contains Driver, Manager, System, or Subsystem)

مانند: BlahBlahSystem یا BlahManager

قاعده شهودی 3.3
مراقب کلاس هایی باشید که در واسط عمومی آنها تعداد زیادی Accessor Method تعریف شده است؛ وجود آنها نشان از این دارد که داده و رفتار، در یکجا نگه داشته نشده اند. (Beware of classes that have many accessor methods defined in their public interface. Having many implies that related data and behavior are not being kept in one place)
ازدیاد عملیات get و set در واسط عمومی کلاس‌ها که Accessor Method نامیده می‌شوند، نشان دهنده ایجاد شکل رفتاری God Class می‌باشند. منظور این است که یک کلاس، رفتارهایی برای کار کردن با داده‌های خود در نظر نگرفته است و این داده‌ها را از طریق accessor method‌ها در اختیار کلاس دیگری قرار می‌دهد تا عملیات روی داده‌ها را انجام دهد. در اینجا هم مقصود God Class شدن کلاسی است که از این accessor method‌ها استفاده می‌کند و نشان از این دارد که تعداد رفتارهای آن زیاد خواهد شد. 
قاعده شهودی 3.4
مراقب کلاس هایی باشید که تعداد خیلی زیادی رفتار نامرتبط دارند؛ یعنی رفتارهایی که فقط برروی زیر مجموعه ای از داده‌های کلاس کار می‌کنند. God Class‌ها اغلب دارای اینگونه رفتارهای نامرتبط به هم هستند. (Beware of classes that have too much noncommunicating behavior, that is, methods that operate on a proper subset of the data members of a class. God classes often exhibit much noncommunicating behavior)
منظور اینکه کلاس مورد نظر را می‌توان شکست و تبدیل به دو کلاس مختلف کرد. به عنوان اولین مثال، دامنه مربوط به سیستم برنامه ریزی دوره‌های آموزشی را در نظر بگیرید. در این دامنه، ما با وهله هایی از «Student» ،«Course» و «CourseOffering» سروکار خواهیم داشت. 
قصد داریم با فراخوانی متد ()add_student مربوط به CourseOffering، یک دانشجو را به لیست شرکت کنندگان یک دوره اضافه کنیم. همچنین در این زمان لازم است مطمئن شویم که دانشجوی مورد نظر تمام پیش نیاز‌های دوره انتخاب شده را گذرانده باشد. به نظر شما کدام کلاس می‌بایست عملیات چک کردن پیش نیازها را انجام دهد؟
کلاس دانشجو از دوره‌هایی که گذرانده است آگاه است و کلاس دوره هم از پیش نیاز‌های خود. در بهترین حالت شاید یکی از طراحی‌های زیر را ارائه دهید. به شکلی که یا کلاس دوره با استفاده از متد get_courses مربوط به کلاس دانشجو، داده مورد نیاز را بدست آورده و عملیات چک کردن را در دل خود بگنجاند یا برعکس.

در هر دو طراحی بالا، بخشی از اطلاعات مربوط به policy (سیاست) در کلاس هایی قرار دارد که موضوع تصمیمات سیاست‌ها هستند. این کار از آن جهت که کلاس‌های مورد نظر ما را به دامنه خاصی که این policy در آن دامنه معنا دارد وابسته می‌کند و امکان استفاده مجدد از آن کلاس‌ها را از دست خواهید داد.

راه حل‌های پیشنهادی برای مشکل مطرح شده به شکل زیر می‌باشند:

با توجه به طراحی شکل بالا، یا خود کلاس CourseOffering با استفاده از لیست دوره‌های گذرانده شده توسط دانشجو و لیست دوره‌های پیش نیاز دوره انتخابی، چک کردن را انجام دهد و یا کلاسی با عنوان PrereqChecker که یک Controller Class (کلاسی که فقط رفتار دارد و داده مورد نظر خود را توسط سایر کلاس‌ها و از طریق accessor methodهای آنها تأمین می‌کند) می‌باشد، وظیفه چک کردن را برعهده بگیرد.


علاوه بر اینکهaccessor method ها، داده را برای کلاس‌های کنترلر مهیا می‌کنند (مانند مثال بالا)، وظیفه‌ی مهیا کردن داده برای UI (رابط کاربری) را نیاز بر عهده خواهند داشت. به این صورت که رابط کاربری، با استفاده از این متدها، مشخصات درونی مدل را نمایش دهد و یا این امکان را به کاربر می‌دهد که این مشخصات درونی مدل را ویرایش کرده و به سمت مدل ارسال نماید.

قاعده شهودی 3.5

در برنامه‌هایی که شامل یک مدل شی گرایی می‌باشند که با رابط کاربری تعامل دارند، مدل نباید به رابط کاربری وابسته باشد. رابط کاربری می‌بایست وابسته به مدل باشد. (In applications that consist of an object-oriented model interacting with a user interface, the model should never be dependent on the interface. The interface should be dependent on the model)

برای عدم نقض این قاعده شهودی، لازم است در مدل یکسری accessor method در نظر گرفته شود تا رابط کاربری از آن استفاده کند؛ ولی باید مراقب بود که این accessor method‌ها صرفا توسط رابط کاربری استفاده شود و عدم توجه به این قضیه، احتمالا شما را به سمت نقض قاعده 3.3 متمایل خواهد کرد.

مطالب
طراحی شیء گرا: OO Design Heuristics - قسمت چهارم

Dynamic Semantics

Objectها علاوه بر داده و رفتار به عنوان توصیفات ثابت، در زمان اجرا دارای یک Local State (‏‏a snapshot) از مقادیر داینامیک مربوط به اعضای داده‌ای خود، می‌باشند. مجموعه تمام حالاتی که وهله‌های یک کلاس می‌توانند بین آنها گذر (transition) داشته باشد، dynamic semantics مربوط به کلاس نامیده می‌شود و به وهله‌های کلاس این امکان را می‌دهند تا به یک پیغام مشابه رسیده و در زمان‌های مختلف از چرخه زندگی خود، به اشکال مختلف پاسخ دهند.

Method junk for the class X 
if (local state #1) then
do something
else if (local state #2) then
do something different
End Method

بخش اصلی هر طراحی شیء گرا، dynamic semantics وهله‌ها می‌باشد. dynamic semantics هر کلاسی باید در قالب یک دیاگرام state-transition مستند شود. شکل زیر dynamic semantics پروسه‌های موجود در یک سیستم عامل را در قابل یک دیاگرام حالت نمایش می‌دهد. این پروسه‌ها توانایی این را دارند که در هر کدام از حالات: runnable، current process، blocked، sleeping و یا در حالت exited، قرار داشته باشند. همچنین به عنوان مثال، یک پروسه زمانی می‌تواند در حالت current process قرار گیرد که حتما قبلا در حالت runnable قرار داشته باشد. این اطلاعات برای ایجاد تست برای کلاس‌ها و وهله‌های آنها می‌تواند مفید واقع شود.

شکل 2.8 State-transition diagram notation 

برخی از طراحان به طور تصادفی، dynamic semantics یک کلاس را به عنوان static semantics آن کلاس مدل می‌کنند. به عنوان مثال اگر color یکی از اعضای داده ای (data member) کلاس توپ باشد و بعد از وهله سازی از کلاس توپ، color آن بازهم قابل تغییر باشد، منظور اینکه توپ آبی به عنوان یک وهله از کلاس توپ در زمان حیات خود تغییر رنگ دهد، اصطلاحا می‌گویند: color جزء dynamic semantics کلاس توپ می‌باشد. با توجه به توضحیاتی که داده شد، حال اگر طراحی برای هر رنگ توپ یک کلاس جدا در نظر گرفته باشد، dynamic semantics را به عنوان static semantics مدل کرده و به احتمال زیاد ما را به سمت ایجاد مشکل Class Proliferation (ازدیاد کلاس ها) سوق خواهد داد.

Abstract Classes

به سوالات زیر توجه کنید:

  • آیا هرگز میوه خورده‌اید؟
  • آیا هرگز پیش غذا خورده‌اید؟ 
  • آیا هرگز دسر خورده‌اید؟ 
اکثر مردم به این سوالات جواب «بله» را خواهند داد.
حال با توجه به سوالات «مزه غذا چطور بود؟ دسری که خوردید، چه تعداد کالری داشت؟ هزینه پیش غذایی که خوردید چقدر بود» پاسخ چه خواهد بود؟
من (نویسنده) ادعا میکنم که هیچ کسی تا به حال میوه نخورده است. بیشتر مردم، سیب، موز و پرتقال خورده‌اند؛ میوه‌ی قرمز رنگی به ارزش 3 پوند را نخورده‌اند.

شبیه به این مسئله برای زمانی است که گارسون رستوران از شما سوال می‌کند: «برای شام چه چیزی میل دارید» و شما جواب می‌دهید: «یک پیش غذا، یک غذای اصلی و یک دسر». در این حالت چون شما دقیقا مشخص نکرده‌اید چه نوعی می‌خواهید، گارسون، مات و مبهوت خواهد ماند. همه می‌دانیم که چیزی تحت عنوان میوه، پیش غذا و یا وهله دسر در واقعیت وجود ندارد؛ بله این عبارات اطلاعات مفیدی را تسخیر می‌کنند. اگر من در دستم یک ساعت زنگی گرفته و از شما می‌پرسیدم: «نظرتان در مورد میوه من چیست؟»؛ بدون شک فکر می‌کردید من دیوانه شده‌ام. حال اگر در دستم سیبی گرفته و سوال قبلی را می‌پرسیدم، این بار از نظر شما من یک شخص عاقل بودم.
با وجود اینکه نمی‌توان از میوه وهله سازی کرد، اما اطلاعات مفیدی را تسخیر می‌کند. در واقع میوه، یک کلاسی (concept) است که دانشی از نحوه وهله سازی وهله هایش به وسیله Type پیاده ساز خود، ندارد.

کلاسی که دانشی از نحوه وهله سازی وهله‌های خود ندارد، abstract class (کلاس مجرد یا انتزاعی) نامیده می‌شود.
کلاسی که دانش نحوه وهله سازی وهله‌های خود دارد، concrete class نامیده می‌شود.

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

Roles Versus Classes

قاعده شهودی 2.11
مطمئن باشید انتزاع هایی را که مدل می‌کنید کلاس بوده و نه نقش‌هایی که وهله‌های آنها بازی می‌کنند. (Be sure the abstractions that you model are classes and not simply the roles objects play)
آیا مادر و پدر به عنوان یک کلاس هستند یا نقش‌هایی هستند که وهله‌های کلاس شخص، بازی می‌کند؟ پاسخ این سوال وابسته به دامینی (domain) است که طراح در حال مدل سازی آن می‌باشد. اگر در دامین مورد نظر، مادر و پدر رفتارهای مختلفی دارند، احتمالا باید به عنوان کلاس‌های جدا مدل شوند. اگر رفتارهای یکسانی دارند، در نتیجه نقش‌های مختلفی هستند که وهله‌های کلاس شخص بازی می‌کنند. به عنوان مثال، می‌توان کلاس خانواده را متشکل از وهله‌ای از کلاس پدر، وهله‌ای از کلاس مادر و مجموعه‌ای از وهله‌های کلاس فرزند در نظر گرفت. در مقابل ممکن است کلاس خانواده را متشکل از وهله‌ای از کلاس شخص به عنوان پدر، وهله‌ای از کلاس شخص به عنوان مادر و آرایه‌ای از وهله‌های شخص به عنوان فرزندان، مدل کنید. قرار گرفتن در وضیعتی که هر نقش، بخشی از رفتاری‌های شخص را مورد استفاده قرار می‌دهد، کافی نیست و باید مطمئن شوید که رفتار‌ها واقعا متفاوت می‌باشند. همچنین باید به یاد داشته باشید که زمانیکه وهله‌ای از بخشی از رفتارهای کلاس خود استفاده می‌کند، نیز مشکلی وجود ندارد و لازم نیست کلاس‌های دیگری را به خاطر این موضوع در طراحی خود در نظر بگیرید.

شکل 2.9 Two views of a family   

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

قواعد شهودی فصل دوم

قاعده شهودی 2.1 
همه داده‌ها باید در داخل کلاس خود پنهان شده باشند. (All data should be hidden within its class) 
قاعده شهودی 2.2
استفاده کنندگان از کلاس باید به واسط عمومی آن وابسته باشند، اما یک کلاس نباید به استفاده کنندگان خود، وابسته باشد. (Users of a class must be dependent on its public interface, but a class should not be dependent on its users)
قاعده شهودی 2.3
تعداد پیغام‌های موجود در قرارداد یک کلاس را کمینه سازید. (Minimize the number of messages in the protocol of a class) 
قاعده شهودی 2.4
پیاده سازی یک واسط عمومی یکسان کمینه برای همه کلاس‌ها  (Implement a minimal public interface that all classes understand [e.g., operations such as copy (deep versus shallow), equality testing, pretty printing, parsing from an ASCII description, etc.].) 
قاعده شهودی 2.5 
جزئیات پیاده سازی، مانند توابع خصوصی common-code  ( توابعی که کد مشترک سایر متدهای کلاس را در بدنه خود دارند) را در واسط عمومی یک کلاس قرار ندهید.  (Do not put implementation details such as common-code private functions into the public interface of a class)
قاعده شهودی 2.6 
واسط عمومی کلاس را با اقلامی که یا استفاده کنندگان از کلاس توانایی استفاده از آن را نداشته و یا تمایلی به استفاده از آنها ندارند، آمیخته نکنید.  (Do not clutter the public interface of a class with items that users of that class are not able to use or are not interested in using )
قاعده شهودی 2.7
اتصال و پیوستگی مابین کلاس‌ها باید از نوع Nil یا Export باشد؛ به این معنی که یک کلاس فقط از واسط عمومی کلاس دیگر استفاده کند یا کاری با آن نداشته باشد. (Classes should only exhibit nil or export coupling with other classes, that is, a class should only use operations in the public interface of another class or have nothing to do with that class.)
قاعده شهودی 2.8 
یک کلاس باید یک و تنها یک Key Abstraction را تسخیر نماید. (A class should capture one and only one key abstraction) 
قاعده شهودی 2.9 
داده و رفتار مرتبط را در یک جا (کلاس) نگه دارید. (Keep related data and behavior in one place)
قاعده شهودی 2.10 
اطلاعات نامرتبط به هم را در کلاس‌های جدا از هم قرار دهید. ((Spin off nonrelated information into another class (i.e., noncommunicating behavior)
قاعده شهودی 2.11
مطمئن باشید انتزاع هایی را که مدل می‌کنید کلاس بوده و نه نقش‌هایی که وهله‌های آنها بازی می‌کنند. (Be sure the abstractions that you model are classes and not simply the roles objects play)  
مطالب
طراحی شیء گرا: OO Design Heuristics - قسمت اول
هدف از طراحی چیست؟

ما طراحی می‌کنیم تا علاوه بر نیاز‌های عملیاتی، به نیاز‌های غیر عملیاتی (Non Functional Requirements) نیز فکر کنیم؛ در حالیکه در زمان برنامه نویسی صرفا به Functionality فکر می‌کنیم.

کتاب Object Oriented Design Heuristics اولین کتاب در زمینه طراحی و توسعه شیء گرا می‌باشد. خواندن آن برای برنامه نویسان در هر رده ای که هستند، مفید خواهد بود و میتوانند از این Heuristicها (قواعد شهودی) به عنوان ابزاری برای تبدیل شدن به یک توسعه دهنده برتر، استفاده کنند.

در این کتاب بیشتر، بهبود طراحی شیء گرا هدف قرار داده شده‌است و در این راستا بیش از 60 دستورالعمل که هیچ وابستگی به زبان خاصی هم ندارند، ارائه شده است. قواعد شهودی در واقع قوانین سخت گیرانه‌ای نیستند. بلکه می‌توان آن‌ها را به عنوان یک مکانیزم هشدار در نظر گرفت که در زمان نیاز حتی میتوان آنها را نقض کرد.

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

Introduction to Classes and Objects

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

این مفاهیم را با یک مثال واقعی، بهتر می‌توان شرح داد. یک اتاق پر از جمعیت را درنظر بگیرید؛ اگر شما می‌پرسیدید «چه تعداد از حاضرین در این اتاق می‌توانند یک ساعت زنگدار(alarm clock ) را با در دست داشتن تمام قطعات آن، بسازند؟» در بهترین حالت یک یا دو نفر تمایل داشتند دست خود را بالا ببرند. اگر در همین اتاق می‌پرسیدید، «چه تعداد از حاضرین در این اتاق می‌توانند یک ساعت زنگدار را برای ساعت 9 صبح تنظیم کنند؟» بدون شک بیشتر جمعیت تمایل داشتند دست خود را بالا ببرند.

آیا نامعقول نیست که این تعداد جمعیت زیاد، ادعا دارند که میتوانند از ساعت زنگدار استفاده کنند، درحالیکه حتی نمی‌توانند یک ساعت زنگدار بسازند؟ پاسخ بی درنگ برای این سوال «البته که نه! سوال شما نامعقول است» می‌باشد.

در دنیای واقعی خیلی چیزها هستند که ما میتوانیم از آنها استفاده کنیم، بدون آنکه دانشی درباره پیاده سازی آنها داشته باشیم؛ مانند: یخچال‌ها، اتومبیل‌ها، دستگاه‌های فتوکپی، کامپیوترها و غیره. چون آنها برای استفاده شدن از طریق واسط عمومی خودشان، تعریف و طراحی شده‌اند. لذا حتی بدون داشتن دانشی از پیاده سازی آنها، استفاده از آنها آسان می‌باشد. این واسط عمومی وابسته به دستگاه مورد نظر است. اما جزئیات پیاده سازی دستگاه را از دید کاربرانش پنهان میکند. این استراتژی طراحی، چیزی است که به سازنده اجازه می‌دهد بدون آنکه کاربران رنجیده شوند، با آزادی عمل، 60 مؤلفه کوچک استفاده شده در ساخت ساعت زنگدار را تعویض کند.

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

این فلسفه، دقیقا یکی از ایده‌های پایه‌ای در پارادایم شیءگرا می‌باشد. تمام جزئیات پیاده سازی در سیستم شما باید در پشت یک واسط عمومی مستحکم و سازگار، از کاربران آنها پنهان باشد. نیاز کاربران، دانستن درباره واسط عمومی می‌باشد؛ اما هرگز مجاز به دیدن جزئیات پیاده سازی آنها نیستند. با این روش، پیاده ساز میتواند به هرشکلی که مناسب است، پیاده سازی را تغییر دهد؛ درحالیکه واسط عمومی مانند سابق می‌باشد. به عنوان مسافری که مکرر سفر میکنم، به شما اطمینان میدهم که استفاده از ساعت‌های زنگدار با وجود عدم اطلاع از پیاده سازی آنها، فواید عظیمی دارند. در هتل‌های زیادی که از دسته بندی‌های گسترده‌ای از ساعت‌ها مانند الکتریکی، قابل کوک (windup)، باتری خور، در هر دو مدل دیجیتال و آنالوگ استفاده میکنند، اقامت کرده‌ام. یکبار هم اتفاق نیفتاده‌است در حالیکه در هواپیما نشسته باشم، نگران این باشم که قادر نخواهم بود از ساعت زنگی اتاقم در هتل استفاده کنم.

بیشتر خوانندگان این کتاب، با وجود اینکه در نزدیکی آنها شاید ساعت زنگداری هم نباشد، ولی منظور بنده را با عبارت «ساعت زنگدار» متوجه شدند. به چه دلیل؟ شما در زندگی خودتان ساعت‌های زنگدار زیادی را می‌بینید و متوجه می‌شوید که همه آنها از یکسری خصوصیات مشترک مانند زمان، یک زمان هشدار و طراحی‌ای که مشخص میکند هشدار روشن یا خاموش است، بهره می‌برند. همچنین متوجه می‌شوید که همه ساعت‌های زنگداری که دیده‌اید امکان تنظیم کردن زمان، تنظیم زمان هشدار و روشن و خاموش کردن هشدار را به شما می‌دهند. در نتیجه، شما الان مفهومی را به نام «ساعت زنگدار» دارید که مفهومی را از داده و رفتار، در یک بسته بندی مرتب برای همه ساعت‌های زنگدار، تسخیر می‌کند. این مفهوم به عنوان یک Class (کلاس) شناخته می‌شود. یک ساعت زنگدار فیزیکی که شما در دست خود آن را نگه داشته‌اید، یک Object (وهله، Instance) ای از کلاس ساعت زنگدار می‌باشد. رابطه بین مفهوم کلاس و وهله، Instantiation Relationship (وهله سازی) نام دارد. به یک object، ساعت زنگدار وهله سازی شده (Instantiated) از کلاس ساعت زنگدار گفته می‌شود؛ در حالیکه از کلاس ساعت زنگدار به عنوان تعمیم (Generalization) از همه object‌های کلاس ساعت زنگدار که شما با آنها روبرو شده‌اید، یاد می‌شود. 

شکل 2.1 An Alarm Class and Its Objects 

شکل 2.1 An Alarm Class and Its Objects

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

یک object همیشه دارای 4 جنبه مهم زیر خواهد بود:
  • هویت خود (ممکن است آدرس آن در حافظه باشد) - its own identity
  • خواص کلاس خود (معمولا استاتیک) و مقادیر این خواص (معمولا پویا) - attributes of its class 
  • رفتار کلاس خود (از دید پیاده ساز) -  behavior of its class
  • واسط منتشر شده کلاس خود (از دید استفاده کننده) - published interface of its class

یک کلاس را  می توان با record definition (ساختار داده پایه، struct) و لیستی از عملیاتی که مجاز به کار بر روی این record definition هستند، پیاده سازی کرد. در زبان‌های رویه‌ای (Procedural) یافتن وابستگی داده‌ها در یک تابع معین، آسان می‌باشد. این کار را می‌توان به سادگی با بررسی کردن جزئیات پیاده سازی تابع و مشاهده نوع داده پارامترهای آن، مقادیر بازگشتی و متغییرهای محلی‌ای که تعریف شده‌اند، انجام داد. اگر قصد شما پیدا کردن وابستگی‌های تابعی بر روی یک داده می‌باشد، باید همه کد را بررسی کرده و به دنبال توابعی باشید که به داده شما وابسته هستند. در مدل شیء گرا، هر دو نوع وابستگی (داده به رفتار و رفتار به داده) به راحتی در دسترس می‌باشند. وهله‌ها، متغیرهایی از یک نوع داده کلاس هستند. جزئیات داخلی آنها باید فقط برای لیست توابع مرتبط با کلاس‌هایشان آشکار باشد. این محدودیت دسترسی به جزئیات داخلی وهله‌ها، Information Hiding نامیده می‌شود. اختیاری بودن این بحث در خیلی از زبان‌های شیء گرا ما را به سمت اولین قاعده شهودی هدایت می‌کند.

قاعده شهودی 2.1 
همه داده‌ها باید در داخل کلاس خود پنهان شده باشند. (All data should be hidden within its class)

با نقض این قاعده، امکان نگهداری را هم از دست می‌دهید. اجبار به پنهان کردن اطلاعات در مراحل طراحی و پیاده سازی، بخش عظیمی از فواید پارادایم شیء گرا می‌باشد. اگر داده به صورت عمومی تعریف شده باشد، تشخیص اینکه کدام بخش از عملیات (functionality) سیستم به آن داده وابسته است، سخت و مشکل خواهد بود. در واقع، نگاشت تغییرات داده به عملیات سیستم، همانند طراحی و پیاده سازی در دنیای action-oriented می‌باشد. ما مجبور می‌شویم برای تشخیص اینکه کدام عملیات به داده مورد نظر ما وابسته است، تمام عملیات سیستم را بررسی کنیم، تا به این ترتیب متوجه شویم.

برخی اوقات، یک توسعه دهنده استدلال می‌کند «نیاز دارم این بخش از داده را عمومی تعریف کنم زیرا ....» در این وضعیت، توسعه دهنده باید از خود سوال کند «کاری که تلاش دارم با این داده انجام دهم چیست و چرا کلاس این عملیات را خودش برای من انجام نمی‌دهد؟» در همه موارد  این کلاس است که به سادگی عملیات ضروری را فراموش کرده‌است. کمی بر روی شکل 2.2 فکر کنید. توسعه دهنده به صورت تصادفی فکر کرده است که عضو byte_offset را برای مجاز ساختن دسترسی تصادفی I/O، به صورت عمومی تعریف کند. اما چیزی که واقعا برای انجام این کار به آن نیاز داشت، تعریف یک operation بود (در زبان سی، توابع fseek و ftell برای ممکن کردن دسترسی تصادفی I/O، موجود هستند).

مراقب توسعه دهنده‌هایی که جسورانه می‌گویند: «ما می‌توانیم این بخش از داده را تغییر دهیم، زیرا هیچوقت تغییر نخواهد کرد!» باشید. طبق قانون برنامه نویسی مورفی، اولین بخشی که نیاز به تغییر خواهد داشت همین بخش از داده است.

شکل 2.2 Accidental Public Data   

 Accidental Public Data

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

شکل 2.3  The danger of public data 

 خطر داده‌های عمومی

مطالب
طراحی شیء گرا: OO Design Heuristics - قسمت دوم

در قسمت اول با مفاهیم اولیه Class و Object آشنا شدیم.

Messages and Methods

Objectها باید مانند ماشین‌هایی تلقی شوند که عملیات موجود در واسط عمومی خود را برای افرادی که درخواست مناسبی ارسال کنند، اجرا خواهند کرد. با توجه به اینکه یک object از استفاده کننده خود مستقل است و وابستگی به او ندارد و همچنین توجه به ساختار نحوی (syntax) برخی از زبان‌های شیء گرای جدید، عبارت «sending a message» برای توصیف اجرای رفتاری از مجموعه رفتارهای object، استفاده میشود.
به محض اینکه پیغامی (Message) به سمت object ارسال شود، ابتدا باید تصمیم بگیرد که این پیغام ارسالی را درک می‌کند. فرض کنیم این پیغام قابل درک است. در این صورت object مورد نظر، همزمان با نگاشت پیغام به یک فراخوانی تابع (function call)، خود را به صورت ضمنی به عنوان اولین آرگومان ارسال می‌کند. تصمیم گرفتن در رابطه با قابل درک بودن یک پیغام، در زبان‌های مفسری در زمان اجرا و در زبان‌های کامپایلری در زمان کامپایل، انجام میگرد. 
نام (یا prototype) رفتار یک وهله، Message (پیغام) نامیده می‌شود. بسیاری از زبان‌های شیء گرا مفهموم Overloaded Functions Or Operators را پشتیبانی می‌کنند. در این صورت می‌توان در سیستم دو تابعی داشت که با نام یکسان، یا انواع مختلف آرگومان (intraclass overloading) داشته باشند یا در کلاس‌های مختلفی (interclass overloading) قرار گیرند. 
ممکن است کلاس ساعت زنگدار، دو پیغام set_time که یکی از آنها با دو آرگومان از نوع عدد صحیح و دیگری یک آرگومان رشته‌ای است داشته باشد.

void AlarmClock::set_time(int hours, int minutes); 

void AlarmClock::set_time(String time);

در مقابل، کلاس ساعت زنگدار و کلاس ساعت مچی هر دو messageای به نام set_time با دو آرگومان از نوع عدد صحیح دارند.

void AlarmClock::set_time(int hours, int minutes); 

void Watch::set_time(int hours, int minutes);

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

در برخی از زبان‌ها و یا سیستم‌ها، اطلاعات دیگری مانند: انواع استثناءهایی که از سمت پیغام پرتاب می‌شوند تا اطلاعات همزمانی (پیغام به صورت همزمان است یا ناهمزمان) را برای استفاده کننده مهیا کنند. از طرفی پیاده سازی کنندگان یک کلاس باید از پیاده سازی پیغام آگاه باشند. جزئیات پیاده سازی یک پیغام -کدی که پیغام را پیاده سازی می‌کند- Method (متد) نامیده میشود. آنگاه که نخ (thread) کنترل درون متد باشد، برای مشخص کردن اینکه پیغام رسیده برای کدام وهله ارسال شده‌است، ارجاعی به وهله مورد نظر و به عنوان اولین آرگومان، به صورت ضمنی ارسال می‌شود. این آرگومان ضمنی، در بیشتر زبان‌ها Self Object نامیده می‌شود (در سی پلاس پلاس this object نام دارد). در نهایت، مجموعه پیغام‌هایی که یک وهله می‌تواند به آنها پاسخ دهد، Protocol (قرارداد) نام دارد.

دو پیغام خاصی که کلاس‌ها یا وهله‌ها می‌توانند به آنها پاسخ دهند، اولی که استفاده کنندگان کلاس برای ساخت وهله‌ها از آن استفاده می‌کنند، Constructor (سازنده) نام دارد. هر کلاسی می‌تواند سازنده‌های متعددی داشته باشد که هر کدام مجموعه پارامترهای مختلفی را برای مقدار دهی اولیه می‌پذیرند. دومین پیغام، عملیاتی است که وهله را قبل از حذف از سیستم، پاک سازی می‌کند. این عملیات، Destructor (تخریب کننده) نام دارد. بیشتر زبان‌های شیء گرا، برای هر کلاس تنها یک تخریب کننده دارند. این پیغام‌ها را به عنوان مکانیزم مقدار دهی اولیه و پاک سازی در پارادایم شیء گرا در نظر بگیرید.

قاعده شهودی 2.2

استفاده کنندگان از کلاس باید به واسط عمومی آن وابسته باشند، اما یک کلاس نباید به استفاده کنندگان خود، وابسته باشد. (Users of a class must be dependent on its public interface, but a class should not be dependent on its users)

منطق پشت این قاعده، یکی از شکل‌های قابلیت استفاده مجدد (resuability) می‌باشد. یک ساعت زنگدار ممکن است توسط شخصی در اتاق خواب او استفاده شود. واضح است که شخص مورد نظر به واسط عمومی ساعت زنگدار وابسته می‌باشد. به هر حال، ساعت زنگدار نباید به شخصی وابسته باشد. اگر ساعت زنگدار به شخصی وابسته باشد، بدون مهیا کردن یک شخص، نمی‌توان از آن برای ساخت یک TimeLockSafe استفاده کرد. این وابستگی‌ها برای مواقعیکه می‌خواهیم امکان این را داشته باشیم تا کلاس ساعت زنگدار را از دامین (domain) خود خارج کرده و در دامین دیگری، بدون وابستگی هایش مورد استفاده قرار دهیم، نامطلوب هستند.

شکل 2.4 The Use Of Alarm Clocks

 The Use Of Alarm Clocks قاعده شهودی 2.3

تعداد پیغام‌های موجود در قرارداد یک کلاس را کمینه سازید. (Minimize the number of messages in the protocol of a class)

چندین سال قبل، مطلبی منتشر شد که کاملا متضاد این قاعده شهودی می‌باشد. طبق آن، پیاده سازی کننده یک کلاس می‌تواند یکسری عملیات را با فرض اینکه در آینده مورد استفاده قرار گیرند، برای آن در نظر بگیرد. ایراد این کار چیست؟ اگر شما از این قاعده پیروی کنید، حتما کلاس LinkedList من، توجه شما را جلب خواهد کرد؛ این کلاس در واسط عمومی خود 4000 عملیات را دارد. فرض کنید قصد ادغام دو وهله از این کلاس را داشته باشید. در این صورت حتما فرض شما این است که عملیاتی تحت عنوان merge در این کلاس تعبیه شده است. بعد از جستجوی بین این تعداد عملیات، در نهایت این عملیات خاص را پیدا نخواهید کرد. چراکه این عملیات متأسفانه به صورت یک overloaded plus operator پیاده سازی شده است. مشکل اصلی واسط عمومی با تعداد زیادی عملیات این است که فرآیند یافتن عملیات مورد نظرمان را خیلی سخت یا حتی ناممکن خواهد کرد و مشکلی جدی برای قابلیت استفاده مجدد تلقی می‌شود.

با کمینه نگه داشتن تعداد عملیات واسط عمومی، سیستم، قابل فهم‌تر و همچنین مولفه‌های (components) آن به راحتی قابل استفاده مجدد خواهند بود.

قاعده شهودی 2.4

پیاده سازی یک واسط عمومی یکسان کمینه برای همه کلاس‌ها  (Implement a minimal public interface that all classes understand [e.g., operations such as copy (deep versus shallow), equality testing, pretty printing, parsing from an ASCII description, etc.].)

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

به عنوان مثال کلاس Object در دات نت به عنوان کلاس پایه ضمنی با یکسری از متدهای عمومی (برای مثال ToString)، نشان دهنده تعریف یک واسط عمومی مشترک برای همه کلاس‌ها در این فریمورک، می‌باشد.

public class Object
    {
        public Object();
        public static bool Equals(Object objA, Object objB){...}
        public static bool ReferenceEquals(Object objA, Object objB){...}
        public virtual bool Equals(Object obj){...}
        public virtual int GetHashCode(){...}
        public Type GetType(){...}
        public virtual string ToString(){...}
        protected Object MemberwiseClone(){...}
    }


قاعده شهودی 2.5 

جزئیات پیاده سازی، مانند توابع خصوصی common-code  ( توابعی که کد مشترک سایر متدهای کلاس را در بدنه خود دارند) را در واسط عمومی یک کلاس قرار ندهید.  (Do not put implementation details such as common-code private functions into the public interface of a class)

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

شکل 2.5  Example of a common-code private function

Example of a common-code private function

مثال واقعی

فرض کنید در شکل بالا، کلاس X معادل یک LinkedList کلاس، f1و f2 به عنوان توابع insert و remove و تابع f به عنوان تابع common-code که عملیات یافتن آدرس را برای درج و حذف انجام می‌دهد، می‌باشند.

قاعده شهودی 2.6

واسط عمومی کلاس را با اقلامی که یا استفاده کنندگان از کلاس توانایی استفاده از آن را نداشته و یا تمایلی به استفاده از آنها ندارند، آمیخته نکنید.  (Do not clutter the public interface of a class with items that users of that class are not able to use or are not interested in using )

 این قاعده شهودی با قاعده قبلی که با قرار دادن تابع common-code در واسط عمومی کلاس، فقط باعث در هم ریختن واسط عمومی شده بود، مرتبط می‌باشد. در برخی از زبان‌ها مانند C++‎، برای مثال این امکان وجود دارد که سازنده یک کلاس انتزاعی (abstract) را در واسط عمومی آن قرار دهید؛ حتی با وجود اینکه در زمان استفاده از آن سازنده با خطای نحوی روبرو خواهید شد. این قاعده شهودی کلی، برای کاهش این مشکلات در نظر گرفته شده است.