اصل هفتم: Liskove Substitution Principle
"ارث بری باید به صورتی باشد که زیر نوع را بتوان بجای ابر نوع استفاده کرد"
این اصل میگوید اگر قرار است
از ارث بری استفاده شود، نحوهی استفاده باید بدین گونه باشد که اگر یک شیء از کلاس
والد ( Base-Parent-Super type ) داشته باشیم، باید بتوان آن را
با شیء کلاس فرزند ( Sub
Type-Child ) بدون
هیچ گونه تغییری در منطق کد استفاده کننده از شیء مورد نظر، تغییر داد. به زبان
ساده باید بتوان شیء فرزند را جایگزین شیء والد کرد.
نکته مهم: این اصل در مورد عکس این رابطه صحبتی نمیکند و دلیل آن هم منطق طراحی میباشد. تصور کنید که شیء ای داشته باشید که از یک کلاس والد، ارث برده باشد. نوشتن کدی که شیء والد را بتوان جایگزین شیء فرزند کرد، بسیار سخت است؛ چرا که منطق متکی بر کلاس فرزند بسیار وابسته به جزییات کلاس فرزند است. در غیر این صورت وجود شیء فرزند، کم اهمیت میباشد.
با رعایت این اصل، میتوانیم در مواقعی که شروط مرتبط با کلاس فرزند را نداریم و یک سری منطق و قیود کلی مرتبط با کلاس والد را داریم، از شیء کلاس والد استفاده نماییم و وظیفه نمونه گیری (instantiation ) آن را به یک کلاس دیگر محول کنیم. به مثال زیر توجه کنید:
public class Parent { public string Name { get; set; } public int X { get; set; } public int Y { get; set; } public Parent() { X = Y = 0; } public virtual void Move() { X += 5; Y += 5; } public void Shoot() { } public virtual void Pass() { } } public class Child1 : Parent { public override void Move() { X += 10; Y += 10; } } public class Child2 : Parent { public override void Move() { X += 20; Y += 20; } } public enum State { Start, Move, Shoot, Pass } public class Creator { public static Parent GetInstance(bool? condition) { if (condition == null) { return new Parent(); } if (condition == true) { return new Child1(); } else { return new Child2(); } } } public class Context { public void SetState(ref State s) { s = State.Move; } public void Main() { State state =State.Start; // در مورد نوع این شیء چیزی نمیدانیم و وابسته به شرایط نوع آن متغیر است // در حقیقت شیء کلاس فرزند را جای شیء کلاس والد قرار میدهیم و نه بالعکس Parent obj = Creator.GetInstance(null); // منطق برنامه وضعیت را تغییر میدهد SetState(ref state); // قواعد کلی و عمومی که بدون در نظر گرفتن کلاس (نوع) شیء بر آن اعمال میشود switch (state) { case State.Move: obj.Move(); break; case State.Shoot: obj.Shoot(); break; case State.Pass: obj.Pass(); break; default: break; } } }
همانطور که در کدها نیز توضیح دادهام، کلاسهای فرزند را جایگزین کلاس والد کردهایم. اگر میخواستیم عکس رابطه را (شیء والد را به شیء فرزند انتقال دهیم) اعمال کنیم باید تغییر زیر را ایجاد میکردیم که با خطا روبرو خواهد شد:
Child1 obj = Creator.GetInstance(null);
اصل هشتم: Interface segregation
"واسطهای کوچک بهتر از واسطهای حجیم است"
این اصل به ما میگوید در تعریف واسطهای متعدد خساست به خرج ندهیم و بجای آنکه یک واسط اصلی با وظیفههای بسیار داشته باشیم، بهتر است واسطهای متعددی با وظیفههای کمتر داشته باشیم. برای درک این اصل ساده به عقب برمیگردیم، جایی که نیاز به واسط را توضیح دادیم. واسط، نقش تعریف پروتکل را دارد. اگر قرار باشد واسطی بزرگ با چندین مسئولیت داشته باشیم، آنگاه تعریف مستحکمی را از وظیفهی واسط ارائه ندادهایم. لذا هر کلاس پیاده ساز این واسط، برخی وظیفههایی را که نیاز به آن ندارد، باید تعریف و پیاده سازی کند. به مثال زیر نگاه کنید:
public interface IHuman { void Move(); void Eat(); void LevelUp(); void FireBullet(); } public class Player : IHuman { public void Eat() { } public void FireBullet() { } public void LevelUp() { } public void Move() { } } public class Enemy : IHuman { public void Eat() { } public void FireBullet() { } public void LevelUp() { } public void Move() { } } public class Citizen : IHuman { public void Eat() { } public void FireBullet() { } public void LevelUp() { } public void Move() { } }
در این مثال که مربوط به مدل یک بازی با نقشهای بازیکن، دشمن و شهروند (بی گناه!) است، طراحی به گونهای است که دشمن و شهروند، توابعی را که نیاز ندارند، باید پیاده سازی کنند. در دشمن: Eat(), LevelUp() و در شهروند: Eat(), LevelUp(), FireBullet() . لذا واسط IHuman یک واسط کلی با وظیفههای متعدد است.
در مدل بهبود یافته که کلاسها با پسوند Better بازنویسی شدهاند داریم:
public interface IMovable { void Move(); } public interface IEatable { void Eat(); } public interface IPlayer { void LevelUp(); } public interface IShooter { void FireBullet(); } public class PlayerBetter : IPlayer, IMovable, IEatable, IShooter { public void Eat() { } public void FireBullet() { } public void LevelUp() { } public void Move() { } } public class EnemyBetter : IMovable, IShooter { public void FireBullet() { } public void Move() { } } public class CitizenBetter : IMovable { public void Move() { } }
در اینجا برای هر وظیفه یک واسط تعریف کرده ایم که باعث قوی شدن معنای هر واسط میشود.
اصل نهم: Dependency inversion
"وابستگی بین ماژولها را به وابستگی آنها به انتزاع (واسط) تغییر بده"
این اصل که نمود آن را در الگوهای طراحی dependency injection و factory میبینیم، میگوید که ماژولهای بالادست (ماژول استفاده کننده ماژول پایین دست) به جای آنکه ارجاع مستقیمی را به ماژولهای پایین دست داشته باشند، به انتزاعی (واسط) ارجاع بدهند که ماژول پایین دست آنرا پیاده سازی میکند یا به ارث میبرد. در واقع این اصل برای از بین بردن وابستگی قوی بین ماژولهای بالا دست و پایین دست، به میدان آمده است. دو حکم اصلی از این اصل بر میآید:
الف – ماژولهای بالا دست نباید وابسته به ماژولهای پایین دست باشند. هر دو باید وابسته به انتزاع (واسط) باشند. وابستگی ماژول بالا دست از نوع ارجاع و وابستگی ماژول پایین دست از نوع ارث بری است.
ب – انتزاع نباید وابسته به جزییات باشد، بلکه جزییات باید وابسته به انتزاع باشد. یعنی در پیاده سازی منطق برنامه (که جزییات محسوب میشود) باید از واسطها یا کلاسهای انتزاعی استفاده کنیم و همچنین در نوشتن کلاسهای انتزاعی نباید هیچ گونه ارجاعی را به کلاسهای جزیی داشته باشیم.
در مثال زیر با نمونهای از طراحی ناقض این اصل روبرو هستیم:
public class Controller { public Service Service { get; set; } public Controller() { // کنترلر باید نحوه نمونه گیری را بداند (ورودیهای لازم) و این از وظایف آن خارج است Service = new Service(1); } public void DoWork() { Service.RunService(); } } public class Service { public int State { get; set; } public Service(int s) { State = s; } public void RunService() { } }
در این مثال کلاس کنترلر، ماژول بالادست و کلاس سرویس، ماژول پایین دست محسوب میگردد. در ادامه طراحی مطلوب را نیز ارائه دادهام:
public class ControllerBetter { // ارجاع به واسط باعث انعطاف و کاهش وابستگی شده است public IService Service { get; set; } public ControllerBetter(IService service) { // یک کلاس دیگر وظیفه ارسال سرویس به سازنده کلاس کنترلر را دارد // و مسئولیت نمونه گیری را از دوش کنترلر برداشته است Service = service; } public void DoWork() { Service.RunService(); } } // کاهش وابستگی با تعریف واسط و تغییر وابستگی مستقیم بین کنترلر و سرویس public interface IService { void RunService(); } // وابستگی جزییات به انتزاع public class ServiceBetter : IService { public int State { get; set; } public ServiceBetter(int s) { State = s; } public void RunService() { } }
نحوه بهبود طراحی را در توضیحات داخل کد مشاهده میکنید. در مقاله بعدی به اصول GRASP خواهم پرداخت.