ตัวอย่างของหลักการชดเชย Liskov คืออะไร?


908

ฉันได้ยินมาว่าหลักการแทน Liskov (LSP) เป็นหลักการพื้นฐานของการออกแบบเชิงวัตถุ มันคืออะไรและมีตัวอย่างของการใช้งานอะไรบ้าง?


ตัวอย่างเพิ่มเติมของการยึดมั่นและการละเมิด LSP ที่นี่
StuartLC

1
คำถามนี้มีคำตอบที่ดีหลายอย่างมากมายและเพื่อให้เป็นกว้างเกินไป
Raedwald

คำตอบ:


892

ตัวอย่างที่ดีที่แสดงให้เห็นถึง LSP (ที่ลุงบ๊อบเขียนในพอดคาสต์ที่ฉันได้ยินเมื่อเร็ว ๆ นี้) เป็นสิ่งที่บางครั้งเสียงที่ถูกต้องในภาษาธรรมชาตินั้นไม่ค่อยได้ผลในโค้ด

ในทางคณิตศาสตร์ที่เป็นSquare Rectangleแน่นอนมันเป็นความเชี่ยวชาญของรูปสี่เหลี่ยมผืนผ้า "เป็น" ทำให้คุณต้องการสร้างโมเดลด้วยการสืบทอด แต่ถ้าในรหัสคุณที่ทำSquareมาจากRectangleนั้นควรจะใช้งานได้ทุกที่ที่คุณคาดหวังSquare Rectangleสิ่งนี้ทำให้เกิดพฤติกรรมแปลก ๆ

ลองนึกภาพคุณมีSetWidthและSetHeightวิธีการในRectangleชั้นฐานของคุณ ดูเหมือนว่าจะมีเหตุผลอย่างสมบูรณ์แบบ แต่ถ้าคุณRectangleอ้างอิงชี้ไปที่Squareแล้วSetWidthและSetHeightไม่ได้ทำให้รู้สึกเพราะการตั้งค่าหนึ่งจะเปลี่ยนการอื่น ๆ เพื่อให้ตรงกับมัน ในกรณีนี้Squareการทดสอบการทดแทน Liskov ล้มเหลวด้วยRectangleและสิ่งที่เป็นนามธรรมของการSquareสืบทอดมาRectangleนั้นเป็นสิ่งที่ไม่ดี

ป้อนคำอธิบายรูปภาพที่นี่

Y'all ควรตรวจสอบอื่น ๆ ล้ำค่าโปสเตอร์ SOLID หลักการสร้างแรงบันดาลใจ


19
@ m-sharp จะเกิดอะไรขึ้นถ้ามันเป็นสี่เหลี่ยมผืนผ้าที่ไม่เปลี่ยนรูปแบบเช่นนั้นแทนที่จะเป็น SetWidth และ SetHeight เรามีวิธี GetWidth และ GetHeight แทน?
Pacerier

139
คุณธรรมของเรื่องราว: จำลองคลาสของคุณตามพฤติกรรมที่ไม่ได้อยู่ในคุณสมบัติ ทำโมเดลข้อมูลของคุณตามคุณสมบัติและไม่ใช่พฤติกรรม ถ้ามันมีพฤติกรรมเหมือนเป็ดแน่นอนว่าเป็นนก
Sklivvz

193
สี่เหลี่ยมจัตุรัสชัดเจนว่าเป็นรูปสี่เหลี่ยมผืนผ้าในโลกแห่งความจริง เราสามารถทำแบบนี้ในรหัสของเราขึ้นอยู่กับสเป็ค สิ่งที่ LSP บ่งชี้คือพฤติกรรมประเภทย่อยควรจับคู่ลักษณะการทำงานของชนิดฐานตามที่กำหนดไว้ในข้อมูลจำเพาะชนิดพื้นฐาน หากข้อมูลจำเพาะประเภทสี่เหลี่ยมผืนผ้าฐานบอกว่าความสูงและความกว้างสามารถตั้งค่าได้อย่างอิสระจากนั้น LSP บอกว่าสี่เหลี่ยมไม่สามารถเป็นประเภทย่อยของสี่เหลี่ยมผืนผ้า ถ้าข้อมูลจำเพาะสี่เหลี่ยมบอกว่าสี่เหลี่ยมไม่เปลี่ยนรูปสี่เหลี่ยมจัตุรัสอาจเป็นชนิดย่อยของสี่เหลี่ยมผืนผ้าได้ มันคือทั้งหมดที่เกี่ยวกับชนิดย่อยที่ยังคงรักษาพฤติกรรมที่ระบุไว้สำหรับประเภทฐาน
SteveT

63
@Pierier ไม่มีปัญหาถ้ามันไม่เปลี่ยนรูป ปัญหาที่แท้จริงของที่นี่คือเราไม่ได้สร้างแบบจำลองรูปสี่เหลี่ยมผืนผ้า แต่แทนที่จะเป็น "รูปสี่เหลี่ยมผืนผ้าที่ปรับขนาดได้" กล่าวคือรูปสี่เหลี่ยมผืนผ้าที่มีความกว้างหรือความสูงสามารถปรับเปลี่ยนได้หลังจากการสร้าง (และเรายังถือว่ามันเป็นวัตถุเดียวกัน) ถ้าเราดูคลาสสี่เหลี่ยมผืนผ้าด้วยวิธีนี้มันเป็นที่ชัดเจนว่าสแควร์ไม่ใช่ "สี่เหลี่ยมผืนผ้าที่ปรับเปลี่ยนได้" เนื่องจากสแควร์ไม่สามารถเปลี่ยนรูปร่างและยังคงเป็นสแควร์ (โดยทั่วไป) ในทางคณิตศาสตร์เราไม่เห็นปัญหาเนื่องจากความไม่แน่นอนไม่มีความหมายในบริบททางคณิตศาสตร์
asmeurer

14
ฉันมีหนึ่งคำถามเกี่ยวกับหลักการ ทำไมจะเป็นปัญหาถ้าSquare.setWidth(int width)ได้ดำเนินการเช่นนี้this.width = width; this.height = width;? ในกรณีนี้รับประกันได้ว่าความกว้างเท่ากับความสูง
MC Emperor

488

หลักการชดเชย Liskov (LSP, ) เป็นแนวคิดในการเขียนโปรแกรมเชิงวัตถุที่ระบุ:

ฟังก์ชันที่ใช้พอยน์เตอร์หรือการอ้างอิงไปยังคลาสพื้นฐานต้องสามารถใช้วัตถุของคลาสที่ได้รับโดยไม่ต้องรู้

หัวใจสำคัญของ LSP นั้นเกี่ยวกับส่วนต่อประสานและสัญญารวมถึงวิธีการตัดสินใจว่าจะขยายชั้นเรียนเมื่อเทียบกับการใช้กลยุทธ์อื่นเช่นองค์ประกอบเพื่อให้บรรลุเป้าหมาย

วิธีที่มีประสิทธิภาพมากที่สุดที่ฉันได้เห็นเพื่อแสดงให้เห็นถึงจุดนี้อยู่ในหัวแรก OOA & D พวกเขานำเสนอสถานการณ์ที่คุณเป็นนักพัฒนาในโครงการเพื่อสร้างกรอบสำหรับเกมกลยุทธ์

พวกเขานำเสนอชั้นเรียนที่แสดงถึงคณะกรรมการที่มีลักษณะเช่นนี้:

แผนภาพระดับ

ทุกวิธีการใช้พิกัด X และ Y Tilesเป็นพารามิเตอร์ในการค้นหาตำแหน่งกระเบื้องในอาร์เรย์สองมิติของ สิ่งนี้จะช่วยให้ผู้พัฒนาเกมสามารถจัดการยูนิตในบอร์ดระหว่างการแข่งขัน

หนังสือเล่มนี้มีการเปลี่ยนแปลงข้อกำหนดเพื่อบอกว่างานเฟรมเกมนั้นต้องสนับสนุนบอร์ดเกม 3 มิติเพื่อรองรับเกมที่มีเที่ยวบิน ดังนั้นระดับเป็นที่รู้จักที่ขยายThreeDBoardBoard

ได้อย่างรวดเร็วก่อนดูเหมือนว่าเป็นการตัดสินใจที่ดี 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


10
ดูเพิ่มเติมที่Circle-Ellipse Problemใน Wikipedia สำหรับตัวอย่างที่คล้ายกัน แต่ง่ายกว่า
Brian

Requote จาก @NotMySelf: "ฉันคิดว่าตัวอย่างนั้นเป็นเพียงการแสดงให้เห็นว่าการสืบทอดจากบอร์ดไม่สมเหตุสมผลในบริบทของ ThreeDBoard และลายเซ็นของเมธอดทั้งหมดไม่มีความหมายกับแกน Z"
Contango

