سؤال

لقد سمعت أن Liskov إحلال مبدأ (LSP) هو مبدأ أساسي من وجوه المنحى والتصميم.ما هو وما هي بعض الأمثلة على استخدامه ؟

هل كانت مفيدة؟

المحلول 2

على Liskov إحلال مبدأ (LSP ، ) هو مفهوم في وجوه المنحى البرمجة التي تنص على:

الوظائف التي تستخدم مؤشرات أو إشارات إلى فئات أساسية يجب أن تكون قادرة على استخدام الكائنات من الفئات المشتقة دون أن يعرفوا ذلك.

في قلبها LSP عن واجهات والعقود وكذلك كيفية تحديد متى تمتد الطبقة مقابلاستخدام استراتيجية أخرى مثل تكوين لتحقيق الهدف الخاص بك.

الطريقة الأكثر فعالية رأيت لتوضيح هذه النقطة كان في أول رئيس OOA&D.يقدم سيناريو حيث كنت مطور على مشروع لبناء إطار الألعاب الاستراتيجية.

أن الفئة التي تمثل المجلس الذي يبدو مثل هذا:

Class Diagram

كل الطرق تأخذ X و Y إحداثيات كما المعلمات لتحديد البلاط الموقف في صفيف ثنائي الأبعاد من Tiles.وسوف تسمح هذه اللعبة المطور لإدارة الوحدات في المجلس أثناء سير المباراة.

الكتاب يذهب إلى تغيير متطلبات أن أقول أن اللعبة إطار العمل يجب أيضا دعم 3D لعبة لوحات لاستيعاب ألعاب الطيران.لذلك ThreeDBoard الفصل هو عرض يمتد Board.

للوهلة الأولى يبدو هذا قرار جيد. Board يوفر كل من Height و Width خصائص ThreeDBoard يوفر المحور Z.

حيث ينهار عندما ننظر إلى جميع الأعضاء الآخرين الموروثة من Board.أساليب AddUnit, GetTile, GetUnits على اتخاذ كل من X و Y المعلمات في Board الدرجة ولكن ThreeDBoard يحتاج Z المعلمة كذلك.

لذا يجب تطبيق هذه الأساليب مرة أخرى مع Z المعلمة.المعلمة Z لا يوجد لديه السياق إلى Board فئة الأساليب الموروثة من Board الطبقة تفقد معناها.وحدة من رمز محاولة استخدام ThreeDBoard فصل قاعدة الطبقة Board سيكون جدا من الحظ.

ربما يجب أن تجد نهج آخر.بدلا من توسيع Board, ThreeDBoard ينبغي أن تتألف من Board الكائنات.واحد Board الكائن في وحدة المحور Z.

هذا يتيح لنا استخدام جيد وجوه المنحى مبادئ مثل التغليف وإعادة استخدامها و لا تنتهك LSP.

نصائح أخرى

مثال يوضح LSP (قدمها العم بوب في بودكاست سمعت مؤخرا) كان كيف في بعض الأحيان شيء يبدو الحق في اللغة الطبيعية لا يعمل تماما في التعليمات البرمجية.

في الرياضيات ، Square هو Rectangle.بل هو التخصص مستطيل.و "هو" يجعلك تريد أن نموذج هذا الميراث.ومع ذلك إذا كان في كود قمت بها Square تستمد من Rectangle, ثم Square يجب أن تكون قابلة للاستخدام في أي مكان تتوقع Rectangle.وهذا يجعل بعض السلوك الغريب.

تخيل أنك قد SetWidth و SetHeight طرق على الخاص Rectangle قاعدة الطبقة ؛ هذا يبدو منطقيا تماما.ومع ذلك إذا كان الخاص بك Rectangle إشارة إلى Square, ثم SetWidth و SetHeight لا معنى لأن وضع واحد من شأنه أن يغير أخرى إلى المباراة.في هذه الحالة Square فشل Liskov استبدال اختبار مع Rectangle و التجريد من وجود Square ترث من Rectangle سيئة واحدة.

enter image description here

جميعكم يجب أن تحقق أخرى لا تقدر بثمن خالص مبادئ ملصقات تحفيزية.

LSP المخاوف الثوابت.

المثال الكلاسيكي هو التالي الزائفة رمز الإعلان (تطبيقات محذوفة):

class Rectangle {
    int getHeight()
    void setHeight(int value)
    int getWidth()
    void setWidth(int value)
}

class Square : Rectangle { }

الآن لدينا مشكلة على الرغم من أن واجهة مباريات.والسبب في ذلك هو أننا قد انتهكت الثوابت النابعة من الرياضية تعريف المربعات والمستطيلات.طريقة حاصل على واضعي العمل ، Rectangle يجب أن تستوفي التالي ثابتة:

void invariant(Rectangle r) {
    r.setHeight(200)
    r.setWidth(100)
    assert(r.getHeight() == 200 and r.getWidth() == 100)
}

ومع ذلك ، فإن هذه ثابتة يجب أن تكون قد انتهكت من قبل التنفيذ الصحيح Square, ولذلك فإنه ليس من صالح بديلا من Rectangle.

إحلال مبدأ في وجوه المنحى البرمجة تفيد في برنامج الكمبيوتر ، إذا ق هو نوع فرعي من T ، ثم كائنات من نوع T يمكن استبدال مع كائنات من نوع S

دعونا نفعل مثال بسيط في جاوة:

مثالا سيئا

public class Bird{
    public void fly(){}
}
public class Duck extends Bird{}

البط يمكن أن تطير بسبب ذلك هو طائر, لكن ماذا عن هذا:

public class Ostrich extends Bird{}

النعام طائر لكنه لا يطير النعامة الدرجة هو نوع فرعي من فئة الطيور لكنه لا يستطيع استخدام يطير الأسلوب ، وهذا يعني أننا كسر LSP المبدأ.

خير مثال

