در این مقاله با یک داستان واقعی، به بررسی “اشتباهات و نکات کلیدی در مبحث ارث بری در شی گرایی” می پردازیم.
چند روز قبل ایمیلی از یکی از دانشجویانم دریافت کردم. در آن، از من در مورد پروژه ای که روی آن کار می کرد راهنمایی خواست. او در حال کار روی برنامه ای است که توسط مدیران تیم های ورزشی استفاده می شود. آنها می توانند بازیکنان، تیم ها و مربیان را ایجاد، ویرایش و حذف کنند. بنابراین، طرحی که دانشجوی من در ذهن داشته است، چیزی شبیه به این نمودار UML است:
پس، SoccerPlayer از نوع Player است و از آن ارث می برد. به همین شکل Player هم از نوع Person است (همانند Coach). این همان چیزی است که بسیاری از کتاب ها در مورد برنامه نویسی شی گرا به شما می آموزند:
- وقتی می توانید ارتباط بین دو مفهوم را با استفاده از Is-a (مثال : SoccerPlayer is a Player یعنی SoccerPlayer از نوع Player) بیان کنید، این یک inheritance است!
بسیاری از این کتاب ها ادعا می کنند که با ایجاد این انتزاعات (abstractions)، برای مثال در سناریو فوق، کلاس Person، می توانید از آنها در پروژه های دیگر استفاده مجدد کنید. با این حال، در ۱۵ سال از تجربه حرفه ای خودم، به سختی دیده ام که این گونه انتزاعات در پروژه ها به شیوه ای مفید مورد استفاده قرار گیرد.
در مقطعی، شروع به ایجاد یک کتابخانه از این انتزاعات کردم که بتوانم در پروژه ها به اشتراک بگذارم. پس از مدتی، این کتابخانه، پر از کلاس ها و انتزاعات نامرتبط شد. به علاوه ورژن بندی و استفاده از آن در پروژه های متفاوت، به کابوس شبانه تبدیل شد. اگر قبلاً این کار را کرده اید، ممکن است کمی مرا درک کنید.
نمونه ای افراطی از وراثت
هنگامی که به عنوان مشاور برای یک پروژه شروع نشده مشغول شدم، و در جلسه مقدماتی، معمار داده (data architect) های این پروژه، مرا با تعداد زیادی از نمودار کلاس UML (بیش از ۵۰ صفحه) متعجب کرد. هر کلاس از کلاس دیگری ارث می برد و سرانجام، همه آنها به کلاس منتهی شدند که “Thing” نامیده شده بود! بدون شوخی!
جالب اینجاست که حتی یک نمودار رفتاری (behavioral diagram) وجود نداشت که بیان کننده رفتار سیستم باشد. بنابراین، این سلسله مراتب عظیم وراثت فقط برای وراثت “صفات” بود و نه رفتار (مثلا توابع)، در صورتی که دقیقا کاربرد وراثت، برای به ارث بردن رفتار است.
حدس بزنید سرانجام پروژه چه شد؟ پس از یک سال و پرداخت بیش از نیم میلیون دلار برای آن پروژه، شرکت تصمیم به لغو پروژه گرفت. هیچ چیز تولید نشده بود، حتی یک نسخه بتا! مدیران شرکت، مشاوران و تیم های مختلفی را درگیر می کردند و هیچکس نمی توانست برنامه ای با چنین مدل پیچیده و وسیعی را پیاده سازی کند.
آیا این داستان برای شما آشنا به نظر می رسد؟
کل جهان هستی را مدل سازی نکنید!
هنگام ساختن نرم افزار، شما باید domain model را بر اساس نیازهای برنامه و نه واقعیت برای آن طراحی کنید. در حقیقت domain model را باید بر اساس کارکرد ها و use case هایی که برنامه باید شامل شود، ایجاد کرد. در دنیای واقعی، یک Soccer Player یک Player و یک Player یک Person است. اما فقط به این دلیل که می توانید چنین رابطه ای را به زبان انگلیسی یا فارسی بیان کنید، نباید بین کلاسهای خود چنین ارتباطی برقرار کنید. زیرا در نهایت مدل خود را با پیچیدگی های غیر ضروری و بی هدف آلوده می کنید.
وراثت باعث افزایش جفت شدن (coupling) در طراحی شما می شود
حدس بزنید مشکل ما با وراثت چیست؟ خوب ، قبل از توضیح ، بگذارید موردی را روشن کنم. من مخالف ارث بری نیستم! وراثت، مانند هر چیز دیگری، کاربردهای خود را دارد. وقتی از آن در کاربرد مناسب استفاده می کنید، کار شما را راه می اندازد. اما اگر از آن به درستی استفاده نکنید، منجر به افزایش پیچیدگی در برنامه های شما می شود.
inheritance باعث افزایش وابستگی بین کلاس های شما می شود(tight coupling). اگر کلاس Child از کلاس Parent ارث بری کند، به Parent وابسته می شود. اگر در کلاس Parent تغییری ایجاد کنید، احتمالا باید کلاس Child را نیز تغییر دهید. یا حداقل ، باید مجدداً آن را کامپایل کرده و deploy کنید.
پس اگر یک سلسله مراتب کوچک با تعداد محدودی کلاس دارید، مشکلی نخواهید داشت. اما با افزایش سلسله مراتب، امکان تغییر در برنامه شما افزایش میابد. هرچه در سطح بالاتری از سلسله مراتب تغییرات ایجاد کنید، کلاس های بیشتری را نیز باید تغییر دهید یا حداقل مجدد کامپایل کنید.
چه موقع از inheritance استفاده کنیم
بنابراین ، چه زمانی باید از وراثت استفاده کنید؟ هنگام استفاده مجدد از رفتار (برای مثال متد ها) و override کردن آنها که منجر به چند ریختی (polymorphism) می شود. اما حتی پس از آن، شما می توانید از ترکیب (composition) برای دستیابی به همان موارد با وابستگی کمتر در طراحی خود استفاده کنید. برای مطالعه بیشتر به این لینک مراجعه کنید.
هنوز متوجه نشدید؟ در ادامه مقاله با من همراه باشید تا به شکلی ساده تر مفاهیم فوق را شرح دهم.
چه موقع از inheritance استفاده نکنیم
اجازه دهید با کمک کد، توصیه های فوق العاده ساده و عمل گرایانه ای را برای شما ارائه دهم. اگر یک یا چند مورد از علائم زیر را در کد خود دارید، احتمالاً به inheritance نیاز ندارید. می توانید سلسله مراتب خود را کاهش دهید، کاپلینگ (coupling : یعنی وابستگی) را کاهش داده و طراحی خود را ساده کنید.
وقتی کلاسهای توخالی دارید
آیا در طراحی خود کلاس هایی از این دست را دارید؟
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Player : Person
{
}
اگر با زبان سی شارپ آشنایی ندارید، در اینجا ما کلاس Person با دو فیلد Id و Name را داریم. کلاس Player از Person ارث بری می کند. کلاس Player در اینجا چیزی است که من آن را کلاس تو خالی می نامم. که بی هدف است.
ما به سادگی می توانستیم از Person استفاده کنیم و پیچیدگی را در طراحی خود کاهش دهیم. به مثال زیر توجه کنید:
public class Person
{
}
public class Player : Person
{
public int Id { get; set; }
public string Name { get; set; }
}
در این مثال، Person یک کلاس توخالی و بی فایده است.
اینجا قسمت مهمی است که می خواهم به آن توجه کنید: در دامنه (domain) این برنامه، Id و Name هر بازیکن مهم است. در حقیقت، اینکه یک Player در دنیای واقعی یک Person است، در این برنامه اهمیتی ندارد. منظور من این است که مدل سازی برنامه های شما نباید بر اساس دنیای واقعی باشد. در عوض ، شما باید بر اساس نیاز برنامه و چگونگی رفتار آن، مدل سازی کنید.
وقتی سلسله مراتب وراثت تماما راجع به صفات است
به این مثال نگاه کنید:
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Player : Person
{
public byte Number { get; set; }
}
public class Coach : Person
{
public byte YearsOfExperience { get; set;
}
استدلال پشت این طرح این است که ما در کلاس های Player و Coach دوباره از خصوصیات Id و Name استفاده می کنیم. قبلاً کلاسهای زیادی مثل این داشتم! مثال “نمونه ای افراطی از وراثت” که قبلاً به آن اشاره کردم دقیقاً مانند این مورد است.
استفاده مجدد از صفات یک روش و تفکر ضعیف در مورد ارث بری در شی گرایی است است. تلاش برای کپی کردن این صفات در کلاس های مشتق شده چقدر خسته کننده خواهد بود؟ copy/paste آنها فقط یک ثانیه طول می کشد!
شما ممکن است بگویید: “تکرار کد بد است”. بله ، اما نه همیشه! بد است اگر قصد تغییر آن را دارید و باید در مکانهای مختلف تغییر ایجاد کنید. اما چند بار این صفات را تغییر خواهید داد؟؟؟ اغلب نه یا هرگز! منطق (logic) چطور؟ منطق ، الگوریتم و رفتار (برای مثال متد ها) اغلب تغییر می کند.
پس بر مبنای دلایل فوق، شما باید، فقط برای استفاده مجدد از رفتار ها از ارث بری در شی گرایی استفاده کنید نه برای استفاده مجدد از صفات.
با اینحال، در اکثر موارد استفاده از composition نسبت به inheritance بهتر خواهد بود.
اجازه دهید جمع بندی کنیم
بنابراین ، به یاد داشته باشید ، ارث بری باعث افزایش اتصال و وابستگی بین کلاس های شما می شود. آن را برای موقعیت هایی استفاده کنید که می خواهید چند ریختی را پیاده سازی کنید، نه فقط برای استفاده مجدد از کد، به ویژه صفات. اگر در طراحی خود یک یا چند مورد از علائم زیر را دارید، شما احتمالاً به وراثت نیاز ندارید:
- کلاس های توخالی که هیچ عضوی ندارند.
- کلاس های پایه که فقط صفات را شامل می شوند. آنها هیچ متدی را برای استفاده مجدد در derived کلاس ها و از همه مهمتر، override کردن ندارند.
می توانید سلسله مراتب خود را کاهش دهید، کاپلینگ (coupling : یعنی وابستگی) را کاهش داده و طراحی خود را ساده کنید.
پس، همواره آن را ساده نگه دارید!
منبع : http://bit.ly/338kP1v