1
ดังนั้นถ้าเราเพิ่มวิธีอื่นในคลาสเด็ก แต่การใช้งานทั้งหมดของผู้ปกครองยังคงสมเหตุสมผลในระดับชั้นเรียนมันจะทำลาย LSP หรือไม่ เนื่องจากในอีกด้านหนึ่งเราได้ปรับอินเทอร์เฟซสำหรับการใช้ลูกสักหน่อยในทางกลับกันถ้าเราโยนเด็กเป็นผู้ปกครองรหัสที่คาดว่าผู้ปกครองจะทำงานได้ดี
Nickolay Kondratyev

5
นี่คือตัวอย่างการต่อต้าน Liskov Liskov ทำให้เราได้สี่เหลี่ยมผืนผ้าจาก Square More-parameters-class จากคลาสที่น้อยกว่าพารามิเตอร์ และคุณได้แสดงให้เห็นอย่างชัดเจนว่ามันไม่ดี มันเป็นเรื่องตลกที่ดีจริงๆที่ได้ทำเครื่องหมายเป็นคำตอบและได้รับการ upvoted 200 เท่าคำตอบต่อต้าน liskov สำหรับคำถาม liskov หลักการของ Liskov เป็นความเข้าใจผิดหรือไม่?
Gangnus

3
ฉันเคยเห็นมรดกทำงานผิดวิธี นี่คือตัวอย่าง คลาสพื้นฐานควรเป็น 3DBoard และ Board class ที่ได้รับ คณะกรรมการยังคงมีแกน Z ของ Max (Z) = Min (Z) = 1
Paulustrious

169

Substitutability เป็นหลักการในการเขียนโปรแกรมเชิงวัตถุโดยระบุว่าในโปรแกรมคอมพิวเตอร์ถ้า S เป็น subtype ของ T ดังนั้นวัตถุประเภท T อาจถูกแทนที่ด้วยวัตถุประเภท S

ลองทำตัวอย่างง่ายๆใน Java:

ตัวอย่างที่ไม่ดี

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{} 

3
ตัวอย่างที่ดี Bird birdแต่สิ่งที่คุณจะทำอย่างไรถ้าลูกค้ามี คุณต้องโยนวัตถุเพื่อ FlyingBirds เพื่อใช้ประโยชน์จากการบินซึ่งไม่ดีใช่มั้ย
Moody

17
ไม่ได้หากลูกค้ามีแสดงBird birdว่าไม่สามารถใช้งานfly()ได้ แค่นั้นแหละ. การผ่าน a Duckจะไม่เปลี่ยนความจริงนี้ หากลูกค้ามีFlyingBirds birdแล้วแม้ว่าจะได้รับDuckมันก็ควรทำงานในลักษณะเดียวกัน
Steve Chamaillard

9
สิ่งนี้จะไม่เป็นตัวอย่างที่ดีสำหรับการแยกส่วนติดต่อด้วยหรือไม่
Saharsh

ตัวอย่างที่ดีเลิศขอบคุณชาย
Abdelhadi Abdo

6
วิธีการเกี่ยวกับการใช้อินเทอร์เฟซ 'Flyable' (นึกไม่ออกว่าชื่อที่ดีกว่า) วิธีนี้เราไม่ผูกมัดตัวเองกับลำดับชั้นที่เข้มงวดนี้ยกเว้นว่าเรารู้ว่าต้องการมันจริงๆ
สามของ

132

LSP เกี่ยวข้องกับค่าคงที่

ตัวอย่างคลาสสิกจะได้รับจากการประกาศหลอกรหัสต่อไปนี้ (การใช้งานที่ละเว้น):

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

class Square : Rectangle { }

ขณะนี้เรามีปัญหาแม้ว่าส่วนต่อประสานตรงกัน เหตุผลก็คือเราได้ละเมิดค่าคงที่อันเนื่องมาจากคำจำกัดความทางคณิตศาสตร์ของสี่เหลี่ยมและสี่เหลี่ยม วิธีการทำงานของ getters และ setters a Rectangleควรเป็นไปตามค่าคงที่ต่อไปนี้:

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

อย่างไรก็ตามคงที่นี้จะต้องถูกละเมิดโดยการดำเนินการที่ถูกต้องของมันจึงไม่ได้เป็นตัวแทนที่ถูกต้องSquareRectangle


35
และด้วยเหตุนี้ความยากลำบากในการใช้ "OO" เพื่อทำสิ่งใด ๆ ที่เราอาจต้องการทำแบบจำลองจริง
DrPizza

9
@DPPizza: แน่นอน อย่างไรก็ตามสองสิ่ง ประการแรกความสัมพันธ์ดังกล่าวยังคงสามารถเป็นแบบอย่างใน OOP แม้ว่าจะไม่สมบูรณ์หรือใช้เส้นทางที่ซับซ้อนมากขึ้น (เลือกสิ่งที่เหมาะสมกับปัญหาของคุณ) ประการที่สองไม่มีทางเลือกที่ดีกว่า การแมป / การดัดแปลงอื่น ๆ มีปัญหาที่เหมือนกันหรือคล้ายกัน ;-)
Konrad Rudolph

7
@NickW ในบางกรณี (แต่ไม่ใช่ด้านบน) คุณสามารถสลับสายโซ่มรดกได้ - พูดอย่างมีเหตุผลจุด 2D คือ - จุด 3D โดยที่มิติที่สามนั้นไม่สนใจ (หรือ 0 - จุดทั้งหมดอยู่บนระนาบเดียวกันใน พื้นที่ 3 มิติ) แต่แน่นอนว่านี่ไม่ใช่การปฏิบัติจริง ๆ โดยทั่วไปนี่เป็นกรณีที่มรดกไม่ได้ช่วยจริงๆและไม่มีความสัมพันธ์ตามธรรมชาติระหว่างหน่วยงาน ทำโมเดลแยกกัน (อย่างน้อยฉันก็ไม่รู้วิธีที่ดีกว่า)
Konrad Rudolph

7
OOP มีวัตถุประสงค์เพื่อจำลองพฤติกรรมไม่ใช่ข้อมูล ชั้นเรียนของคุณละเมิดการห่อหุ้มแม้กระทั่งก่อนที่จะละเมิด LSP
Sklivvz

2
@AustinWBryan Yep; ยิ่งฉันทำงานในฟิลด์นี้นานเท่าไหร่ฉันก็ยิ่งมีแนวโน้มที่จะใช้การสืบทอดสำหรับอินเทอร์เฟซและคลาสพื้นฐานที่เป็นนามธรรมเท่านั้นและจัดองค์ประกอบสำหรับส่วนที่เหลือ บางครั้งมันก็ใช้งานได้มากกว่า (พิมพ์ด้วยความฉลาด) แต่มันก็ช่วยหลีกเลี่ยงปัญหาได้ทั้งหมดและได้รับคำแนะนำจากโปรแกรมเมอร์ผู้มีประสบการณ์คนอื่น ๆ
Konrad Rudolph

77

โรเบิร์ตมาร์ตินมีดีกระดาษบน Liskov ชดเชยหลักการ มันกล่าวถึงวิธีการที่บอบบางและไม่ลึกซึ้งซึ่งหลักการอาจถูกละเมิด

บางส่วนที่เกี่ยวข้องของกระดาษ (โปรดทราบว่าตัวอย่างที่สองคือข้นอย่างมาก):

ตัวอย่างง่ายๆของการละเมิด LSP

หนึ่งในการละเมิดที่เห็นได้ชัดที่สุดของหลักการนี้คือการใช้ C ++ Run-Time Type Information (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;
};

[... ] ลองจินตนาการว่าวันหนึ่งผู้ใช้ต้องการความสามารถในการจัดการกับสี่เหลี่ยมจัตุรัสนอกเหนือจากสี่เหลี่ยม [ ... ]

เห็นได้ชัดว่าจตุรัสเป็นสี่เหลี่ยมสำหรับจุดประสงค์และวัตถุประสงค์ปกติทั้งหมด ตั้งแต่ความสัมพันธ์ ISA ถือมันเป็นตรรกะแบบจำลองระดับกับการที่ได้มาจากSquare Rectangle[ ... ]

SquareจะสืบทอดSetWidthและSetHeightฟังก์ชั่น ฟังก์ชั่นเหล่านี้ไม่เหมาะสมอย่างยิ่งสำหรับ a Squareเนื่องจากความกว้างและความสูงของจตุรัสเหมือนกัน นี่ควรเป็นเงื่อนงำสำคัญที่มีปัญหากับการออกแบบ อย่างไรก็ตามมีวิธีการหลีกเลี่ยงปัญหา เราสามารถแทนที่SetWidthและSetHeight[... ]

แต่พิจารณาฟังก์ชั่นต่อไปนี้:

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