public class Bird{
}
public class FlyingBirds extends Bird{
    public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{} 

روبرت مارتن ممتازة ورقة عن Liskov مبدأ الإحلال.ويناقش خفية وغير خفية طرق مبدأ قد انتهكت.

بعض الأجزاء ذات الصلة من الورق (لاحظ أن المثال الثاني بشكل كبير المكثف):

مثال بسيط من انتهاك LSP

واحدة من أكثر انتهاكات صارخة من هذا المبدأ هو استخدام C++ وقت تشغيل نوع المعلومات (RTTI) لتحديد وظيفة استنادا إلى نوع من كائن.أي:

void DrawShape(const Shape& s)
{
  if (typeid(s) == typeid(Square))
    DrawSquare(static_cast<Square&>(s)); 
  else if (typeid(s) == typeid(Circle))
    DrawCircle(static_cast<Circle&>(s));
}

بوضوح DrawShape وظيفة سيئة تشكيلها.يجب أن تعرف عن كل مشتق من Shape الفئة و يجب تغييره كلما مشتقات جديدة من Shape يتم إنشاؤها.والواقع أن العديد من وجهة نظر هيكل هذا بمثابة لعنة على وجوه المنحى والتصميم.

مربع و مستطيل ، أكثر دهاء انتهاك.

ومع ذلك ، هناك أخرى أكثر دهاء ، طرق انتهاك LSP.النظر في التطبيق الذي يستخدم Rectangle الطبقة كما هو موضح أدناه:

class Rectangle
{
  public:
    void SetWidth(double w) {itsWidth=w;}
    void SetHeight(double h) {itsHeight=w;}
    double GetHeight() const {return itsHeight;}
    double GetWidth() const {return itsWidth;}
  private:
    double itsWidth;
    double itsHeight;
};

[...] تخيل أن يوم واحد المستخدمين الطلب القدرة على التعامل مع الساحات بالإضافة إلى مستطيلات.[...]

ومن الواضح مربع هو مستطيل لجميع العادي المقاصد والأغراض.لأن عيسى علاقة يحمل ، فمن المنطقي أن نموذج Square الطبقة بأنها مشتقة من Rectangle. [...]

Square سوف ترث SetWidth و SetHeight المهام.هذه وظائف هي تماما غير مناسب Square, منذ العرض ، ارتفاع مربع متطابقة.هذا ينبغي أن يكون كبيرا فكرة أن هناك مشكلة مع التصميم.ومع ذلك ، هناك طريقة تجنب المشكلة.يمكننا تجاوز SetWidth و SetHeight [...]

ولكن النظر في الدالة التالية:

void f(Rectangle& r)
{
  r.SetWidth(32); // calls Rectangle::SetWidth
}

إذا أردنا تمرير إشارة إلى Square كائن في هذه الوظيفة ، Square كائن وسوف يكون معطوبا بسبب ارتفاع لن يتغير.هذا هو انتهاك واضح LSP.وظيفة لا تعمل المشتقات من الحجج.

[...]

LSP ضروري حيث بعض التعليمات البرمجية يعتقد أنه هو استدعاء الأساليب من نوع T, و قد تدري استدعاء الأساليب من نوع S, حيث S extends T (أي S يرث ، مستمد من ، أو هو نوع فرعي من supertype T).

على سبيل المثال, هذا يحدث فيها وظيفة مع معلمة الإدخال من نوع T, يسمى (أيالاحتجاج) مع حجة قيمة من نوع S.أو معرف من نوع T, هو تعيين قيمة من نوع S.

val id : T = new S() // id thinks it's a T, but is a S

LSP يتطلب التوقعات (أيالثوابت) عن أساليب نوع T (مثلا ، Rectangle) لا تكون انتهكت عندما طرق من نوع S (مثلا ، Square) وتسمى بدلا من ذلك.

val rect : Rectangle = new Square(5) // thinks it's a Rectangle, but is a Square
val rect2 : Rectangle = rect.setWidth(10) // height is 10, LSP violation

حتى نوع مع ثابتة المجالات لا يزال لديه الثوابت مثلاعلى غير قابل للتغيير المستطيل واضعي نتوقع الأبعاد بصورة مستقلة تعديلها لكن غير قابل للتغيير مربع واضعي يخالف هذا التوقع.

class Rectangle( val width : Int, val height : Int )
{
   def setWidth( w : Int ) = new Rectangle(w, height)
   def setHeight( h : Int ) = new Rectangle(width, h)
}

class Square( val side : Int ) extends Rectangle(side, side)
{
   override def setWidth( s : Int ) = new Square(s)
   override def setHeight( s : Int ) = new Square(s)
}

LSP يتطلب أن كل طريقة من النوع S يجب أن يكون contravariant معلمة الإدخال(ق) و التغاير الناتج.

Contravariant يعني الفرق هو عكس اتجاه الميراث ، أينوع Si, من كل معلمة الإدخال كل طريقة من النوع S, يجب أن يكون نفس أو supertype من نوع Ti المقابلة معلمة الإدخال من أسلوب المقابلة supertype T.

التغاير يعني الفرق هو في نفس الاتجاه من الميراث ، أينوع So, من إخراج كل طريقة من النوع S, يجب أن يكون نفس أو النوع الفرعي من نوع To من الناتج المقابلة من أسلوب المقابلة supertype T.

هذا هو لأنه إذا كان الطالب يعتقد أنه لديه نوع T, يعتقد أنه يتم استدعاء أسلوب T, ثم تزود الحجة(ق) من نوع Ti ويعين الإخراج إلى نوع To.عندما هو في الواقع استدعاء أسلوب المقابلة S, ثم كل Ti المدخلات الوسيطة يتم تعيين Si معلمة الإدخال ، So الانتاج يتم تعيين نوع To.وبالتالي إذا Si لا contravariant ث.r.t.إلى Ti, ثم فرعي Xi—الذي لن يكون فرعي من Si—يمكن تعيين Ti.

بالإضافة إلى اللغات (مثلا ، سكالا أو سيلان) التي لها تعريف الموقع الفرق الشروح على نوع تعدد الأشكال المعلمات (أيالوراثة) ، المشارك أو كونترا - اتجاه الفرق الشرح لكل نوع المعلمة من نوع T يجب أن يكون المعاكس أو نفس الاتجاه على التوالي إلى كل معلمة الإدخال أو الإخراج (من كل طريقة T) يحتوي على نوع من نوع المعلمة.

بالإضافة إلى ذلك, لكل معلمة الإدخال أو الإخراج الذي لديه وظيفة نوع الفرق الاتجاه المطلوب هو عكس ذلك.هذه القاعدة يتم تطبيقها بشكل متكرر.


Subtyping المناسب حيث الثوابت يمكن حصرها.

هناك الكثير من الأبحاث الجارية على كيفية نموذج الثوابت ، بحيث تنفذ من قبل المترجم.

Typestate (انظر صفحة 3) تعلن ويفرض الدولة الثوابت متعامد نوع.بدلا من ذلك, الثوابت يمكن إنفاذها تحويل التأكيدات إلى أنواع.على سبيل المثال ، إلى التأكيد على أن ملف مفتوح قبل إغلاقه, ثم الملف.فتح() يمكن العودة إلى OpenFile النوع الذي يحتوي على close() طريقة غير متوفرة في الملف.A تيك تاك تو API يمكن مثال آخر على استخدام الكتابة لفرض الثوابت في وقت التحويل البرمجي.نوع نظام مايو حتى يكون تورينغ-كاملة مثلا سكالا.بالاعتماد كتبته اللغات نظرية provers إضفاء الطابع الرسمي على النماذج العليا في الكتابة.

بسبب الحاجة إلى دلالات مجردة على امتداد, أتوقع أن توظيف الكتابة إلى نموذج الثوابت ، أيموحدة العليا denotational دلالات متفوقة على Typestate.'التمديد' يعني غير محدود ، متبدل الترتيب permuted تكوين غير منسقة ، وحدات التنمية.لأنه يبدو لي أن يكون نقيض التوحيد وبالتالي درجة من الحرية أن يكون كل منهما تعتمد على النماذج (مثلأنواع Typestate) للتعبير عن المشتركة الدلالات التي لا يمكن أن تكون موحدة مع بعضها البعض من أجل الموسعة التكوين.على سبيل المثال ، التعبير المشكلةمثل تمديد كانت موحدة في subtyping, وظيفة الحمولة الزائدة ، وحدودي الكتابة المجالات.

بلدي الموقف النظري هو أن المعرفة موجودة (انظر القسم "المركزية أعمى وغير صالحة") ، سيكون هناك أبدا يكون النموذج العام الذي فرض تغطية 100 ٪ من الثوابت في تورينج-كاملة لغة الكمبيوتر.المعرفة في الوجود ، غير متوقع الكثير من الاحتمالات موجودة ، أياضطراب الكون يجب أن يكون دائما في ازدياد.هذا هو التدهور الحتمي القوة.لإثبات كل ما يمكن حساب إمكانية التمديد ، لحساب بداهة كل إمكانية التمديد.

هذا هو السبب في وقف نظرية موجود ، أيفمن غير مقرر ما إذا كان كل برنامج ممكن في تورينج-كاملة لغة البرمجة تنتهي.ثبت أن بعض برنامج محدد ينهي (واحد جميع الاحتمالات وتم تحديد حسابها).ولكن من المستحيل أن يثبت أن كل ما يمكن تمديد هذا البرنامج تنتهي إلا إذا إمكانيات تمديد هذا البرنامج ليس تورينج كاملة (مثلا ، عبر تعتمد الكتابة).منذ مطلب أساسي تورينج-اكتمال غير محدود العودية, ، فمن البديهي أن نفهم كيف Gödel هو عدم اكتمال النظريات رسل مفارقة تنطبق على التمديد.

تفسير هذه النظريات يتضمن في تعميم مفهوم التدهور الحتمي القوة:

