ฉันได้ยินมาว่าหลักการแทน Liskov (LSP) เป็นหลักการพื้นฐานของการออกแบบเชิงวัตถุ มันคืออะไรและมีตัวอย่างของการใช้งานอะไรบ้าง?
ฉันได้ยินมาว่าหลักการแทน Liskov (LSP) เป็นหลักการพื้นฐานของการออกแบบเชิงวัตถุ มันคืออะไรและมีตัวอย่างของการใช้งานอะไรบ้าง?
คำตอบ:
ตัวอย่างที่ดีที่แสดงให้เห็นถึง LSP (ที่ลุงบ๊อบเขียนในพอดคาสต์ที่ฉันได้ยินเมื่อเร็ว ๆ นี้) เป็นสิ่งที่บางครั้งเสียงที่ถูกต้องในภาษาธรรมชาตินั้นไม่ค่อยได้ผลในโค้ด
ในทางคณิตศาสตร์ที่เป็นSquare
Rectangle
แน่นอนมันเป็นความเชี่ยวชาญของรูปสี่เหลี่ยมผืนผ้า "เป็น" ทำให้คุณต้องการสร้างโมเดลด้วยการสืบทอด แต่ถ้าในรหัสคุณที่ทำSquare
มาจากRectangle
นั้นควรจะใช้งานได้ทุกที่ที่คุณคาดหวังSquare
Rectangle
สิ่งนี้ทำให้เกิดพฤติกรรมแปลก ๆ
ลองนึกภาพคุณมีSetWidth
และSetHeight
วิธีการในRectangle
ชั้นฐานของคุณ ดูเหมือนว่าจะมีเหตุผลอย่างสมบูรณ์แบบ แต่ถ้าคุณRectangle
อ้างอิงชี้ไปที่Square
แล้วSetWidth
และSetHeight
ไม่ได้ทำให้รู้สึกเพราะการตั้งค่าหนึ่งจะเปลี่ยนการอื่น ๆ เพื่อให้ตรงกับมัน ในกรณีนี้Square
การทดสอบการทดแทน Liskov ล้มเหลวด้วยRectangle
และสิ่งที่เป็นนามธรรมของการSquare
สืบทอดมาRectangle
นั้นเป็นสิ่งที่ไม่ดี
Y'all ควรตรวจสอบอื่น ๆ ล้ำค่าโปสเตอร์ SOLID หลักการสร้างแรงบันดาลใจ
Square.setWidth(int width)
ได้ดำเนินการเช่นนี้this.width = width; this.height = width;
? ในกรณีนี้รับประกันได้ว่าความกว้างเท่ากับความสูง
หลักการชดเชย Liskov (LSP, LSP) เป็นแนวคิดในการเขียนโปรแกรมเชิงวัตถุที่ระบุ:
ฟังก์ชันที่ใช้พอยน์เตอร์หรือการอ้างอิงไปยังคลาสพื้นฐานต้องสามารถใช้วัตถุของคลาสที่ได้รับโดยไม่ต้องรู้
หัวใจสำคัญของ LSP นั้นเกี่ยวกับส่วนต่อประสานและสัญญารวมถึงวิธีการตัดสินใจว่าจะขยายชั้นเรียนเมื่อเทียบกับการใช้กลยุทธ์อื่นเช่นองค์ประกอบเพื่อให้บรรลุเป้าหมาย
วิธีที่มีประสิทธิภาพมากที่สุดที่ฉันได้เห็นเพื่อแสดงให้เห็นถึงจุดนี้อยู่ในหัวแรก OOA & D พวกเขานำเสนอสถานการณ์ที่คุณเป็นนักพัฒนาในโครงการเพื่อสร้างกรอบสำหรับเกมกลยุทธ์
พวกเขานำเสนอชั้นเรียนที่แสดงถึงคณะกรรมการที่มีลักษณะเช่นนี้:
ทุกวิธีการใช้พิกัด X และ Y Tiles
เป็นพารามิเตอร์ในการค้นหาตำแหน่งกระเบื้องในอาร์เรย์สองมิติของ สิ่งนี้จะช่วยให้ผู้พัฒนาเกมสามารถจัดการยูนิตในบอร์ดระหว่างการแข่งขัน
หนังสือเล่มนี้มีการเปลี่ยนแปลงข้อกำหนดเพื่อบอกว่างานเฟรมเกมนั้นต้องสนับสนุนบอร์ดเกม 3 มิติเพื่อรองรับเกมที่มีเที่ยวบิน ดังนั้นระดับเป็นที่รู้จักที่ขยาย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
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{}
Bird bird
แต่สิ่งที่คุณจะทำอย่างไรถ้าลูกค้ามี คุณต้องโยนวัตถุเพื่อ FlyingBirds เพื่อใช้ประโยชน์จากการบินซึ่งไม่ดีใช่มั้ย
Bird bird
ว่าไม่สามารถใช้งานfly()
ได้ แค่นั้นแหละ. การผ่าน a Duck
จะไม่เปลี่ยนความจริงนี้ หากลูกค้ามีFlyingBirds bird
แล้วแม้ว่าจะได้รับDuck
มันก็ควรทำงานในลักษณะเดียวกัน
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)
}
อย่างไรก็ตามคงที่นี้จะต้องถูกละเมิดโดยการดำเนินการที่ถูกต้องของมันจึงไม่ได้เป็นตัวแทนที่ถูกต้องSquare
Rectangle
โรเบิร์ตมาร์ตินมีดีกระดาษบน 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
ฟังก์ชั่น ฟังก์ชั่นเหล่านี้ไม่เหมาะสมอย่างยิ่งสำหรับ aSquare
เนื่องจากความกว้างและความสูงของจตุรัสเหมือนกัน นี่ควรเป็นเงื่อนงำสำคัญที่มีปัญหากับการออกแบบ อย่างไรก็ตามมีวิธีการหลีกเลี่ยงปัญหา เราสามารถแทนที่SetWidth
และSetHeight
[... ]แต่พิจารณาฟังก์ชั่นต่อไปนี้:
void f(Rectangle& r) { r.SetWidth(32); // calls Rectangle::SetWidth }
หากเราส่งการอ้างอิงไปยัง
Square
วัตถุในฟังก์ชั่นนี้Square
วัตถุจะเสียหายเนื่องจากความสูงจะไม่เปลี่ยนแปลง นี่เป็นการละเมิด LSP ที่ชัดเจน ฟังก์ชั่นไม่ทำงานสำหรับอนุพันธ์ของข้อโต้แย้ง[ ... ]
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
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และความขัดแย้งของรัสเซลนำมาใช้กับการขยายได้อย่างไร
การตีความของทฤษฎีบทเหล่านี้รวมไว้ในการทำความเข้าใจแนวความคิดทั่วไปของพลังเอนโทรปี:
ฉันเห็นสี่เหลี่ยมและสี่เหลี่ยมจัตุรัสในทุกคำตอบและวิธีการละเมิด 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 !
}
}
ตอนนี้ชนิดย่อยไม่สามารถใช้วิธีเดียวกันได้เนื่องจากพวกมันไม่ได้ผลลัพธ์ที่เหมือนกันอีกต่อไป
Database::selectQuery
การสนับสนุนเพียงส่วนย่อยของ SQL ที่ได้รับการสนับสนุนโดยโปรแกรมฐานข้อมูลทั้งหมด มันใช้งานไม่ได้จริง ๆ ... ที่กล่าวมาตัวอย่างยังเข้าใจได้ง่ายกว่าที่คนอื่นใช้กันที่นี่
มีรายการตรวจสอบเพื่อพิจารณาว่าคุณละเมิด Liskov หรือไม่
รายการตรวจสอบ:
ข้อ จำกัด ด้านประวัติ : เมื่อแทนที่เมธอดคุณจะไม่ได้รับอนุญาตให้แก้ไขคุณสมบัติที่ไม่สามารถแก้ไขได้ในคลาสพื้นฐาน ลองดูรหัสเหล่านี้และคุณจะเห็นชื่อถูกกำหนดให้ไม่สามารถแก้ไขได้ (ชุดส่วนตัว) แต่ประเภทย่อยแนะนำวิธีการใหม่ที่ช่วยให้สามารถแก้ไขได้ (ผ่านการสะท้อน):
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 เป็นกฎเกี่ยวกับสัญญาของ 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 บนวัตถุฐานตราบใดที่อาร์กิวเมนต์ยังคงเหมือนเดิม
2 + "2"
) บางทีคุณอาจสับสน "พิมพ์อย่างยิ่ง" กับ "พิมพ์แบบคงที่" หรือไม่?
ยาวสั้นเรื่องขอปล่อยให้สี่เหลี่ยมสี่เหลี่ยมและสี่เหลี่ยมสี่เหลี่ยมตัวอย่างในทางปฏิบัติเมื่อการขยายชั้นเรียนพ่อแม่คุณมีทั้งรักษาผู้ปกครอง 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/
ฟังก์ชันที่ใช้พอยน์เตอร์หรือการอ้างอิงไปยังคลาสพื้นฐานต้องสามารถใช้วัตถุของคลาสที่ได้รับโดยไม่ต้องรู้
เมื่อฉันอ่านเกี่ยวกับ LSP ครั้งแรกฉันคิดว่านี่เป็นความหมายที่เข้มงวดมากโดยเฉพาะอย่างยิ่งการเปรียบเทียบกับการใช้อินเตอร์เฟสและการคัดเลือกนักพิมพ์ที่ปลอดภัย ซึ่งจะหมายถึงว่า LSP นั้นมั่นใจได้หรือไม่ว่าจะเป็นภาษาของตัวเอง ตัวอย่างเช่นในแง่ที่เข้มงวดนี้ ThreeDBoard สามารถทดแทนบอร์ดได้อย่างแน่นอนเท่าที่คอมไพเลอร์เกี่ยวข้อง
หลังจากอ่านแนวคิดเพิ่มเติมแม้ว่าฉันพบว่า LSP โดยทั่วไปจะตีความกว้างกว่านั้น
กล่าวโดยย่อความหมายของรหัสไคลเอนต์คือ "รู้" ว่าวัตถุที่อยู่หลังตัวชี้นั้นเป็นประเภทที่ได้รับแทนที่จะเป็นประเภทตัวชี้นั้นไม่ได้ จำกัด อยู่ที่ความปลอดภัยของประเภท การยึดติดกับ LSP ยังสามารถทดสอบได้ผ่านการตรวจสอบพฤติกรรมการทำงานจริงของวัตถุ นั่นคือการตรวจสอบผลกระทบของสถานะของวัตถุและวิธีการขัดแย้งกับผลของการเรียกวิธีการหรือประเภทของข้อยกเว้นที่ถูกโยนออกมาจากวัตถุ
กลับไปที่ตัวอย่างอีกครั้งในทางทฤษฎีแล้ววิธีการของบอร์ดสามารถทำงานได้ดีบน ThreeDBoard อย่างไรก็ตามในทางปฏิบัติมันเป็นเรื่องยากมากที่จะป้องกันความแตกต่างในพฤติกรรมที่ลูกค้าอาจไม่สามารถจัดการได้อย่างถูกต้องโดยไม่รบกวนการทำงานที่ ThreeDBoard ตั้งใจจะเพิ่ม
ด้วยความรู้นี้ในมือการประเมินการยึดมั่นของ LSP จึงเป็นเครื่องมือที่ยอดเยี่ยมในการพิจารณาว่าการจัดวางองค์ประกอบเป็นกลไกที่เหมาะสมกว่าสำหรับการขยายฟังก์ชั่นที่มีอยู่มากกว่าการสืบทอด
ฉันเดาว่าทุกคนจะเข้าใจว่า LSP คืออะไรในทางเทคนิค: โดยทั่วไปคุณต้องการที่จะสรุปรายละเอียดย่อยและใช้ supertypes อย่างปลอดภัย
Liskov มีกฎพื้นฐาน 3 ข้อ:
กฎลายเซ็น: ควรมีการใช้งานที่ถูกต้องของทุกการดำเนินการของ supertype ในประเภทย่อย syntactically คอมไพเลอร์จะสามารถตรวจสอบคุณได้ มีกฎเล็กน้อยเกี่ยวกับการโยนข้อยกเว้นให้น้อยลงและเข้าถึงอย่างน้อยที่สุดเท่าที่จะทำได้โดยใช้วิธี supertype
กฎวิธีการ: การปฏิบัติงานของการดำเนินการเหล่านั้นมีความหมายทางเสียง
คุณสมบัติกฎ: สิ่งนี้นอกเหนือไปจากการเรียกใช้ฟังก์ชั่นของแต่ละบุคคล
คุณสมบัติเหล่านี้ทั้งหมดจะต้องได้รับการเก็บรักษาไว้และฟังก์ชันย่อยพิเศษไม่ควรละเมิดคุณสมบัติของซุปเปอร์ประเภท
หากสามสิ่งนี้ได้รับการดูแลคุณจะต้องแยกออกจากสิ่งที่อยู่ข้างใต้และคุณกำลังเขียนโค้ดคู่ที่หลวม
ที่มา: การพัฒนาโปรแกรมใน Java - Barbara Liskov
ตัวอย่างที่สำคัญของการใช้งานของ LSP อยู่ในการทดสอบซอฟต์แวร์
ถ้าฉันมีคลาส A ซึ่งเป็นคลาสย่อยที่สอดคล้องกับ LSP ของ B ดังนั้นฉันสามารถใช้ชุดการทดสอบของ B เพื่อทดสอบ A อีกครั้ง
หากต้องการทดสอบคลาสย่อย A อย่างสมบูรณ์ฉันอาจต้องเพิ่มกรณีทดสอบอีกสองสามกรณี แต่อย่างน้อยที่สุดฉันสามารถใช้กรณีทดสอบของ Superclass B ทั้งหมดอีกครั้ง
วิธีที่จะทำให้รู้ได้คือการสร้างสิ่งที่ McGregor เรียกว่า "ลำดับชั้นแบบขนานสำหรับการทดสอบ": ATest
ชั้นเรียนของฉันจะรับช่วงBTest
ต่อ การฉีดบางรูปแบบนั้นจำเป็นต้องมีเพื่อให้แน่ใจว่ากรณีทดสอบสามารถทำงานกับวัตถุประเภท A แทนประเภท B (รูปแบบวิธีการเทมเพลตอย่างง่ายจะทำ)
โปรดทราบว่าการนำชุดการทดสอบพิเศษมาใช้ใหม่สำหรับการปรับใช้คลาสย่อยทั้งหมดในความเป็นจริงเป็นวิธีการทดสอบว่าการปรับใช้คลาสย่อยเหล่านี้สอดคล้องกับ LSP ดังนั้นหนึ่งสามารถยืนยันว่าควรรันชุดทดสอบ superclass ในบริบทของคลาสย่อยใด ๆ
ดูคำตอบสำหรับคำถาม Stackoverflow " ฉันสามารถใช้ชุดการทดสอบที่ใช้ซ้ำได้เพื่อทดสอบการใช้งานของส่วนต่อประสาน "
มาอธิบายใน 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() { ... }
}
การกำหนด LSP นี้แข็งแกร่งเกินไป:
ถ้าสำหรับวัตถุแต่ละชนิด o1 ของ S มีวัตถุ o2 ของชนิด T เช่นนั้นสำหรับโปรแกรมทั้งหมดที่ P ในแง่ของ T พฤติกรรมของ P จะไม่เปลี่ยนแปลงเมื่อ o1 ถูกแทนสำหรับ o2 ดังนั้น S เป็นประเภทย่อยของ T
ซึ่งโดยทั่วไปหมายถึงว่า S เป็นอีกสิ่งหนึ่งที่มีการห่อหุ้มอย่างสมบูรณ์ในสิ่งเดียวกันกับ T และฉันอาจกล้าที่จะตัดสินใจว่าประสิทธิภาพเป็นส่วนหนึ่งของพฤติกรรมของ P ...
ดังนั้นโดยทั่วไปแล้วการใช้งานของการผูกมัดล่าช้าจะเป็นการละเมิด LSP เป็นจุดรวมของ OO ที่จะได้รับพฤติกรรมที่แตกต่างกันเมื่อเราแทนที่วัตถุชนิดหนึ่งเป็นอีกประเภทหนึ่ง!
สูตรที่อ้างถึงโดยวิกิพีเดียดีกว่าเนื่องจากคุณสมบัติขึ้นอยู่กับบริบทและไม่จำเป็นต้องรวมถึงพฤติกรรมทั้งหมดของโปรแกรม
ในประโยคที่ง่ายมากเราสามารถพูดได้:
ชั้นเรียนของเด็กจะต้องไม่ละเมิดลักษณะชั้นฐานของมัน มันจะต้องมีความสามารถกับมัน เราสามารถพูดได้ว่ามันเหมือนกับ subtyping
หลักการทดแทนของ 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 และหมายความว่าเราต้องทำให้แน่ใจว่าคลาสที่ได้รับใหม่นั้นกำลังขยายคลาสพื้นฐานโดยไม่เปลี่ยนพฤติกรรม
ดูเพิ่มเติม: เปิดหลักการปิด
แนวคิดที่คล้ายกันบางอย่างสำหรับโครงสร้างที่ดีกว่า: การประชุมผ่านการกำหนดค่า
หลักการชดเชย Liskov
ภาคผนวกบางอย่าง:
ฉันสงสัยว่าทำไมไม่มีใครเขียนเกี่ยวกับ Invariant, preconditions และเงื่อนไขการโพสต์ของคลาสพื้นฐานที่ต้องปฏิบัติตามคลาสที่ได้รับมา เพื่อให้คลาส D ที่ได้รับนั้นมีความสมบูรณ์โดยคลาส B คลาส D ต้องเป็นไปตามเงื่อนไขบางประการ:
ดังนั้นผู้ที่ได้รับจะต้องตระหนักถึงเงื่อนไขสามข้อที่กำหนดโดยคลาสพื้นฐาน ดังนั้นกฎของการพิมพ์ย่อยจะถูกตัดสินใจล่วงหน้า ซึ่งหมายความว่าความสัมพันธ์ 'IS A' จะถูกเชื่อฟังต่อเมื่อกฎบางอย่างเชื่อฟังโดยชนิดย่อยเท่านั้น กฎเหล่านี้ในรูปแบบของค่าคงที่, precoditions และ postcondition ที่ควรจะตัดสินใจโดยอย่างเป็นทางการ ' สัญญาการออกแบบ '
อภิปรายเพิ่มเติมเกี่ยวกับเรื่องนี้ได้ที่บล็อกของฉัน: หลักการทดแทน Liskov
LSP ในเงื่อนไขอย่างง่ายระบุว่าวัตถุของซูเปอร์คลาสเดียวกันควรสามารถสลับกันโดยไม่ทำลายอะไรเลย
ตัวอย่างเช่นถ้าเรามีCat
และDog
ชั้นเรียนมาจากAnimal
ระดับฟังก์ชั่นใด ๆ โดยใช้ระดับสัตว์ควรจะสามารถที่จะใช้Cat
หรือDog
และทำงานได้ตามปกติ
การใช้ ThreeDBoard ในแง่ของอาเรย์ของคณะกรรมการจะมีประโยชน์หรือไม่?
บางทีคุณอาจต้องการจัดการกับ ThreeDBoard ในระนาบต่าง ๆ เป็นคณะกรรมการ ในกรณีนี้คุณอาจต้องการแยกออกจากส่วนต่อประสาน (หรือคลาสนามธรรม) สำหรับบอร์ดเพื่อให้สามารถใช้งานได้หลายอย่าง
ในแง่ของอินเทอร์เฟซภายนอกคุณอาจต้องการแยกอินเทอร์เฟซแบบบอร์ดสำหรับทั้ง TwoDBoard และ ThreeDBoard (แม้ว่าจะไม่มีวิธีการใดที่เหมาะสม)
สี่เหลี่ยมเป็นสี่เหลี่ยมที่ความกว้างเท่ากับความสูง หากสี่เหลี่ยมจัตุรัสตั้งขนาดที่แตกต่างกันสองขนาดสำหรับความกว้างและความสูงมันละเมิดค่าคงที่สี่เหลี่ยม สิ่งนี้สามารถแก้ไขได้ด้วยการแนะนำผลข้างเคียง แต่ถ้าสี่เหลี่ยมมี setSize (ความสูงความกว้าง) ที่มีเงื่อนไข 0 <ความสูงและ 0 <ความกว้าง วิธีการชนิดย่อยที่ได้รับนั้นต้องการความสูง == ความกว้าง เงื่อนไขที่แข็งแกร่ง (และที่ละเมิด lsp) นี่แสดงให้เห็นว่าถึงแม้ว่ารูปสี่เหลี่ยมจะเป็นรูปสี่เหลี่ยมผืนผ้า แต่ไม่ใช่ชนิดย่อยที่ถูกต้องเนื่องจากเงื่อนไขมีความเข้มแข็ง การทำงานโดยรอบ (โดยทั่วไปเป็นสิ่งที่ไม่ดี) ทำให้เกิดผลข้างเคียงและทำให้สภาพโพสต์อ่อนลง (ซึ่งละเมิด lsp) setWidth บนฐานมีเงื่อนไขการโพสต์ 0 <ความกว้าง ความอ่อนแอที่ได้มานั้นมีความสูง == ความกว้าง
ดังนั้นสี่เหลี่ยมจัตุรัสที่ปรับขนาดได้ไม่ใช่สี่เหลี่ยมผืนผ้าที่ปรับขนาดได้
หลักการนี้ได้รับการแนะนำโดย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 ในความเป็นจริงการสร้างคลาสสแควร์ที่ได้รับนั้นก่อให้เกิดการเปลี่ยนแปลงในสี่เหลี่ยมผืนผ้าคลาสพื้นฐาน
คำอธิบายที่ชัดเจนที่สุดสำหรับ LSP ที่ฉันพบมาแล้วคือ "หลักการทดแทน Liskov กล่าวว่าวัตถุของคลาสที่ได้รับควรจะสามารถแทนที่วัตถุของคลาสพื้นฐานโดยไม่นำข้อผิดพลาดใด ๆ ในระบบหรือแก้ไขพฤติกรรมของคลาสพื้นฐาน "จากที่นี่ บทความแสดงตัวอย่างโค้ดสำหรับการละเมิด LSP และแก้ไข
สมมติว่าเราใช้สี่เหลี่ยมในรหัสของเรา
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 อาจทำให้เกิดข้อผิดพลาดในรหัสของคุณในบางจุด
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();
}
ผมแนะนำให้คุณอ่านบทความ: การละเมิด Liskov ชดเชยหลักการ (LSP)
คุณสามารถหาคำอธิบายได้ว่าหลักการชดเชย Liskov คืออะไรเบาะแสทั่วไปที่ช่วยให้คุณเดาได้ว่าคุณละเมิดไปแล้วและเป็นตัวอย่างของวิธีการที่จะช่วยให้คุณทำให้ลำดับชั้นเรียนของคุณปลอดภัยมากขึ้น
หลักการแทน LISKOV (จากหนังสือ Mark Seemann) ระบุว่าเราควรจะสามารถแทนที่การใช้งานอินเทอร์เฟซหนึ่งด้วยอีกอันหนึ่งโดยไม่ทำลายลูกค้าหรือการติดตั้งมันเป็นหลักการนี้ที่ช่วยให้สามารถตอบสนองความต้องการที่เกิดขึ้นในอนาคต คาดการณ์พวกเขาในวันนี้
หากเราถอดปลั๊กคอมพิวเตอร์ออกจากผนัง (ใช้งาน) ไม่ได้เสียบปลั๊กผนัง (อินเทอร์เฟซ) หรือคอมพิวเตอร์ (ไคลเอนต์) พัง (อันที่จริงถ้าเป็นคอมพิวเตอร์แล็ปท็อปมันสามารถใช้แบตเตอรี่ได้เป็นระยะเวลาหนึ่ง) . อย่างไรก็ตามด้วยซอฟต์แวร์ลูกค้ามักคาดหวังว่าจะสามารถให้บริการได้ หากบริการถูกลบเราจะได้รับ NullReferenceException เพื่อจัดการกับสถานการณ์ประเภทนี้เราสามารถสร้างการใช้อินเทอร์เฟซที่ไม่ทำอะไรเลย นี่เป็นรูปแบบการออกแบบที่เรียกว่า Null Object [4] และมันสอดคล้องกับการถอดปลั๊กคอมพิวเตอร์ออกจากผนัง เนื่องจากเราใช้ข้อต่อหลวมเราสามารถแทนที่การใช้งานจริงด้วยสิ่งที่ไม่ทำอะไรเลยโดยไม่ทำให้เกิดปัญหา
หลักการการแทนที่ของ Likov ระบุว่าหากโมดูลโปรแกรมกำลังใช้คลาสฐานดังนั้นการอ้างอิงถึงคลาสพื้นฐานสามารถถูกแทนที่ด้วยคลาส Derived โดยไม่มีผลกระทบต่อการทำงานของโมดูลโปรแกรม
เจตนา - ประเภทที่ได้รับจะต้องสามารถทดแทนประเภทฐานได้อย่างสมบูรณ์
ตัวอย่าง - ชนิดส่งคืนค่าตัวแปรร่วมใน java
นี่คือข้อความที่ตัดตอนมาจากโพสต์นี้ที่อธิบายสิ่งต่าง ๆ อย่างชัดเจน:
[.. ] เพื่อที่จะเข้าใจหลักการบางอย่างมันเป็นเรื่องสำคัญที่จะต้องตระหนักเมื่อถูกละเมิด นี่คือสิ่งที่ฉันจะทำตอนนี้
การละเมิดหลักการนี้หมายความว่าอย่างไร หมายความว่าวัตถุไม่ปฏิบัติตามสัญญาที่กำหนดโดยนามธรรมซึ่งแสดงด้วยส่วนต่อประสาน กล่าวอีกนัยหนึ่งก็หมายความว่าคุณระบุสิ่งที่คุณคิดผิดไป
ลองพิจารณาตัวอย่างต่อไปนี้:
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” อย่างไรก็ตามมันจะไม่ - ตราบใดที่เด็กไม่ได้ละเมิดสัญญาของผู้ปกครอง