หากเราส่งการอ้างอิงไปยังSquareวัตถุในฟังก์ชั่นนี้ Squareวัตถุจะเสียหายเนื่องจากความสูงจะไม่เปลี่ยนแปลง นี่เป็นการละเมิด LSP ที่ชัดเจน ฟังก์ชั่นไม่ทำงานสำหรับอนุพันธ์ของข้อโต้แย้ง

[ ... ]


14
ช้าไปหน่อย แต่ฉันคิดว่านี่เป็นข้อความที่น่าสนใจในบทความนั้น: Now the rule for the preconditions and postconditions for derivatives, as stated by Meyer is: ...when redefining a routine [in a derivative], you may only replace its precondition by a weaker one, and its postcondition by a stronger one. หากเงื่อนไขระดับชั้นเด็กแข็งแรงกว่าเงื่อนไขระดับชั้นผู้ปกครองคุณไม่สามารถทดแทนเด็กให้เป็นผู้ปกครองได้โดยไม่ละเมิดเงื่อนไขล่วงหน้า ดังนั้น LSP
user2023861

@ user2023861 คุณพูดถูก ฉันจะเขียนคำตอบตามนี้
inf3rno

40

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ต้องมีพารามิเตอร์อินพุตที่แตกต่างกันและเอาต์พุต covariant

Contravariant หมายถึงความแปรปรวนที่ตรงกันข้ามกับทิศทางของการสืบทอดเช่นชนิดSiของพารามิเตอร์อินพุตแต่ละวิธีของแต่ละประเภทย่อยSจะต้องเหมือนกันหรือเป็นประเภทsupertypeของTiพารามิเตอร์อินพุตที่สอดคล้องกันของวิธีการที่สอดคล้องกันของ supertype T.

ความแปรปรวนร่วมหมายถึงความแปรปรวนในทิศทางเดียวกันของการสืบทอดเช่นชนิดSoของเอาต์พุตของแต่ละวิธีของประเภทย่อยSจะต้องเหมือนกันหรือประเภทย่อยของประเภทToของเอาต์พุตที่สอดคล้องกันของวิธีการที่สอดคล้องกันของซุปเปอร์Tประเภท

นี้เป็นเพราะถ้าโทรคิดว่ามันมีประเภทT, คิดว่ามันจะเรียกวิธีการของTแล้วมันซัพพลายอาร์กิวเมนต์ (s) ประเภทและกำหนดออกไปชนิดTi Toเมื่อมันเป็นจริงเรียกวิธีการที่สอดคล้องกันของSแล้วแต่ละTiอาร์กิวเมนต์อินพุตถูกกำหนดให้กับSiพารามิเตอร์การป้อนข้อมูลและการส่งออกได้รับมอบหมายให้ชนิดSo Toดังนั้นหากSiไม่ได้ contravariant WRT ไปTiแล้วชนิดย่อยXi-which จะไม่เป็นชนิดย่อยของSi-could Tiได้รับมอบหมายให้

นอกจากนี้สำหรับภาษา (เช่น Scala หรือ Ceylon) ซึ่งมีคำอธิบายประกอบแบบแปรปรวนแบบนิยามไซต์บนพารามิเตอร์ polymorphism ชนิด (เช่น generics) การร่วมหรือการตรงกันข้ามของคำอธิบายประกอบแบบแปรปรวนสำหรับพารามิเตอร์ประเภทแต่ละประเภทTจะต้องอยู่ในทิศทางตรงกันข้ามหรือในทิศทางเดียวกัน ตามลำดับสำหรับพารามิเตอร์อินพุตหรือเอาต์พุต (ของทุกเมธอดT) ที่มีชนิดของพารามิเตอร์ชนิด

นอกจากนี้สำหรับแต่ละพารามิเตอร์อินพุทหรือเอาท์พุทที่มีประเภทฟังก์ชั่นทิศทางความแปรปรวนที่จำเป็นจะถูกกลับรายการ กฎนี้ใช้ซ้ำ


ประเภทย่อยมีความเหมาะสมที่สามารถระบุค่า invariants

มีการวิจัยอย่างต่อเนื่องเกี่ยวกับวิธีจำลองแบบค่าคงที่เพื่อให้ถูกบังคับใช้โดยคอมไพเลอร์

พิมพ์ (ดูหน้า 3) ประกาศและบังคับใช้ค่าคงที่มุมฉากของรัฐในการพิมพ์ อีกวิธีหนึ่งคือค่าคงที่สามารถบังคับใช้โดยการแปลงยืนยันประเภท ตัวอย่างเช่นเพื่อยืนยันว่าไฟล์เปิดอยู่ก่อนที่จะปิดไฟล์จากนั้น File.open () สามารถส่งคืนประเภท OpenFile ซึ่งมีเมธอด close () ที่ไม่สามารถใช้ได้ในไฟล์ โอเอกซ์ APIสามารถเป็นตัวอย่างของการจ้างพิมพ์ในการบังคับใช้ค่าคงที่ที่รวบรวมเวลาอีก ระบบประเภทอาจจะทัวริงสมบูรณ์เช่นสกาล่า ภาษาที่พิมพ์ได้ไม่แน่นอนและผู้พิสูจน์ทฤษฎีบททำให้รูปแบบของการพิมพ์ที่มีลำดับสูงขึ้นเป็นทางการ

เนื่องจากความต้องการความหมายเป็นนามธรรมมากกว่าส่วนขยายฉันคาดหวังว่าการใช้การพิมพ์เพื่อจำลองค่าคงที่เช่นความหมายการรวมกันของคำสั่ง denotational ที่สูงกว่าจึงมีความเหนือกว่า Typestate 'ส่วนขยาย' หมายถึงองค์ประกอบที่ไม่ จำกัด และมีการเปลี่ยนแปลงของการพัฒนาแบบแยกส่วน เพราะมันดูเหมือนว่าฉันจะเป็นสิ่งที่ตรงกันข้ามของการรวมและองศาของอิสระมีสองแบบขึ้นอยู่กับแต่ละ (เช่นประเภทและพิมพ์) สำหรับการแสดงความหมายที่ใช้ร่วมกันซึ่งไม่สามารถรวมเข้าด้วยกันเพื่อขยายองค์ประกอบ . ตัวอย่างเช่นส่วนขยายที่มีปัญหาเช่นนิพจน์ถูกรวมเป็นหนึ่งในการพิมพ์ย่อยฟังก์ชันมากไปและโดเมนการพิมพ์ตามพารามิเตอร์

ตำแหน่งทางทฤษฎีของฉันคือสำหรับ ความรู้ที่มีอยู่ (ดูหัวข้อ“ การรวมศูนย์เป็นคนตาบอดและไม่เหมาะ”) จะไม่มีรูปแบบทั่วไปที่สามารถบังคับใช้ความครอบคลุม 100% ของค่าคงที่ที่เป็นไปได้ทั้งหมดในภาษาคอมพิวเตอร์ทัวริง สำหรับความรู้ที่มีอยู่ความเป็นไปได้ที่ไม่คาดคิดมีอยู่มากมายเช่นความยุ่งเหยิงและเอนโทรปีจะต้องเพิ่มขึ้นเสมอ นี่คือพลัง entropic เพื่อพิสูจน์การคำนวณส่วนขยายที่เป็นไปได้ทั้งหมดคือการคำนวณส่วนขยายที่เป็นไปได้ทั้งหมดก่อน

นี่คือเหตุผลที่ทฤษฎีบท Halting มีอยู่นั่นคือไม่สามารถบอกได้ว่าทุกโปรแกรมที่เป็นไปได้ในภาษาโปรแกรมทัวริงสมบูรณ์ สามารถพิสูจน์ได้ว่าโปรแกรมบางโปรแกรมสิ้นสุดลง (ซึ่งเป็นไปได้ทั้งหมดที่ได้รับการกำหนดและคำนวณ) แต่มันเป็นไปไม่ได้ที่จะพิสูจน์ว่าการขยายที่เป็นไปได้ทั้งหมดของโปรแกรมนั้นสิ้นสุดลงเว้นแต่ว่าความเป็นไปได้สำหรับการขยายโปรแกรมนั้นจะไม่สมบูรณ์ทัวริง (เช่นผ่านการพิมพ์ตาม) เนื่องจากความต้องการขั้นพื้นฐานสำหรับทัวริงสมบูรณ์คือเรียกซ้ำแบบไม่มีขอบเขตจึงง่ายที่จะเข้าใจว่าทฤษฎีบทความไม่สมบูรณ์ของGödelและความขัดแย้งของรัสเซลนำมาใช้กับการขยายได้อย่างไร