  • Gödel هو عدم اكتمال النظريات:أي رسمية النظرية في جميع الحسابية الحقائق التي يمكن اثباتها, غير متناسقة.
  • راسل مفارقة:كل قاعدة عضوية مجموعة يمكن أن تحتوي على مجموعة ، إما تعداد نوع معين من كل عضو أو يحتوي على نفسها.وهكذا مجموعات إما لا يمكن تمديد أو أنهم غير محدود العودية.على سبيل المثال, مجموعة من كل ما هو غير إبريق الشاي ، يتضمن في حد ذاته ، والذي يتضمن في حد ذاته ، والذي يتضمن في حد ذاته ، الخ....وهكذا حكم يتعارض إذا كان (قد تحتوي على مجموعة و) لا تعداد أنواع محددة (أييسمح كل غير محدد أنواع) و لا تسمح غير محدود التمديد.هذا هو مجموعة من مجموعات غير الأعضاء أنفسهم.هذا العجز أن تكون متسقة تماما المذكورة على كل ممكن التمديد ، Gödel هو عدم اكتمال النظريات.
  • Liskov Substition مبدأ:عموما هو غير مقرر المشكلة إذا كان أي مجموعة فرعية أخرى ، أيالميراث هو عموما غير مقرر.
  • لنسكي الرجوع:فمن غير مقرر ما في الحساب من شيء هو عندما يتم وصفها أو المتصورة ، أيتصور (واقع) لا مطلق نقطة مرجعية.
  • كوس نظرية:لا توجد النقطة المرجعية الخارجية ، وبالتالي أي حاجز غير محدود الخارجية الاحتمالات سوف تفشل.
  • القانون الثاني للديناميكا الحرارية:الكون كله (نظام مغلق ، أيكل شيء) الاتجاهات إلى أقصى اضطراب ، أيأقصى المستقلة الاحتمالات.

على LSP قاعدة العقد من clases:إذا قاعدة الطبقة يرضي العقد ، ثم LSP الفئات المشتقة يجب أيضا تلبية هذا العقد.

في شبه الثعبان

class Base:
   def Foo(self, arg): 
       # *... do stuff*

class Derived(Base):
   def Foo(self, arg):
       # *... do stuff*

يرضي LSP إذا في كل مرة كنت اتصل فو على اشتقاق كائن ، فإنه يعطي نفس النتائج بالضبط كما يدعو فو على قاعدة كائن طالما arg هو نفسه.

الوظائف التي تستخدم مؤشرات أو إشارات إلى فئات أساسية يجب أن تكون قادرة على استخدام الكائنات من الفئات المشتقة من دون أن يعرفوا ذلك.

عندما قرأت لأول مرة عن LSP, أنا افترض أن هذا كان من المفترض ضيقا جدا ، أساسا المساواة إلى واجهة تنفيذ ونوع آمنة الصب.وهو ما يعني أن LSP إما ضمان أم لا من خلال اللغة نفسها.على سبيل المثال ، في هذا صارمة المعنى ، ThreeDBoard هو بالتأكيد تصلح بديلا المجلس ، بقدر ما هو مترجم المعنية.

بعد القراءة أكثر على مفهوم على الرغم من أنني وجدت أن LSP عموما فسر على نطاق أوسع من ذلك.

باختصار, ماذا يعني رمز العميل إلى "تعلم" أن الهدف من وراء المؤشر من نوع مشتق بدلا من نوع مؤشر لا يقتصر على نوع-السلامة.الالتزام LSP هي أيضا قابلة للاختبار من خلال التحقيق الكائنات السلوك الفعلي.هذا هو دراسة تأثير حالة الكائن وطريقة الحجج على نتائج طريقة المكالمات ، أو أنواع استثناءات من وجوه.

وبالعودة إلى المثال مرة أخرى ، في نظرية المجلس طرق يمكن أن تعمل على ما يرام على ThreeDBoard.في الممارسة ومع ذلك ، سيكون من الصعب جدا لمنع الاختلافات في سلوك العميل قد لا يعالج بشكل صحيح ، دون يمشي الوظيفة التي ThreeDBoard يهدف إلى إضافة.

مع هذه المعرفة في متناول اليد ، تقييم LSP الانضمام يمكن أن يكون أداة عظيمة في تحديد متى التكوين هو أنسب آلية توسيع القائمة وظائف بدلا من الميراث.

هناك قائمة الاختيار لتحديد ما إذا كان أو لا أنت تنتهك Liskov.

  • إذا كنت تنتهك واحدة من البنود التالية -> كنت تنتهك Liskov.
  • إذا كنت لا تنتهك أي -> غير قادر على إبرام أي شيء.

قائمة الاختيار:

  • لا استثناءات جديدة ينبغي أن يلقى في فئة مشتقة:إذا كان لديك قاعدة الطبقة رمى ArgumentNullException ثم فئات فرعية فقط يسمح لك لرمي استثناءات من نوع ArgumentNullException أو أي استثناءات المستمدة من ArgumentNullException.رمي IndexOutOfRangeException هو انتهاك Liskov.
  • شروط مسبقة لا يمكن تعزيز:نفترض قاعدة الخاص بك فئة تعمل مع عضو int.الآن الفرعية الخاصة بك نوع يتطلب من الباحث أن تكون إيجابية.هذا هو تعزيز شروط مسبقة ، والآن أي رمز التي عملت على ما يرام تماما من قبل مع السلبية رجات مكسورة.
  • بعد الظروف لا يمكن أن يكون ضعف:نفترض قاعدة الخاص بك الدرجة المطلوبة كافة الاتصالات إلى قاعدة البيانات يجب أن تكون مغلقة قبل أن الطريقة التي تم إرجاعها.في الدرجة الفرعية أنت تجاهلت هذه الطريقة و الأوراق اتصال مفتوحة لمزيد من إعادة استخدامها.كنت قد ضعفت بعد شروط هذا الأسلوب.
  • الثوابت يجب الحفاظ عليها:الأكثر صعبة ومؤلمة القيد الوفاء.الثوابت بعض الوقت مخبأة في قاعدة الطبقة الطريقة الوحيدة التي تكشف لهم هو قراءة رمز الفئة الأساسية.أساسا لديك للتأكد من عند تجاوز أسلوب شيء ثابت يجب أن تبقى على حالها بعد تجاوز طريقة تنفيذها.أفضل شيء يمكنني أن أفكر في أن تنفذ هذه ثابتة القيود في الفئة الأساسية ولكن هذا لن يكون سهلا.
  • تاريخ القيد:عندما تجاوز الأسلوب لا يسمح لك تعديل الامم المتحدة للتعديل الملكية في الفئة الأساسية.نلقي نظرة على هذه المدونة يمكنك أن ترى اسم ويعرف أن تكون الأمم المتحدة للتعديل (مجموعة خاصة) ولكن النوع يقدم أسلوب جديد يسمح بتعديل ذلك (من خلال التفكير):