การตีความของทฤษฎีบทเหล่านี้รวมไว้ในการทำความเข้าใจแนวความคิดทั่วไปของพลังเอนโทรปี:

  • ทฤษฎีบทความไม่สมบูรณ์ของGödel : ทฤษฎีทางการใด ๆ ที่ความจริงทางคณิตศาสตร์ทั้งหมดสามารถพิสูจน์ได้นั้นไม่สอดคล้องกัน
  • ความขัดแย้งของรัสเซล : กฎการเป็นสมาชิกทุกชุดที่สามารถมีได้ทั้งชุดจะระบุประเภทเฉพาะของสมาชิกแต่ละคนหรือมีตัวของมันเอง ดังนั้นจึงตั้งค่าไม่สามารถขยายหรือถูกเรียกซ้ำไม่ได้ ตัวอย่างเช่นชุดของทุกสิ่งที่ไม่ใช่กาน้ำชารวมถึงตัวเองซึ่งรวมถึงตัวเองซึ่งรวมถึงตัวเองซึ่งรวมถึงตัวเอง ฯลฯ ... ดังนั้นกฎจะไม่สอดคล้องกันถ้ามัน (อาจมีชุดและ) ไม่ได้ระบุประเภทเฉพาะ (เช่นอนุญาตประเภทที่ไม่ได้ระบุทั้งหมด) และไม่อนุญาตส่วนขยายที่ไม่ได้ จำกัด นี่คือชุดของเซตที่ไม่ใช่สมาชิกของตัวเอง การไร้ความสามารถที่จะสอดคล้องและแจกแจงอย่างสมบูรณ์ในทุกส่วนที่เป็นไปได้คือทฤษฎีบทที่ไม่สมบูรณ์ของGödel
  • Liskov Substition Principle : โดยทั่วไปแล้วมันเป็นปัญหาที่ไม่สามารถตัดสินใจได้ไม่ว่าเซตใดจะเป็นเซตย่อยของอีกชุดหนึ่งนั่นคือการสืบทอดโดยทั่วไปจะไม่สามารถตัดสินใจได้
  • การอ้างอิง Linsky : มันไม่สามารถบอกได้ว่าการคำนวณของบางสิ่งเมื่ออธิบายหรือรับรู้คือการรับรู้ (ความจริง) ไม่มีจุดอ้างอิงที่แน่นอน
  • ทฤษฎีบทของ Coase : ไม่มีจุดอ้างอิงภายนอกดังนั้นอุปสรรคใด ๆ ต่อความเป็นไปได้จากภายนอกที่ไร้ขอบเขตจะล้มเหลว
  • กฎข้อที่สองของอุณหพลศาสตร์ : ทั้งจักรวาล (ระบบปิดคือทุกอย่าง) แนวโน้มที่จะเกิดความยุ่งเหยิงสูงสุดนั่นคือความเป็นไปได้ที่อิสระสูงสุด

17
@Shelyby: คุณผสมหลายอย่างมากเกินไป สิ่งต่าง ๆ ไม่สับสนอย่างที่คุณบอก การยืนยันทางทฤษฎีของคุณส่วนใหญ่ตั้งอยู่บนพื้นที่ที่บอบบางเช่น 'เพื่อให้มีความรู้มีความเป็นไปได้ที่ไม่คาดคิดมีอยู่มากมาย ......... ' และ 'โดยทั่วไปมันเป็นปัญหาที่ไม่อาจตัดสินใจได้ว่าชุดใดเป็นเซตย่อยของอีก โดยทั่วไปจะไม่สามารถสืบทอดมรดกได้ ' คุณสามารถเริ่มบล็อกแยกต่างหากสำหรับแต่ละจุดเหล่านี้ อย่างไรก็ตามการยืนยันและสมมติฐานของคุณเป็นที่น่าสงสัยอย่างมาก เราต้องไม่ใช้สิ่งที่คนอื่นไม่รู้!
aknon

1
@aknon ฉันมีบล็อกที่อธิบายเรื่องเหล่านี้ในเชิงลึกยิ่งขึ้น แบบจำลอง TOE Space กาลเวลาไม่มีที่สิ้นสุดของฉันคือความถี่ที่ไม่ จำกัด ฉันไม่สับสนว่าฟังก์ชั่นอุปนัยแบบเรียกซ้ำมีค่าเริ่มต้นที่ทราบพร้อมกับจุดสิ้นสุดแบบไม่สิ้นสุดหรือฟังก์ชันเหรียญหน้าที่มีค่าจุดสิ้นสุดที่ไม่รู้จักและจุดเริ่มต้นที่รู้จัก สัมพัทธภาพเป็นปัญหาเมื่อมีการแนะนำการเรียกซ้ำ นี่คือเหตุผลที่ทัวริงสมบูรณ์เทียบเท่ากับการเรียกซ้ำมากมาย
Shelby Moore III

4
@ShelbyMooreIII คุณกำลังไปในทิศทางที่มากเกินไป นี่ไม่ใช่คำตอบ
Soldalma

1
@Soldalma มันเป็นคำตอบ คุณไม่เห็นมันในส่วนคำตอบ ของคุณคือความคิดเห็นเพราะมันอยู่ในส่วนความคิดเห็น
Shelby Moore III

1
เหมือนการมิกซ์กับโลกสกาล่า!
Ehsan M. Kermani

24

ฉันเห็นสี่เหลี่ยมและสี่เหลี่ยมจัตุรัสในทุกคำตอบและวิธีการละเมิด 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 !
    }
}

ตอนนี้ชนิดย่อยไม่สามารถใช้วิธีเดียวกันได้เนื่องจากพวกมันไม่ได้ผลลัพธ์ที่เหมือนกันอีกต่อไป


6
ตัวอย่างนี้ไม่ได้ละเมิด LSP เพียงตราบเท่าที่เรา จำกัด ความหมายของDatabase::selectQueryการสนับสนุนเพียงส่วนย่อยของ SQL ที่ได้รับการสนับสนุนโดยโปรแกรมฐานข้อมูลทั้งหมด มันใช้งานไม่ได้จริง ๆ ... ที่กล่าวมาตัวอย่างยังเข้าใจได้ง่ายกว่าที่คนอื่นใช้กันที่นี่
Palec

5
ฉันพบคำตอบนี้ง่ายที่สุดที่จะเข้าใจจากส่วนที่เหลือ
Malcolm Salvador

23

มีรายการตรวจสอบเพื่อพิจารณาว่าคุณละเมิด Liskov หรือไม่

  • หากคุณละเมิดรายการใดรายการหนึ่งต่อไปนี้ -> คุณละเมิด Liskov
  • หากคุณไม่ละเมิด -> ไม่สามารถสรุปอะไรได้

รายการตรวจสอบ:

  • ไม่ควรมีข้อยกเว้นใหม่ในคลาสที่ได้รับ : ถ้าคลาสพื้นฐานของคุณขว้าง ArgumentNullException คลาสย่อยของคุณจะได้รับอนุญาตให้โยนข้อยกเว้นประเภท ArgumentNullException หรือข้อยกเว้นใด ๆ ที่ได้จาก ArgumentNullException การโยน IndexOutOfRangeException เป็นการละเมิด Liskov
  • ไม่สามารถเสริมความแข็งแกร่งล่วงหน้าได้ : สมมติว่าคลาสพื้นฐานของคุณทำงานกับสมาชิก int ตอนนี้ประเภทย่อยของคุณต้องการให้ int เป็นค่าบวก นี่คือเงื่อนไขล่วงหน้าที่แข็งแกร่งขึ้นและตอนนี้โค้ดใด ๆ ที่ทำงานได้ดีอย่างสมบูรณ์แบบมาก่อนด้วย ints เชิงลบจะถูกทำลาย
  • โพสต์เงื่อนไขไม่สามารถลดลงได้ : สมมติว่าคลาสพื้นฐานของคุณจำเป็นต้องปิดการเชื่อมต่อกับฐานข้อมูลทั้งหมดก่อนที่เมธอดจะส่งคืน ในคลาสย่อยของคุณให้คุณเอาชนะวิธีการนั้นและปล่อยให้การเชื่อมต่อเปิดเพื่อใช้ซ้ำ คุณทำให้โพสต์เงื่อนไขของวิธีนั้นอ่อนลง
  • ค่าคงที่ต้องรักษา : ข้อ จำกัด ที่ยากและเจ็บปวดที่สุดในการเติมเต็ม ค่าคงที่เป็นเวลาที่ซ่อนอยู่ในชั้นฐานและวิธีเดียวที่จะเปิดเผยพวกเขาคือการอ่านรหัสของชั้นฐาน โดยทั่วไปคุณต้องแน่ใจว่าเมื่อใดที่คุณแทนที่วิธีที่ไม่สามารถเปลี่ยนแปลงได้จะต้องไม่เปลี่ยนแปลงหลังจากที่วิธีการแทนที่ของคุณถูกดำเนินการ สิ่งที่ดีที่สุดที่ฉันคิดได้คือการบังคับใช้ข้อ จำกัด ที่ไม่เปลี่ยนแปลงนี้ในคลาสฐาน แต่นั่นไม่ใช่เรื่องง่าย
  • ข้อ จำกัด ด้านประวัติ : เมื่อแทนที่เมธอดคุณจะไม่ได้รับอนุญาตให้แก้ไขคุณสมบัติที่ไม่สามารถแก้ไขได้ในคลาสพื้นฐาน ลองดูรหัสเหล่านี้และคุณจะเห็นชื่อถูกกำหนดให้ไม่สามารถแก้ไขได้ (ชุดส่วนตัว) แต่ประเภทย่อยแนะนำวิธีการใหม่ที่ช่วยให้สามารถแก้ไขได้ (ผ่านการสะท้อน):

    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 #) ดังนั้นฉันไม่สนใจพวกเขา