    public class SuperType
    {
        public string Name { get; private set; }
        public SuperType(string name, int age)
        {
            Name = name;
            Age = age;
        }
    }
    public class SubType : SuperType
    {
        public void ChangeName(string newName)
        {
            var propertyType = base.GetType().GetProperty("Name").SetValue(this, newName);
        }
    }
    

هناك 2 آخرين البنود: Contravariance من طريقة الحجج و التغاير من أنواع عودة.ولكن ليس من الممكن في C# (أنا C# المطور) لذلك أنا لا أهتم بهم.

المرجع:

مثال مهم على استخدام من LSP في اختبار البرمجيات.

إذا كان لدي الدرجة التي LSP المتوافقة مع فئة فرعية من ب ، ثم يمكنني استخدام اختبار جناح ب لاختبار A.

لاختبار تماما فرعية ، ربما تحتاج إلى إضافة عدد قليل من أكثر حالات الاختبار ، لكن في الدنيا لا يمكن إعادة استخدام كل الفائقة B حالات الاختبار.

وسيلة لتحقيق هذا عن طريق بناء ما ماكغريغور يدعو "موازية الهرمي لاختبار":بلدي ATest الفئة سوف ترث من BTest.بعض شكل حقن ثم هناك حاجة إلى ضمان اختبار الحالة يعمل مع كائنات من نوع A بدلا من نوع B (قالب بسيط نمط طريقة سوف تفعل).

علما بأن إعادة استخدام سوبر-اختبار جناح لجميع تطبيقات فرعية هو في الواقع وسيلة لاختبار هذه الفئة الفرعية تطبيقات LSP المتوافقة.وبالتالي ، يمكن القول أيضا أن واحد يجب أن تشغيل الفائقة اختبار جناح في سياق أي فئة فرعية.

انظر أيضا الإجابة على ستاكوفيرفلوو السؤال "يمكنني تنفيذ سلسلة من القابل لإعادة الاستخدام الاختبارات اختبار الواجهة التنفيذ ؟ "

أعتقد أن كل نوع من تغطية ما LSP من الناحية الفنية:كنت في الأساس تريد أن تكون قادرة على مجردة من النوع تفاصيل استخدام supertypes بأمان.

حتى Liskov 3 القواعد الأساسية:

  1. التوقيع القاعدة :يجب أن يكون هناك صالحة تنفيذ كل عملية من supertype في النوع الفرعي نحويا.شيء مترجم سوف تكون قادرة على التحقق بالنسبة لك.هناك القليل من القاعدة عن رمي أقل الاستثناءات ويجري على الأقل موجودا كما في supertype الأساليب.

  2. أساليب القاعدة:تنفيذ تلك العمليات لغويا الصوت.

    • أضعف شروط مسبقة :النوع الفرعي الوظائف يجب أن تأخذ على الأقل ما supertype أخذت كمدخل ، إن لم يكن أكثر.
    • أقوى Postconditions:وينبغي أن تنتج مجموعة فرعية من إخراج supertype طرق إنتاجها.
  3. خصائص القاعدة :هذا يتجاوز الفردية وظيفة المكالمات.

    • الثوابت :الأشياء التي هي دائما صحيحا يجب أن يبقى صحيحا.على سبيل المثال.مجموعة حجم أبدا السلبية.
    • التطور خصائص :عادة ما يكون أن تفعل شيئا مع ثبات أو نوع من الدول يمكن أن يكون الكائن في.أو ربما كائن ينمو فقط و لم ينكمش لذلك النوع أساليب لا ينبغي أن تجعل من.

كل هذه الخصائص تحتاج إلى الحفاظ اضافية النوع وظيفة يجب أن لا تنتهك supertype خصائص.

إذا هذه ثلاثة أشياء تؤخذ من الرعاية ، لديك المستخرجة من الأشياء الأساسية و كتابة المتباعدة رمز.

المصدر:برنامج التنمية في جافا - باربرا Liskov

طويلة قصة قصيرة, دعونا نترك مستطيلات المستطيلات و المربعات المربعات ، مثال عملي عندما تمتد فئة الأم لديك إما الحفاظ على الدقيق الأم API أو تمديدها.

دعونا نقول لديك قاعدة ItemsRepository.

class ItemsRepository
{
    /**
    * @return int Returns number of deleted rows
    */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        return $numberOfDeletedRows;
    }
}

و فئة فرعية امتداده:

class BadlyExtendedItemsRepository extends ItemsRepository
{
    /**
     * @return void Was suppose to return an INT like parent, but did not, breaks LSP
     */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        // we broke the behaviour of the parent class
        return;
    }
}

ثم هل يمكن أن يكون العميل العمل مع قاعدة ItemsRepository API والاعتماد عليها.

/**
 * Class ItemsService is a client for public ItemsRepository "API" (the public delete method).
 *
 * Technically, I am able to pass into a constructor a sub-class of the ItemsRepository
 * but if the sub-class won't abide the base class API, the client will get broken.
 */
class ItemsService
{
    /**
     * @var ItemsRepository
     */
    private $itemsRepository;

    /**
     * @param ItemsRepository $itemsRepository
     */
    public function __construct(ItemsRepository $itemsRepository)
    {
        $this->itemsRepository = $itemsRepository;
    }

    /**
     * !!! Notice how this is suppose to return an int. My clients expect it based on the
     * ItemsRepository API in the constructor !!!
     *
     * @return int
     */
    public function delete()
    {
        return $this->itemsRepository->delete();
    }
} 

على LSP هو كسر عندما استبدال الأم فئة مع فئة فرعية يكسر API العقد.

class ItemsController
{
    /**
     * Valid delete action when using the base class.
     */
    public function validDeleteAction()
    {
        $itemsService = new ItemsService(new ItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is an INT :)
    }

    /**
     * Invalid delete action when using a subclass.
     */
    public function brokenDeleteAction()
    {
        $itemsService = new ItemsService(new BadlyExtendedItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is a NULL :(
    }
}

يمكنك معرفة المزيد عن الكتابة للصيانة برامج في الحال: https://www.udemy.com/enterprise-php/

هذه الصيغة من LSP هو وسيلة قوية جدا:

إذا كان لكل كائن o1 من نوع S هناك كائن o2 من نوع T هذه لجميع البرامج P المحددة في شروط T ، سلوك P هو دون تغيير عند o1 بديلا o2 ، ثم ق هو نوع فرعي من ت.

وهو ما يعني أساسا أن S هو آخر تماما مغلفة تنفيذ نفس الشيء بالضبط كما T.وأنا يمكن أن تكون جريئة و تقرر أن الأداء هو جزء من سلوك P...

لذا, في الأساس, أي استخدام أواخر ملزم ينتهك LSP.هذا هو بيت القصيد من OO إلى الحصول على سلوك مختلف عندما نستبدل كائن من نوع واحد من نوع آخر!

صياغة استشهد ويكيبيديا هو الأفضل منذ العقار يعتمد على السياق و لا تشمل بالضرورة كل سلوك البرنامج.

بعض الإضافة:
أتساءل لماذا لم أي شخص يكتب عن ثابتة , شروط مسبقة وبعد شروط الفئة الأساسية التي يجب أن يطاع من قبل الفئات المشتقة.عن فئة مشتقة د أن يكون تماما sustitutable القاعدة فئة B ، D فئة يجب أن تطيع شروط معينة:

  • في أنواع من الفئة الأساسية يجب الحفاظ عليها من قبل فئة مشتقة
  • شروط مسبقة من الفئة الأساسية لا يجب أن تكون معززة من قبل فئة مشتقة
  • بعد الظروف الفئة الأساسية يجب أن لا يكون ضعف من قبل فئة مشتقة.

حتى المشتقة يجب أن يكون على بينة من الشروط الثلاثة المذكورة أعلاه التي تفرضها الفئة الأساسية.ومن ثم فإن قواعد subtyping قبل البت فيها.مما يعني 'هو' العلاقة يجب أن يطاع إلا عند بعض القواعد يطاع من النوع الفرعي.هذه القواعد في شكل الثوابت ، precoditions و postcondition ينبغي قررت رسمي 'تصميم العقد'.

مزيد من المناقشات بشأن هذا متاح في بلدي بلوق: Liskov مبدأ الإحلال

في جملة بسيطة يمكننا أن نقول:

الطبقة الطفل يجب أن لا ينتهك قاعدة الطبقة الخصائص.يجب أن تكون قادرة على ذلك.يمكننا القول انها نفس subtyping.

أرى المستطيلات و المربعات في كل إجابة ، كيف تنتهك LSP.

أود أن تظهر كيف LSP يمكن أن تكون مطابقة مع العالم الحقيقي سبيل المثال :

<?php

interface Database 
{
    public function selectQuery(string $sql): array;
}

class SQLiteDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // sqlite specific code

        return $result;
    }
}

class MySQLDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // mysql specific code

        return $result; 
    }
}

هذا التصميم يتوافق مع LSP لأن السلوك لا تزال دون تغيير بغض النظر عن تنفيذ نختار للاستخدام.

و نعم يمكنك تنتهك LSP في هذا التكوين فعل واحد بسيط تغيير مثل ذلك :

<?php

interface Database 
{
    public function selectQuery(string $sql): array;
}

class SQLiteDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // sqlite specific code

        return $result;
    }
}

class MySQLDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // mysql specific code

        return ['result' => $result]; // This violates LSP !
    }
}

الآن فرعية لا يمكن استخدام نفس الطريقة لأنها لا تنتج نفس النتيجة بعد الآن.

Liskov هو إحلال مبدأ(LSP)

في كل وقت نقوم بتصميم برنامج وحدة نخلق بعض الصف التسلسلات الهرمية.ثم نمد بعض فصول خلق بعض المشتقة فصول.

يجب أن نتأكد أن الجديد في الفئات المشتقة فقط دون تمديد استبدال وظائف الطبقات القديمة.وإلا فإن فصول جديدة يمكن أن تنتج الآثار غير المرغوب فيها عندما يتم استخدامها في القائمة البرامج وحدات.

Liskov هو إحلال مبدأ تنص على أنه إذا كان برنامج وحدة باستخدام قاعدة الطبقة ، ثم الإشارة إلى الفئة الأساسية يمكن أن تكون استبدال فئة مشتقة دون التأثير على وظائف وحدة البرنامج.

على سبيل المثال:

أدناه هي المثال الكلاسيكي الذي Liskov الاستعاضة المبدأ انتهكت.في المثال 2 الطبقات المستخدمة:مستطيل مربع.دعونا نفترض أن المستطيل كائن يستخدم في مكان ما في التطبيق.ونحن توسيع نطاق تطبيق وإضافة مربع الدرجة.مربع الدرجة يتم إرجاعها بواسطة مصنع نمط استنادا إلى بعض الشروط و نحن لا نعرف بالضبط ما هو نوع الكائن سوف تعاد.ولكن نحن نعلم أنه مستطيل.نحصل على مستطيل كائن تعيين العرض إلى 5 و ارتفاع 10 والحصول على المنطقة.عن مستطيل مع العرض 5 الطول 10, يجب أن تكون منطقة 50.بدلا من, سوف تكون النتيجة 100

    // Violation of Likov's Substitution Principle
class Rectangle {
    protected int m_width;
    protected int m_height;

    public void setWidth(int width) {
        m_width = width;
    }

    public void setHeight(int height) {
        m_height = height;
    }

    public int getWidth() {
        return m_width;
    }

    public int getHeight() {
        return m_height;
    }

    public int getArea() {
        return m_width * m_height;
    }
}

class Square extends Rectangle {
    public void setWidth(int width) {
        m_width = width;
        m_height = width;
    }

    public void setHeight(int height) {
        m_width = height;
        m_height = height;
    }

}

class LspTest {
    private static Rectangle getNewRectangle() {
        // it can be an object returned by some factory ...
        return new Square();
    }

    public static void main(String args[]) {
        Rectangle r = LspTest.getNewRectangle();

        r.setWidth(5);
        r.setHeight(10);
        // user knows that r it's a rectangle.
        // It assumes that he's able to set the width and height as for the base
        // class

        System.out.println(r.getArea());
        // now he's surprised to see that the area is 100 instead of 50.
    }
}

الخلاصة:

هذا المبدأ هو مجرد امتداد مفتوحة بالقرب من مبدأ و يعني أنه يجب أن نتأكد من أن الجديد في الفئات المشتقة يتم تمديد قاعدة الطبقات دون تغيير سلوكهم.

انظر أيضا: فتح إغلاق مبدأ

بعض المفاهيم المشابهة لتحسين هيكل: الاتفاقية على تكوين

أن تنفيذ ThreeDBoard حيث مجموعة من المجلس يكون ذلك مفيدا ؟

ربما قد تحتاج إلى علاج شرائح من ThreeDBoard في مختلف الطائرات المجلس.في هذه الحالة قد تحتاج إلى مجردة من واجهة (أو فئة مجردة) على مجلس الإدارة أن تسمح تطبيقات متعددة.