อ้างอิง:


ฉันเป็นนักพัฒนา C # และฉันจะบอกว่าข้อความล่าสุดของคุณไม่เป็นจริงใน Visual Studio 2010 ด้วยกรอบงาน. Net 4.0 ความแปรปรวนร่วมของประเภทผลตอบแทนอนุญาตให้มีประเภทผลตอบแทนที่ได้รับมากกว่าสิ่งที่ถูกกำหนดโดยอินเตอร์เฟส ตัวอย่าง: ตัวอย่าง: IEnumerable <T> (T คือ covariant) IEnumerator <T> (T คือ covariant) IQueryable <T> (T คือ covariant) IGrouping <TKey, TElement> (TKey และ TElement เป็น covariant) IComparer <T> (T คือ covariant is contravariant) IEqualityComparer <T> (T คือ contravariant) IComparable <T> (T คือ contravariant) msdn.microsoft.com/en-us/library/dd233059(v=vs.100).aspx
LCarter

1
คำตอบที่ดีและมุ่งเน้น (แม้ว่าคำถามดั้งเดิมเป็นเรื่องเกี่ยวกับตัวอย่างมากกว่ากฎ)
Mike

22

LSP เป็นกฎเกี่ยวกับสัญญาของ clases: ถ้าคลาสพื้นฐานเป็นไปตามสัญญาจากนั้นคลาสที่ได้รับจาก LSP จะต้องปฏิบัติตามสัญญานั้นด้วย

ใน Pseudo-python

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

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

ตอบสนอง LSP ถ้าทุกครั้งที่คุณเรียก Foo บนวัตถุ Derived จะให้ผลลัพธ์เหมือนกับการเรียก Foo บนวัตถุฐานตราบใดที่อาร์กิวเมนต์ยังคงเหมือนเดิม


9
แต่ ... ถ้าคุณมีพฤติกรรมแบบเดียวกันอยู่เสมออะไรคือจุดที่มีคลาสที่ได้รับมา
Leonid

2
คุณพลาดจุด: มันเป็นพฤติกรรมที่สังเกตได้เหมือนกัน ตัวอย่างเช่นคุณอาจแทนที่บางสิ่งด้วยประสิทธิภาพ O (n) ด้วยสิ่งที่เทียบเท่ากับการใช้งานได้ แต่มีประสิทธิภาพ O (lg n) หรือคุณอาจแทนที่สิ่งที่เข้าถึงข้อมูลที่ใช้กับ MySQL และแทนที่ด้วยฐานข้อมูลในหน่วยความจำ
Charlie Martin

@ Charlie Martin ทำการเข้ารหัสไปยังส่วนต่อประสานแทนที่จะใช้งาน - ฉันขุดมัน สิ่งนี้ไม่ซ้ำกับ OOP ภาษาที่ใช้งานได้เช่น Clojure ส่งเสริมเช่นกัน แม้ในแง่ของ Java หรือ C # ฉันคิดว่าการใช้อินเทอร์เฟซแทนการใช้คลาสนามธรรมบวกกับลำดับชั้นของคลาสจะเป็นธรรมชาติสำหรับตัวอย่างที่คุณให้ Python ไม่ได้พิมพ์อย่างมากและไม่มีอินเทอร์เฟซอย่างน้อยก็ไม่ชัดเจน ความยากลำบากของฉันคือฉันได้ทำ OOP มาหลายปีโดยไม่ยึดติดกับ SOLID ตอนนี้ฉันเจอแล้วดูเหมือนว่าจะมีข้อ จำกัด และเกือบจะขัดแย้งกันเอง
Hamish Grubijan

คุณต้องย้อนกลับไปดูเอกสารต้นฉบับของบาร์บาร่า reports-archive.adm.cs.cmu.edu/anon/1999/CMU-CS-99-156.psมันไม่ได้ระบุไว้จริงๆในแง่ของอินเทอร์เฟซและเป็นความสัมพันธ์เชิงตรรกะที่มี (หรือไม่) ในใด ๆ ภาษาโปรแกรมที่มีรูปแบบของการสืบทอด
Charlie Martin

1
@HamishGrubijan ฉันไม่รู้ว่าใครบอกคุณว่า Python ไม่ได้พิมพ์ออกมาอย่างรุนแรง แต่พวกเขาโกหกคุณ (และถ้าคุณไม่เชื่อฉันให้ลองล่าม Python และลอง2 + "2") บางทีคุณอาจสับสน "พิมพ์อย่างยิ่ง" กับ "พิมพ์แบบคงที่" หรือไม่?
asmeurer

21

ยาวสั้นเรื่องขอปล่อยให้สี่เหลี่ยมสี่เหลี่ยมและสี่เหลี่ยมสี่เหลี่ยมตัวอย่างในทางปฏิบัติเมื่อการขยายชั้นเรียนพ่อแม่คุณมีทั้งรักษาผู้ปกครอง 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;
    }
}

จากนั้นคุณสามารถให้ลูกค้าทำงานกับ Base 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/


20

ฟังก์ชันที่ใช้พอยน์เตอร์หรือการอ้างอิงไปยังคลาสพื้นฐานต้องสามารถใช้วัตถุของคลาสที่ได้รับโดยไม่ต้องรู้

เมื่อฉันอ่านเกี่ยวกับ LSP ครั้งแรกฉันคิดว่านี่เป็นความหมายที่เข้มงวดมากโดยเฉพาะอย่างยิ่งการเปรียบเทียบกับการใช้อินเตอร์เฟสและการคัดเลือกนักพิมพ์ที่ปลอดภัย ซึ่งจะหมายถึงว่า LSP นั้นมั่นใจได้หรือไม่ว่าจะเป็นภาษาของตัวเอง ตัวอย่างเช่นในแง่ที่เข้มงวดนี้ ThreeDBoard สามารถทดแทนบอร์ดได้อย่างแน่นอนเท่าที่คอมไพเลอร์เกี่ยวข้อง

หลังจากอ่านแนวคิดเพิ่มเติมแม้ว่าฉันพบว่า LSP โดยทั่วไปจะตีความกว้างกว่านั้น

กล่าวโดยย่อความหมายของรหัสไคลเอนต์คือ "รู้" ว่าวัตถุที่อยู่หลังตัวชี้นั้นเป็นประเภทที่ได้รับแทนที่จะเป็นประเภทตัวชี้นั้นไม่ได้ จำกัด อยู่ที่ความปลอดภัยของประเภท การยึดติดกับ LSP ยังสามารถทดสอบได้ผ่านการตรวจสอบพฤติกรรมการทำงานจริงของวัตถุ นั่นคือการตรวจสอบผลกระทบของสถานะของวัตถุและวิธีการขัดแย้งกับผลของการเรียกวิธีการหรือประเภทของข้อยกเว้นที่ถูกโยนออกมาจากวัตถุ

กลับไปที่ตัวอย่างอีกครั้งในทางทฤษฎีแล้ววิธีการของบอร์ดสามารถทำงานได้ดีบน ThreeDBoard อย่างไรก็ตามในทางปฏิบัติมันเป็นเรื่องยากมากที่จะป้องกันความแตกต่างในพฤติกรรมที่ลูกค้าอาจไม่สามารถจัดการได้อย่างถูกต้องโดยไม่รบกวนการทำงานที่ ThreeDBoard ตั้งใจจะเพิ่ม

ด้วยความรู้นี้ในมือการประเมินการยึดมั่นของ LSP จึงเป็นเครื่องมือที่ยอดเยี่ยมในการพิจารณาว่าการจัดวางองค์ประกอบเป็นกลไกที่เหมาะสมกว่าสำหรับการขยายฟังก์ชั่นที่มีอยู่มากกว่าการสืบทอด


19

ฉันเดาว่าทุกคนจะเข้าใจว่า LSP คืออะไรในทางเทคนิค: โดยทั่วไปคุณต้องการที่จะสรุปรายละเอียดย่อยและใช้ supertypes อย่างปลอดภัย

Liskov มีกฎพื้นฐาน 3 ข้อ:

  1. กฎลายเซ็น: ควรมีการใช้งานที่ถูกต้องของทุกการดำเนินการของ supertype ในประเภทย่อย syntactically คอมไพเลอร์จะสามารถตรวจสอบคุณได้ มีกฎเล็กน้อยเกี่ยวกับการโยนข้อยกเว้นให้น้อยลงและเข้าถึงอย่างน้อยที่สุดเท่าที่จะทำได้โดยใช้วิธี supertype

  2. กฎวิธีการ: การปฏิบัติงานของการดำเนินการเหล่านั้นมีความหมายทางเสียง

    • เงื่อนไขที่อ่อนแอกว่า: ฟังก์ชันย่อยควรใช้เวลาอย่างน้อยสิ่งที่ supertype ใช้เป็นอินพุตหากไม่มากกว่านั้น
    • Stronger Postconditions: พวกเขาควรสร้างเซ็ตย่อยของเอาท์พุทของวิธีซูเปอร์ไทป์ที่สร้างขึ้น
  3. คุณสมบัติกฎ: สิ่งนี้นอกเหนือไปจากการเรียกใช้ฟังก์ชั่นของแต่ละบุคคล

    • Invariants: สิ่งที่เป็นจริงเสมอจะต้องเป็นจริง เช่น. ขนาดของชุดจะไม่ลบ
    • คุณสมบัติวิวัฒนาการ: โดยทั่วไปแล้วสิ่งที่เกี่ยวกับการเปลี่ยนแปลงไม่ได้หรือสถานะของวัตถุนั้นอาจเป็นไปได้หรือบางทีวัตถุนั้นจะเติบโตขึ้นและไม่หดตัวลงดังนั้นวิธีการย่อยจึงไม่ควรทำ

คุณสมบัติเหล่านี้ทั้งหมดจะต้องได้รับการเก็บรักษาไว้และฟังก์ชันย่อยพิเศษไม่ควรละเมิดคุณสมบัติของซุปเปอร์ประเภท

หากสามสิ่งนี้ได้รับการดูแลคุณจะต้องแยกออกจากสิ่งที่อยู่ข้างใต้และคุณกำลังเขียนโค้ดคู่ที่หลวม

ที่มา: การพัฒนาโปรแกรมใน Java - Barbara Liskov


18

ตัวอย่างที่สำคัญของการใช้งานของ LSP อยู่ในการทดสอบซอฟต์แวร์

ถ้าฉันมีคลาส A ซึ่งเป็นคลาสย่อยที่สอดคล้องกับ LSP ของ B ดังนั้นฉันสามารถใช้ชุดการทดสอบของ B เพื่อทดสอบ A อีกครั้ง

หากต้องการทดสอบคลาสย่อย A อย่างสมบูรณ์ฉันอาจต้องเพิ่มกรณีทดสอบอีกสองสามกรณี แต่อย่างน้อยที่สุดฉันสามารถใช้กรณีทดสอบของ Superclass B ทั้งหมดอีกครั้ง

วิธีที่จะทำให้รู้ได้คือการสร้างสิ่งที่ McGregor เรียกว่า "ลำดับชั้นแบบขนานสำหรับการทดสอบ": ATestชั้นเรียนของฉันจะรับช่วงBTestต่อ การฉีดบางรูปแบบนั้นจำเป็นต้องมีเพื่อให้แน่ใจว่ากรณีทดสอบสามารถทำงานกับวัตถุประเภท A แทนประเภท B (รูปแบบวิธีการเทมเพลตอย่างง่ายจะทำ)

โปรดทราบว่าการนำชุดการทดสอบพิเศษมาใช้ใหม่สำหรับการปรับใช้คลาสย่อยทั้งหมดในความเป็นจริงเป็นวิธีการทดสอบว่าการปรับใช้คลาสย่อยเหล่านี้สอดคล้องกับ LSP ดังนั้นหนึ่งสามารถยืนยันว่าควรรันชุดทดสอบ superclass ในบริบทของคลาสย่อยใด ๆ

ดูคำตอบสำหรับคำถาม Stackoverflow " ฉันสามารถใช้ชุดการทดสอบที่ใช้ซ้ำได้เพื่อทดสอบการใช้งานของส่วนต่อประสาน "


14

มาอธิบายใน Java:

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!*/
}

ตอนนี้ทุกอย่างไม่เป็นไปตามแผนที่วางไว้! ใช่จักรยานเป็นอุปกรณ์การขนส่ง แต่ไม่มีเครื่องยนต์ดังนั้นจึงไม่สามารถใช้วิธีการเริ่มเครื่องยนต์ ()

ปัญหาเหล่านี้เป็นปัญหาที่การละเมิดหลักการทดแทน Liskov นำไปสู่และพวกเขามักจะได้รับการยอมรับโดยวิธีการที่ไม่ทำอะไรเลยหรือแม้แต่ไม่สามารถดำเนินการได้

การแก้ปัญหาเหล่านี้เป็นลำดับชั้นการสืบทอดที่ถูกต้องและในกรณีของเราเราจะแก้ปัญหาโดยการจำแนกประเภทของอุปกรณ์การขนส่งที่มีและไม่มีเครื่องยนต์ แม้ว่าจักรยานเป็นอุปกรณ์การขนส่ง แต่ก็ไม่มีเครื่องยนต์ ในตัวอย่างนี้นิยามอุปกรณ์ขนส่งของเราผิด มันไม่ควรมีเครื่องยนต์

เราสามารถ refactor คลาส 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() { ... }
}

และขยายการขนส่งอุปกรณ์สำหรับอุปกรณ์ที่ใช้มอเตอร์ นี่คือความเหมาะสมในการเพิ่มวัตถุเครื่องยนต์

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() { ... }
}

9

การกำหนด LSP นี้แข็งแกร่งเกินไป:

ถ้าสำหรับวัตถุแต่ละชนิด o1 ของ S มีวัตถุ o2 ของชนิด T เช่นนั้นสำหรับโปรแกรมทั้งหมดที่ P ในแง่ของ T พฤติกรรมของ P จะไม่เปลี่ยนแปลงเมื่อ o1 ถูกแทนสำหรับ o2 ดังนั้น S เป็นประเภทย่อยของ T

ซึ่งโดยทั่วไปหมายถึงว่า S เป็นอีกสิ่งหนึ่งที่มีการห่อหุ้มอย่างสมบูรณ์ในสิ่งเดียวกันกับ T และฉันอาจกล้าที่จะตัดสินใจว่าประสิทธิภาพเป็นส่วนหนึ่งของพฤติกรรมของ P ...

ดังนั้นโดยทั่วไปแล้วการใช้งานของการผูกมัดล่าช้าจะเป็นการละเมิด LSP เป็นจุดรวมของ OO ที่จะได้รับพฤติกรรมที่แตกต่างกันเมื่อเราแทนที่วัตถุชนิดหนึ่งเป็นอีกประเภทหนึ่ง!

สูตรที่อ้างถึงโดยวิกิพีเดียดีกว่าเนื่องจากคุณสมบัติขึ้นอยู่กับบริบทและไม่จำเป็นต้องรวมถึงพฤติกรรมทั้งหมดของโปรแกรม


2
เอ่อสูตรนั้นเป็นของบาร์บาร่าลิสคอฟเอง บาร์บาร่าลิสคอฟ“ Data Abstraction and Hierarchy,” SIGPLAN Notices, 23,5 (พฤษภาคม, 1988) มันไม่ได้ "แข็งแกร่งเกินไป" เป็น "ถูกต้อง" และไม่มีความหมายที่คุณคิดว่ามี มันแข็งแกร่ง แต่มีความแข็งแกร่งในปริมาณที่เหมาะสม
DrPizza

แล้วมีเชื้อน้อยมากในชีวิตจริง :)
ดาเมียน Pollet

3
"พฤติกรรมไม่เปลี่ยนแปลง" ไม่ได้หมายความว่าประเภทย่อยจะให้ผลลัพธ์ที่เป็นรูปธรรมเหมือนกันทุกประการ หมายความว่าพฤติกรรมของประเภทย่อยตรงกับสิ่งที่คาดหวังในประเภทฐาน ตัวอย่าง: ประเภทฐานรูปร่างอาจมีวิธีการวาด () และกำหนดว่าวิธีนี้ควรแสดงผลรูปร่าง สองชนิดย่อยของ Shape (เช่น Square และ Circle) จะใช้วิธีการ draw () และผลลัพธ์จะดูแตกต่างกัน แต่ตราบใดที่พฤติกรรม (การเรนเดอร์รูปร่าง) ตรงกับพฤติกรรมที่ระบุของ Shape ดังนั้น Square และ Circle จะเป็นชนิดย่อยของ Shape ตาม LSP
SteveT