من حيث الواجهة الخارجية ، قد ترغب عامل لوح واجهة لكل TwoDBoard و ThreeDBoard (على الرغم من أن أيا من الطرق المذكورة أعلاه صالح).

مربع هو مستطيل حيث عرض يساوي الارتفاع.إذا كان مربع مجموعات اثنين من مختلف الأحجام عرض وارتفاع ينتهك مربع ثابتة.هذا هو عمل حول طريق إدخال آثار جانبية.ولكن إذا كان المستطيل كان setSize(الطول والعرض) مع مسبق 0 < ارتفاع 0 < العرض.مشتقة النوع الأسلوب يتطلب الارتفاع == العرض ؛ أقوى مسبق (و ينتهك lsp).وهذا يدل على أنه على الرغم من مربع هو مستطيل ليس من صالح النوع لأن الشرط الأساسي هو تعزيز.عمل حول (بشكل عام سيئا) يسبب تأثير جانبي و هذا يضعف ما بعد الشرط (الذي ينتهك lsp).setWidth على قاعدة ما بعد الشرط 0 < العرض.مشتقة يضعف مع ارتفاع == العرض.

ولذلك يمكن تغيير حجم مربع لا يمكن تغيير حجم المستطيل.

دعونا نقول اننا استخدام مستطيل في كود

r = new Rectangle();
// ...
r.setDimensions(1,2);
r.fill(colors.red());
canvas.draw(r);

في فئة هندسة علمنا أن مربع نوع خاص من المستطيل لأن العرض هو نفس طول ارتفاعه.دعونا جعل Square فئة استنادا على هذه المعلومات:

class Square extends Rectangle {
    setDimensions(width, height){
        assert(width == height);
        super.setDimensions(width, height);
    }
} 

إذا استبدال Rectangle مع Square في أول الكود ثم فإنه سيتم كسر:

r = new Square();
// ...
r.setDimensions(1,2); // assertion width == height failed
r.fill(colors.red());
canvas.draw(r);

وذلك لأن Square جديد مسبق لم يكن لدينا في Rectangle الدرجة: width == height.وفقا LSP على Rectangle الحالات ينبغي أن يكون إحلاله مع Rectangle فرعية الحالات.وذلك لأن هذه الحالات تمر من نوع الاختيار Rectangle الحالات حيث أنها سوف تسبب أخطاء غير متوقعة في التعليمات البرمجية الخاصة بك.

كان هذا مثالا "شروط مسبقة لا يمكن تعزيزها في فرعي" جزء في ويكي المادة.لذا باختصار انتهاك LSP المحتمل أن تتسبب في حدوث أخطاء في التعليمات البرمجية الخاصة بك في بعض نقطة.

دعونا توضيح في جاوة:

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }

   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

class Car extends TransportationDevice
{
   @Override
   void startEngine() { ... }
}

لا يوجد مشكلة هنا, صحيح ؟ سيارة هو بالتأكيد وسيلة مواصلات ، و هنا يمكننا أن نرى أنه يتجاوز startEngine (طريقة) من المتفوقة.

دعونا إضافة آخر النقل الجهاز:

class Bicycle extends TransportationDevice
{
   @Override
   void startEngine() /*problem!*/
}

كل شيء لا تسير كما هو مخطط لها الآن!نعم الدراجة وسيلة مواصلات ، ومع ذلك ، فإنه لا يكون في المحرك ومن ثم طريقة startEngine() لا يمكن تنفيذها.

هذه هي أنواع المشاكل التي انتهاك Liskov الاستبدال المبدأ يؤدي إلى أنها يمكن أن يكون أكثر عادة تكون معترف بها الأسلوب الذي لا يفعل شيئا ، أو حتى لا يمكن تنفيذها.

حل هذه المشاكل هو الصحيح الميراث الهرمي ، و في حالتنا نحن من شأنه أن يحل المشكلة من خلال التفريق بين فئات من النقل الأجهزة مع محركات.على الرغم من أن الدراجة وسيلة مواصلات ، فإنه لا يجب المحرك.في هذا المثال لدينا تعريف النقل الجهاز هو الخطأ.فإنه لا ينبغي أن يكون محركا.

يمكننا ريفاكتور لدينا TransportationDevice فئة على النحو التالي:

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }
}

الآن نحن يمكن أن تمتد TransportationDevice غير يجهز الأجهزة.

class DevicesWithoutEngines extends TransportationDevice
{  
   void startMoving() { ... }
}

وتوسيع TransportationDevice على الأجهزة الآلية.هنا هو أكثر مناسبة لإضافة محرك الكائن.