9

ในประโยคที่ง่ายมากเราสามารถพูดได้:

ชั้นเรียนของเด็กจะต้องไม่ละเมิดลักษณะชั้นฐานของมัน มันจะต้องมีความสามารถกับมัน เราสามารถพูดได้ว่ามันเหมือนกับ subtyping


9

หลักการทดแทนของ Liskov (LSP)

ตลอดเวลาที่เราออกแบบโมดูลโปรแกรมและเราสร้างลำดับชั้นของชั้นเรียน จากนั้นเราขยายคลาสบางคลาสที่สร้างคลาสที่ได้รับมา

เราต้องตรวจสอบให้แน่ใจว่าคลาสที่ได้รับใหม่จะขยายออกไปโดยไม่ต้องแทนที่ฟังก์ชันการทำงานของคลาสเก่า มิฉะนั้นคลาสใหม่สามารถสร้างเอฟเฟกต์ที่ไม่พึงประสงค์เมื่อใช้ในโมดูลโปรแกรมที่มีอยู่

หลักการทดแทนของ Liskov ระบุว่าหากโมดูลโปรแกรมกำลังใช้คลาสฐานดังนั้นการอ้างอิงถึงคลาสฐานสามารถถูกแทนที่ด้วยคลาส Derived โดยไม่มีผลกระทบต่อการทำงานของโมดูลโปรแกรม

ตัวอย่าง:

ด้านล่างเป็นตัวอย่างแบบคลาสสิกที่มีการละเมิดหลักการทดแทนของ Liskov ในตัวอย่างจะใช้ 2 คลาส: สี่เหลี่ยมผืนผ้าและสแควร์ สมมติว่ามีการใช้วัตถุสี่เหลี่ยมผืนผ้าในแอปพลิเคชัน เราขยายแอปพลิเคชันและเพิ่มคลาส Square คลาสสแควร์ถูกส่งคืนโดยรูปแบบจากโรงงานโดยขึ้นอยู่กับเงื่อนไขบางอย่างและเราไม่ทราบว่าวัตถุชนิดใดที่จะถูกส่งคืน แต่เรารู้ว่ามันเป็นรูปสี่เหลี่ยมผืนผ้า เราได้วัตถุสี่เหลี่ยมผืนผ้ากำหนดความกว้างเป็น 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.
    }
}

สรุป:

หลักการนี้เป็นเพียงส่วนขยายของ Open Open Principle และหมายความว่าเราต้องทำให้แน่ใจว่าคลาสที่ได้รับใหม่นั้นกำลังขยายคลาสพื้นฐานโดยไม่เปลี่ยนพฤติกรรม

ดูเพิ่มเติม: เปิดหลักการปิด

แนวคิดที่คล้ายกันบางอย่างสำหรับโครงสร้างที่ดีกว่า: การประชุมผ่านการกำหนดค่า


8

หลักการชดเชย Liskov

  • วิธีการแทนที่ไม่ควรว่างเปล่า
  • วิธีการแทนที่ไม่ควรเกิดข้อผิดพลาด
  • คลาสฐานหรือลักษณะการทำงานของอินเทอร์เฟซไม่ควรไปสำหรับการปรับเปลี่ยน (ทำใหม่) เป็นเพราะพฤติกรรมของคลาสที่ได้รับ

7

ภาคผนวกบางอย่าง:
ฉันสงสัยว่าทำไมไม่มีใครเขียนเกี่ยวกับ Invariant, preconditions และเงื่อนไขการโพสต์ของคลาสพื้นฐานที่ต้องปฏิบัติตามคลาสที่ได้รับมา เพื่อให้คลาส D ที่ได้รับนั้นมีความสมบูรณ์โดยคลาส B คลาส D ต้องเป็นไปตามเงื่อนไขบางประการ:

  • In-variants ของคลาสพื้นฐานต้องถูกรักษาไว้โดยคลาสที่ได้รับ
  • พรีเงื่อนไขของคลาสพื้นฐานจะต้องไม่ได้รับความเข้มแข็งโดยคลาสที่ได้รับ
  • โพสต์เงื่อนไขของคลาสฐานจะต้องไม่ถูกทำให้อ่อนแอโดยคลาสที่ได้รับ

ดังนั้นผู้ที่ได้รับจะต้องตระหนักถึงเงื่อนไขสามข้อที่กำหนดโดยคลาสพื้นฐาน ดังนั้นกฎของการพิมพ์ย่อยจะถูกตัดสินใจล่วงหน้า ซึ่งหมายความว่าความสัมพันธ์ 'IS A' จะถูกเชื่อฟังต่อเมื่อกฎบางอย่างเชื่อฟังโดยชนิดย่อยเท่านั้น กฎเหล่านี้ในรูปแบบของค่าคงที่, precoditions และ postcondition ที่ควรจะตัดสินใจโดยอย่างเป็นทางการ ' สัญญาการออกแบบ '

อภิปรายเพิ่มเติมเกี่ยวกับเรื่องนี้ได้ที่บล็อกของฉัน: หลักการทดแทน Liskov


6

LSP ในเงื่อนไขอย่างง่ายระบุว่าวัตถุของซูเปอร์คลาสเดียวกันควรสามารถสลับกันโดยไม่ทำลายอะไรเลย

ตัวอย่างเช่นถ้าเรามีCatและDogชั้นเรียนมาจากAnimalระดับฟังก์ชั่นใด ๆ โดยใช้ระดับสัตว์ควรจะสามารถที่จะใช้CatหรือDogและทำงานได้ตามปกติ


4

การใช้ ThreeDBoard ในแง่ของอาเรย์ของคณะกรรมการจะมีประโยชน์หรือไม่?

บางทีคุณอาจต้องการจัดการกับ ThreeDBoard ในระนาบต่าง ๆ เป็นคณะกรรมการ ในกรณีนี้คุณอาจต้องการแยกออกจากส่วนต่อประสาน (หรือคลาสนามธรรม) สำหรับบอร์ดเพื่อให้สามารถใช้งานได้หลายอย่าง

ในแง่ของอินเทอร์เฟซภายนอกคุณอาจต้องการแยกอินเทอร์เฟซแบบบอร์ดสำหรับทั้ง TwoDBoard และ ThreeDBoard (แม้ว่าจะไม่มีวิธีการใดที่เหมาะสม)


1
ฉันคิดว่าตัวอย่างเป็นเพียงการแสดงให้เห็นว่าการสืบทอดจากบอร์ดไม่สมเหตุสมผลในบริบทของ ThreeDBoard และลายเซ็นของวิธีการทั้งหมดไม่มีความหมายกับแกน Z
NotMyself

4

สี่เหลี่ยมเป็นสี่เหลี่ยมที่ความกว้างเท่ากับความสูง หากสี่เหลี่ยมจัตุรัสตั้งขนาดที่แตกต่างกันสองขนาดสำหรับความกว้างและความสูงมันละเมิดค่าคงที่สี่เหลี่ยม สิ่งนี้สามารถแก้ไขได้ด้วยการแนะนำผลข้างเคียง แต่ถ้าสี่เหลี่ยมมี setSize (ความสูงความกว้าง) ที่มีเงื่อนไข 0 <ความสูงและ 0 <ความกว้าง วิธีการชนิดย่อยที่ได้รับนั้นต้องการความสูง == ความกว้าง เงื่อนไขที่แข็งแกร่ง (และที่ละเมิด lsp) นี่แสดงให้เห็นว่าถึงแม้ว่ารูปสี่เหลี่ยมจะเป็นรูปสี่เหลี่ยมผืนผ้า แต่ไม่ใช่ชนิดย่อยที่ถูกต้องเนื่องจากเงื่อนไขมีความเข้มแข็ง การทำงานโดยรอบ (โดยทั่วไปเป็นสิ่งที่ไม่ดี) ทำให้เกิดผลข้างเคียงและทำให้สภาพโพสต์อ่อนลง (ซึ่งละเมิด lsp) setWidth บนฐานมีเงื่อนไขการโพสต์ 0 <ความกว้าง ความอ่อนแอที่ได้มานั้นมีความสูง == ความกว้าง

ดังนั้นสี่เหลี่ยมจัตุรัสที่ปรับขนาดได้ไม่ใช่สี่เหลี่ยมผืนผ้าที่ปรับขนาดได้


4

หลักการนี้ได้รับการแนะนำโดยBarbara Liskovในปี 1987 และขยายหลักการ Open-Closed โดยมุ่งเน้นไปที่พฤติกรรมของซูเปอร์คลาสและประเภทย่อย

ความสำคัญของมันจะชัดเจนเมื่อเราพิจารณาผลที่ตามมาจากการละเมิด พิจารณาแอปพลิเคชันที่ใช้คลาสต่อไปนี้

public class Rectangle 
{ 
  private double width;

  private double height; 

  public double Width 
  { 
    get 
    { 
      return width; 
    } 
    set 
    { 
      width = value; 
    }
  } 

  public double Height 
  { 
    get 
    { 
      return height; 
    } 
    set 
    { 
      height = value; 
    } 
  } 
}

ลองจินตนาการว่าวันหนึ่งลูกค้าต้องการความสามารถในการจัดการช่องสี่เหลี่ยมเพิ่มเติมจากสี่เหลี่ยม เนื่องจากจตุรัสเป็นสี่เหลี่ยมสี่เหลี่ยมผืนผ้าควรจะได้มาจากคลาสสี่เหลี่ยมผืนผ้า

public class Square : Rectangle
{
} 

อย่างไรก็ตามโดยการทำเช่นนั้นเราจะพบปัญหาสองประการ:

สี่เหลี่ยมจัตุรัสไม่จำเป็นต้องใช้ทั้งความสูงและความกว้างของตัวแปรที่สืบทอดมาจากสี่เหลี่ยมและสิ่งนี้อาจทำให้เกิดความสูญเสียอย่างมีนัยสำคัญในหน่วยความจำหากเราต้องสร้างวัตถุสี่เหลี่ยมนับแสน คุณสมบัติตัวตั้งค่าความกว้างและความสูงที่สืบทอดมาจากสี่เหลี่ยมนั้นไม่เหมาะสมสำหรับสี่เหลี่ยมจัตุรัสเนื่องจากความกว้างและความสูงของสี่เหลี่ยมเหมือนกัน ในการตั้งค่าทั้งความสูงและความกว้างเป็นค่าเดียวกันเราสามารถสร้างคุณสมบัติใหม่สองรายการดังนี้:

public class Square : Rectangle
{
  public double SetWidth 
  { 
    set 
    { 
      base.Width = value; 
      base.Height = value; 
    } 
  } 

  public double SetHeight 
  { 
    set 
    { 
      base.Height = value; 
      base.Width = value; 
    } 
  } 
}

ตอนนี้เมื่อใครบางคนจะตั้งค่าความกว้างของวัตถุสี่เหลี่ยมความสูงของมันจะเปลี่ยนตามและในทางกลับกัน

Square s = new Square(); 
s.SetWidth(1); // Sets width and height to 1. 
s.SetHeight(2); // sets width and height to 2. 

ลองก้าวไปข้างหน้าและพิจารณาฟังก์ชั่นอื่น ๆ นี้:

public void A(Rectangle r) 
{ 
  r.SetWidth(32); // calls Rectangle.SetWidth 
} 

หากเราส่งการอ้างอิงไปยังวัตถุทรงสี่เหลี่ยมในฟังก์ชั่นนี้เราจะละเมิด LSP เพราะฟังก์ชั่นนี้ไม่ทำงานสำหรับอนุพันธ์ของการขัดแย้ง ความกว้างและความสูงของคุณสมบัติไม่ใช่ polymorphic เนื่องจากไม่ได้ประกาศเสมือนในสี่เหลี่ยมผืนผ้า (วัตถุสี่เหลี่ยมจะเสียหายเนื่องจากความสูงจะไม่เปลี่ยนแปลง)

อย่างไรก็ตามด้วยการประกาศคุณสมบัติเซ็ตเตอร์ให้เสมือนจริงเราจะต้องเผชิญกับการละเมิดอีกครั้งหนึ่งคือ OCP ในความเป็นจริงการสร้างคลาสสแควร์ที่ได้รับนั้นก่อให้เกิดการเปลี่ยนแปลงในสี่เหลี่ยมผืนผ้าคลาสพื้นฐาน


3

คำอธิบายที่ชัดเจนที่สุดสำหรับ LSP ที่ฉันพบมาแล้วคือ "หลักการทดแทน Liskov กล่าวว่าวัตถุของคลาสที่ได้รับควรจะสามารถแทนที่วัตถุของคลาสพื้นฐานโดยไม่นำข้อผิดพลาดใด ๆ ในระบบหรือแก้ไขพฤติกรรมของคลาสพื้นฐาน "จากที่นี่ บทความแสดงตัวอย่างโค้ดสำหรับการละเมิด LSP และแก้ไข


1
โปรดระบุตัวอย่างของรหัสใน stackoverflow
sebenalern

3

สมมติว่าเราใช้สี่เหลี่ยมในรหัสของเรา

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 อาจทำให้เกิดข้อผิดพลาดในรหัสของคุณในบางจุด


3

LSP กล่าวว่า '' ควรเปลี่ยนวัตถุด้วย 'ประเภทย่อย' ' ในทางกลับกันหลักการนี้ชี้ไปที่

คลาสลูกไม่ควรทำลายนิยามชนิดของคลาส parent

และตัวอย่างต่อไปนี้ช่วยให้เข้าใจ 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();
}

2

ผมแนะนำให้คุณอ่านบทความ: การละเมิด Liskov ชดเชยหลักการ (LSP)

คุณสามารถหาคำอธิบายได้ว่าหลักการชดเชย Liskov คืออะไรเบาะแสทั่วไปที่ช่วยให้คุณเดาได้ว่าคุณละเมิดไปแล้วและเป็นตัวอย่างของวิธีการที่จะช่วยให้คุณทำให้ลำดับชั้นเรียนของคุณปลอดภัยมากขึ้น


2

หลักการแทน LISKOV (จากหนังสือ Mark Seemann) ระบุว่าเราควรจะสามารถแทนที่การใช้งานอินเทอร์เฟซหนึ่งด้วยอีกอันหนึ่งโดยไม่ทำลายลูกค้าหรือการติดตั้งมันเป็นหลักการนี้ที่ช่วยให้สามารถตอบสนองความต้องการที่เกิดขึ้นในอนาคต คาดการณ์พวกเขาในวันนี้

หากเราถอดปลั๊กคอมพิวเตอร์ออกจากผนัง (ใช้งาน) ไม่ได้เสียบปลั๊กผนัง (อินเทอร์เฟซ) หรือคอมพิวเตอร์ (ไคลเอนต์) พัง (อันที่จริงถ้าเป็นคอมพิวเตอร์แล็ปท็อปมันสามารถใช้แบตเตอรี่ได้เป็นระยะเวลาหนึ่ง) . อย่างไรก็ตามด้วยซอฟต์แวร์ลูกค้ามักคาดหวังว่าจะสามารถให้บริการได้ หากบริการถูกลบเราจะได้รับ NullReferenceException เพื่อจัดการกับสถานการณ์ประเภทนี้เราสามารถสร้างการใช้อินเทอร์เฟซที่ไม่ทำอะไรเลย นี่เป็นรูปแบบการออกแบบที่เรียกว่า Null Object [4] และมันสอดคล้องกับการถอดปลั๊กคอมพิวเตอร์ออกจากผนัง เนื่องจากเราใช้ข้อต่อหลวมเราสามารถแทนที่การใช้งานจริงด้วยสิ่งที่ไม่ทำอะไรเลยโดยไม่ทำให้เกิดปัญหา


2

หลักการการแทนที่ของ Likov ระบุว่าหากโมดูลโปรแกรมกำลังใช้คลาสฐานดังนั้นการอ้างอิงถึงคลาสพื้นฐานสามารถถูกแทนที่ด้วยคลาส Derived โดยไม่มีผลกระทบต่อการทำงานของโมดูลโปรแกรม

เจตนา - ประเภทที่ได้รับจะต้องสามารถทดแทนประเภทฐานได้อย่างสมบูรณ์

ตัวอย่าง - ชนิดส่งคืนค่าตัวแปรร่วมใน java


1

นี่คือข้อความที่ตัดตอนมาจากโพสต์นี้ที่อธิบายสิ่งต่าง ๆ อย่างชัดเจน:

[.. ] เพื่อที่จะเข้าใจหลักการบางอย่างมันเป็นเรื่องสำคัญที่จะต้องตระหนักเมื่อถูกละเมิด นี่คือสิ่งที่ฉันจะทำตอนนี้

การละเมิดหลักการนี้หมายความว่าอย่างไร หมายความว่าวัตถุไม่ปฏิบัติตามสัญญาที่กำหนดโดยนามธรรมซึ่งแสดงด้วยส่วนต่อประสาน กล่าวอีกนัยหนึ่งก็หมายความว่าคุณระบุสิ่งที่คุณคิดผิดไป

ลองพิจารณาตัวอย่างต่อไปนี้:

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” อย่างไรก็ตามมันจะไม่ - ตราบใดที่เด็กไม่ได้ละเมิดสัญญาของผู้ปกครอง

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.