class DevicesWithEngines extends TransportationDevice
{  
   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

وبالتالي لدينا فئة السيارات يصبح أكثر تخصصا ، مع الالتزام Liskov مبدأ الإحلال.

class Car extends DevicesWithEngines
{
   @Override
   void startEngine() { ... }
}

لدينا فئة الدراجات هو أيضا في الامتثال Liskov مبدأ الإحلال.

class Bicycle extends DevicesWithoutEngines
{
   @Override
   void startMoving() { ... }
}

أنا أشجعكم على قراءة المقال: انتهاك Liskov إحلال مبدأ (LSP).

يمكنك أن تجد هناك تفسيرا ما هو Liskov إحلال مبدأ العامة القرائن لمساعدتك إذا كنت قد انتهكت بالفعل و مثال على النهج الذي سوف تساعدك على جعل الطبقة الهرمي تكون أكثر أمانا.

أوضح تفسير LSP لقد وجدت حتى الآن "، Liskov إحلال مبدأ يقول أن الهدف من فئة مشتقة ينبغي أن تكون قادرة على استبدال كائن من الفئة الأساسية دون توجيه أية أخطاء في النظام أو تعديل سلوك فئة القاعدة" من هنا.هذه المادة يعطي المثال رمز لانتهاك LSP وتحديد ذلك.

LISKOV إحلال مبدأ (من علامة Seemann الكتاب) تنص على أنه يجب أن تكون قادرة على استبدال تطبيق واحد من واجهة مع آخر دون كسر إما عميل أو التنفيذ.فمن هذا المبدأ التي تمكن من معالجة المتطلبات التي تحدث في المستقبل, حتى لو كنا لا يمكن التنبؤ بها اليوم.

إذا كنا افصل الكمبيوتر من الجدار (تنفيذ) ولا مقبس الحائط (واجهة) ولا الكمبيوتر (العميل) ينهار (في الواقع ، إذا كان الكمبيوتر المحمول ، حتى أنها يمكن أن تعمل على البطاريات لفترة من الوقت).مع البرمجيات ، ومع ذلك ، فإن العميل غالبا ما يتوقع الخدمة أن تكون متاحة.إذا تم إزالة خدمة نحصل على NullReferenceException.للتعامل مع هذا النوع من الحالات, نحن يمكن أن تخلق تنفيذ واجهة لا "لا شيء". هذا هو نمط تصميم المعروف Null كائن ، [4] كما أنه يتوافق تقريبا إلى فصل الكمبيوتر من الجدار.لأننا باستخدام اقتران فضفاض ، ونحن يمكن أن تحل محل التنفيذ الحقيقي مع شيء لا يفعل شيئا دون أن تسبب مشكلة.

Likov هو مبدأ الإحلال الدول التي إذا كان برنامج وحدة باستخدام قاعدة الطبقة ، ثم الإشارة إلى الفئة الأساسية يمكن استبدالها مع فئة مشتقة دون التأثير على وظائف البرنامج وحدة.

القصد المستمدة من أنواع يجب أن يكون تماما بديلة قادرة على قاعدتهم أنواع.

على سبيل المثال - Co-البديل عودة أنواع في جاوة.

LSP يقول أن "الكائنات يجب أن تكون قابلة للاستبدال من قبل فرعية".من ناحية أخرى, ويشير هذا المبدأ إلى

فصول الأطفال يجب أن لا نكسر الطبقة الأم تعريفات نوع.

و المثال التالي يساعد على فهم أفضل LSP.

دون LSP:

public interface CustomerLayout{

    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            return; //it isn`t rendered in this case
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}

تحديد بواسطة LSP:

public interface CustomerLayout{
    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            showAd();//it has a specific behavior based on its requirement
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}

اسمحوا لي أن أحاول ، النظر في واجهة:

interface Planet{
}

ويتم تنفيذ ذلك من خلال الفصل:

class Earth implements Planet {
    public $radius;
    public function construct($radius) {
        $this->radius = $radius;
    }
}

سيتم استخدام الأرض:

$planet = new Earth(6371);
$calc = new SurfaceAreaCalculator($planet);
$calc->output();

تنظر الآن أكثر واحد الطبقة التي تمتد من الأرض:

class LiveablePlanet extends Earth{
   public function color(){
   }
}

الآن وفقا LSP, يجب أن تكون قادرا على استخدام LiveablePlanet في مكان من الأرض فإنه لا ينبغي أن كسر النظام الخاص بك.مثل:

$planet = new LiveablePlanet(6371);  // Earlier we were using Earth here
$calc = new SurfaceAreaCalculator($planet);
$calc->output();

أمثلة مأخوذة من هنا

هنا مقتطف من هذا المنصب أن يوضح الأمور بشكل جيد:

[..] من أجل فهم بعض المبادئ ، من المهم أن ندرك عندما انتهكت.هذا هو ما سأفعله الآن.

ماذا انتهاك هذا المبدأ يعني ؟ يعني هذا كائن لا تنفيذ العقد التي تفرضها فكرة مجردة عن مع واجهة.وبعبارة أخرى فإنه يعني أنه يمكنك تحديد تجريدات الخطأ.

النظر في المثال التالي:

interface Account
{
    /**
     * Withdraw $money amount from this account.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}
class DefaultAccount implements Account
{
    private $balance;
    public function withdraw(Money $money)
    {
        if (!$this->enoughMoney($money)) {
            return;
        }
        $this->balance->subtract($money);
    }
}

هذا انتهاك LSP?نعم.وهذا لأن الحساب العقد يخبرنا أن حساب سيتم سحبها ، ولكن هذا ليس هو الحال دائما.إذا ماذا علي أن أفعل من أجل إصلاح ذلك ؟ أنا فقط تعديل العقد:

interface Account
{
    /**
     * Withdraw $money amount from this account if its balance is enough.
     * Otherwise do nothing.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}

وعدت الآن العقد هو راض.

هذا خفية انتهاك غالبا ما يفرض العميل مع القدرة على التفريق بين الأشياء الملموسة المستخدمة.على سبيل المثال, نظرا الأولى في حساب العقد ، يمكن أن تبدو كما يلي:

class Client
{
    public function go(Account $account, Money $money)
    {
        if ($account instanceof DefaultAccount && !$account->hasEnoughMoney($money)) {
            return;
        }
        $account->withdraw($money);
    }
}

و هذا تلقائيا ينتهك مفتوحة-مغلقة مبدأ [هذا هو الانسحاب المال شرط.لأنك لا تعرف أبدا ما سيحدث إذا كائن لانتهاك العقد لا يملك ما يكفي من المال.ربما يعود فقط لا شيء ، ربما استثناء سيتم طرح.بحيث يكون لديك للتحقق مما إذا كان hasEnoughMoney() - هي ليست جزءا من واجهة.لذلك هذا القسري ملموسة الدرجة تعتمد على الاختيار هو OCP انتهاك].

هذه النقطة أيضا عناوين اعتقاد خاطئ أن واجهت في كثير من الأحيان عن LSP انتهاك.يقول "إذا كان أحد الوالدين تغير سلوك الطفل ، ثم ، فإنه ينتهك LSP." ومع ذلك ، لا — ما دام الطفل لا تنتهك الأم العقد.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top