LSP เทียบกับการทดแทน OCP / Liskov VS เปิดปิด


48

ฉันพยายามที่จะเข้าใจหลักการของ OOP และฉันได้ข้อสรุปว่า LSP และ OCP มีความคล้ายคลึงกัน (ถ้าไม่พูดมากกว่านี้)

หลักการเปิด / ปิดระบุ "เอนทิตีของซอฟต์แวร์ (คลาส, โมดูล, ฟังก์ชั่น, อื่น ๆ ) ควรจะเปิดสำหรับการขยาย แต่ปิดสำหรับการปรับเปลี่ยน"

ในคำง่ายๆ LSP ระบุว่าอินสแตนซ์ใด ๆ ของFooสามารถถูกแทนที่ด้วยอินสแตนซ์ใด ๆBarที่ได้มาจากFooและโปรแกรมจะทำงานในลักษณะเดียวกัน

ฉันไม่ใช่โปรแกรมเมอร์ OOP มืออาชีพ แต่สำหรับฉันแล้วดูเหมือนว่า LSP จะเป็นไปได้ก็ต่อเมื่อBarได้รับมาจากFooไม่ได้เปลี่ยนแปลงอะไรเลย แต่จะขยายออกไปเท่านั้น นั่นหมายความว่าโดยเฉพาะอย่างยิ่งโปรแกรม LSP จะเป็นจริงเฉพาะเมื่อ OCP เป็นจริงและ OCP เป็นจริงเฉพาะในกรณีที่ LSP เป็นจริง นั่นหมายความว่าพวกเขาเท่าเทียมกัน

ช่วยแก้ให้ด้วยนะถ้าฉันผิด. ฉันต้องการเข้าใจแนวคิดเหล่านี้จริงๆ ขอบคุณมากสำหรับคำตอบ


4
นี่เป็นการตีความที่แคบมากของแนวคิดทั้งสอง สามารถเปิด / ปิดได้ แต่ยังคงละเมิด LSP อยู่ ตัวอย่างสี่เหลี่ยมผืนผ้า / สแควร์หรือวงรี / วงกลมเป็นภาพประกอบที่ดี ทั้งสองปฏิบัติตาม OCP แต่ทั้งคู่ละเมิด LSP
Joel Etherton

1
โลก (หรืออย่างน้อยอินเทอร์เน็ต) สับสนในเรื่องนี้ kirkk.com/modularity/2009/12/solid-principles-of-class-design ผู้ชายคนนี้บอกว่าการละเมิด LSP ก็เป็นการละเมิด OCP ด้วย และในหนังสือ "การออกแบบวิศวกรรมซอฟต์แวร์: ทฤษฎีและการปฏิบัติ" ในหน้า 156 ผู้เขียนยกตัวอย่างของสิ่งที่ยึดติดกับ OCP แต่ละเมิด LSP ฉันยอมแพ้ต่อสิ่งนี้
Manoj R

@JoelEtherton คู่เหล่านั้นละเมิด LSP ก็ต่อเมื่อไม่แน่นอน ในกรณีที่ไม่เปลี่ยนรูปซึ่งสืบเนื่องมาSquareจากRectangleไม่ได้ละเมิด LSP (แต่มันอาจจะยังคงการออกแบบที่ไม่ดีในกรณีที่ไม่เปลี่ยนรูปเนื่องจากคุณสามารถมีRectangles ที่Squareไม่ตรงกับคณิตศาสตร์)
CodesInChaos

การเปรียบเทียบอย่างง่าย (จากมุมมองของผู้ใช้ผู้เขียนไลบรารี) LSP เปรียบเสมือนการขายผลิตภัณฑ์ (ไลบรารี่) ที่อ้างว่าใช้ 100% ของสิ่งที่กล่าว (บนอินเทอร์เฟซหรือคู่มือผู้ใช้) แต่จริงๆแล้วไม่ได้ (หรือไม่ตรงกับที่กล่าว) OCP เปรียบเสมือนการขายผลิตภัณฑ์ (ไลบรารี่) โดยสัญญาว่าจะสามารถอัปเกรดได้ (ขยาย) เมื่อมีฟังก์ชั่นใหม่ออกมา (เช่นเฟิร์มแวร์) แต่จริง ๆ แล้วไม่สามารถอัพเกรดได้หากไม่มีบริการจากโรงงาน
rwong

คำตอบ:


119

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

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

หลักการแบบเปิด (OCP)

ตามที่ลุงบ๊อบ :

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

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

ต้นฉบับมาจาก Bertrand Meyer ในปี 1988:

หน่วยงานซอฟต์แวร์ (คลาส, โมดูล, ฟังก์ชั่น, ฯลฯ ) ควรจะเปิดเพื่อขยาย แต่ปิดเพื่อการปรับเปลี่ยน

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

// Context is closed for modifications. Meaning you are
// not supposed to change the code here.
public class Context {

    // Context is however open for extension through
    // this private field
    private IBehavior behavior;

    // The context calls the behavior in this public 
    // method. If you want to change this you need
    // to implement it in the IBehavior object
    public void doStuff() {
        if (this.behavior != null)
            this.behavior.doStuff();
    }

    // You can dynamically set a new behavior at will
    public void setBehavior(IBehavior behavior) {
        this.behavior = behavior;
    }
}

// The extension point looks like this and can be
// subclassed/implemented
public interface IBehavior {
    public void doStuff();
}

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

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

"ชอบ" การจัดองค์ประกอบวัตถุ "เหนือ" การสืบทอดคลาส " (แก๊งสี่ 1995: 20)

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

public class HelloWorldBehavior implements IBehavior {
    public void doStuff() {
        System.println("Hello world!");
    }
}

public class GoodByeBehavior implements IBehavior {
    public void doStuff() {
        System.out.println("Good bye cruel world!");
    }
}

การใช้รูปแบบนี้เราสามารถปรับเปลี่ยนพฤติกรรมของบริบทที่รันไทม์ผ่านsetBehaviorวิธีการเป็นจุดส่วนขยาย

// in your main method
Context c = new Context();

c.setBehavior(new HelloWorldBehavior());
c.doStuff();
// prints out "Hello world!"

c.setBehavior(new GoodByeBehavior());
c.doStuff();
// prints out "Good bye cruel world!"

ดังนั้นเมื่อใดก็ตามที่คุณต้องการขยายคลาสบริบท "ปิด" ให้ทำโดยการทำคลาสย่อยโดยการพึ่งพาแบบ "เปิด" ซึ่งเป็นการทำงานร่วมกัน เห็นได้ชัดว่าไม่ใช่สิ่งเดียวกันกับการทำคลาสย่อยบริบทเอง แต่มันเป็น OCP LSP ไม่ได้กล่าวถึงเรื่องนี้เช่นกัน

การขยายด้วย Mixins แทนที่จะเป็นการสืบทอด

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

นี่คือตัวอย่าง javascript ของมิกซ์อินที่แสดงเทมเพลต HTML แบบง่ายสำหรับจุดยึด:

// The mixin, provides a template for anchor HTML elements, i.e. <a>
var LinkMixin = {
    render: function() {
        return '<a href="' + this.link +'">'
            + this.content 
            + '</a>;
    }
}

// Constructor for a youtube link
var YoutubeLink = function(content, youtubeId) {
    this.content = content;
    this.setLink(this.youtubeId);
};
// Methods are added to the prototype
YoutubeLink.prototype = {
    setLink: function(youtubeid) {
        this.link = 'http://www.youtube.com/watch?v=' + youtubeid;
    }
};
// Extend YoutubeLink prototype with the LinkMixin using
// underscore/lodash extend
_.extend(YoutubeLink.protoype, LinkMixin);

// When used:
var ytLink = new YoutubeLink("Cool Movie!", "idOaZpX8lnA");

console.log(ytLink.render());
// will output: 
// <a href="http://www.youtube.com/watch?=vidOaZpX8lnA">Cool Movie!</a>

แนวคิดคือการขยายวัตถุแบบไดนามิกและข้อดีของสิ่งนี้คือวัตถุอาจใช้วิธีการร่วมกันแม้ว่าพวกเขาจะอยู่ในโดเมนที่แตกต่างกันโดยสิ้นเชิง ในกรณีดังกล่าวข้างต้นคุณสามารถสร้างชนิดอื่น ๆ ของแองเคอ HTML LinkMixinโดยการขยายการดำเนินงานเฉพาะของคุณด้วย

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

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

_.extend(MyClass, Mixin1, Mixin2 /* [, ...] */);

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

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

ลุงบ๊อบนิยามโดย:

คลาสที่ได้รับมาต้องถูกแทนที่สำหรับคลาสพื้นฐานของพวกเขา

หลักการนี้เก่าแล้วอันที่จริงคำจำกัดความของลุงบ๊อบไม่ได้แยกความแตกต่างของหลักการที่ทำให้ LSP ยังคงเกี่ยวข้องกับ OCP อย่างใกล้ชิดโดยข้อเท็จจริงที่ว่าในตัวอย่างกลยุทธ์ข้างต้นจะใช้ supertype ชนิดเดียวกัน ( IBehavior) ดังนั้นให้ดูที่นิยามดั้งเดิมโดย Barbara Liskov และดูว่าเราสามารถหาข้อมูลอื่นเกี่ยวกับหลักการนี้ที่ดูเหมือนทฤษฎีบททางคณิตศาสตร์ได้หรือไม่:

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

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

  • จะต้องมีการคำนวณในลักษณะเดียวกัน
  • มีพฤติกรรมเหมือนกันและ
  • แตกต่างกันอย่างสิ้นเชิง

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

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

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

มีวิธีแก้ไข "การออกแบบที่ผิดพลาด" ขึ้นอยู่กับปัญหาจริง:

  • คลาส super ไม่ได้เรียกข้อกำหนดเบื้องต้นบังคับให้ผู้เรียกทำเช่นนั้นแทน
  • คลาส super ไม่มีเมธอดทั่วไปที่ผู้เรียกต้องการ

ทั้งสองอย่างนี้เป็น "ความผิดพลาด" ในการออกแบบรหัสทั่วไป มีคู่ของ refactorings ที่แตกต่างกันที่คุณสามารถทำเช่นนี้เป็นวิธีการดึงขึ้นหรือ refactor เป็นรูปแบบดังกล่าวในรูปแบบของผู้เข้าชม

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

public class Context {

    public void doStuff(string query) {

        // outcome no. 1
        if (query.Equals("Hello")) {
            System.out.println("Hello world!");
        } 

        // outcome no. 2
        else if (query.Equals("Bye")) {
            System.out.println("Good bye cruel world!");
        }

        // a change request may require another outcome...

    }

}

// usage:
Context c = new Context();

c.doStuff("Hello");
// prints "Hello world"

c.doStuff("Bye");
// prints "Bye"

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

public interface IVisitor {
    public bool canDo(string query);
    public void doStuff();
}

// outcome 1
public class HelloVisitor implements IVisitor {
    public bool canDo(string query) {
        return query.Equals("Hello");
    }
    public void doStuff() {
         System.out.println("Hello World");
    }
}

// outcome 2
public class ByeVisitor implements IVisitor {
    public bool canDo(string query) {
        return query.Equals("Bye");
    }
    public void doStuff() {
        System.out.println("Good bye cruel world");
    }
}

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

public class Context {
    private ArrayList<IVisitor> visitors = new ArrayList<IVisitor>();

    public Context() {
        visitors.add(new HelloVisitor());
        visitors.add(new ByeVisitor());
    }

    // instead of if-statements, go through all visitors
    // and use the canDo method to determine if the 
    // visitor object is the right one to "visit"
    public void doStuff(string query) {
        for(IVisitor visitor : visitors) {
            if (visitor.canDo(query)) {
                visitor.doStuff();
                break;
                // or return... it depends if you have logic 
                // after this foreach loop
            }
        }
    }

    // dynamically adds new visitors
    public void addVisitor(IVisitor visitor) {
        if (visitor != null)
            visitors.add(visitor);
    }
}

รูปแบบทั้งสองเป็นไปตาม OCP และ LSP อย่างไรก็ตามทั้งคู่ระบุสิ่งต่าง ๆ เกี่ยวกับพวกเขา ดังนั้นรหัสจะมีลักษณะอย่างไรถ้ามันละเมิดหนึ่งในหลักการ?

ละเมิดหลักการหนึ่ง แต่ปฏิบัติตามหลักการอื่น

มีวิธีที่จะทำลายหนึ่งในหลักการ แต่ก็ยังมีอื่น ๆ ตามมา ตัวอย่างด้านล่างดูเหมือนเป็นการประดิษฐ์ด้วยเหตุผลที่ดี แต่ฉันได้เห็นจริง ๆ แล้วเกิดขึ้นในรหัสการผลิต (และยิ่งแย่ลง):

เป็นไปตาม OCP แต่ไม่ใช่ LSP

ให้บอกว่าเรามีรหัสที่ได้รับ:

public interface IPerson {}

public class Boss implements IPerson {
    public void doBossStuff() { ... }
}

public class Peon implements IPerson {
    public void doPeonStuff() { ... }
}

public class Context {
    public Collection<IPerson> getPersons() { ... }
}

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

// in some routine that needs to do stuff with 
// a collection of IPerson:
Collection<IPerson> persons = context.getPersons();
for (IPerson person : persons) {
    // now we have to check the type... :-P
    if (person instanceof Boss) {
        ((Boss) person).doBossStuff();
    }
    else if (person instanceof Peon) {
        ((Peon) person).doPeonStuff();
    }
}

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

public class Boss implements IPerson {
    // we're adding this general method
    public void doStuff() {
        // that does the call instead
        this.doBossStuff();
    }
    public void doBossStuff() { ... }
}


public interface IPerson {
    // pulled up method from Boss
    public void doStuff();
}

// do the same for Peon

ประโยชน์ตอนนี้คือคุณไม่จำเป็นต้องรู้ประเภทที่แน่นอนอีกต่อไปติดตาม LSP:

// in some routine that needs to do stuff with 
// a collection of IPerson:
Collection<IPerson> persons = context.getPersons();
for (IPerson person : persons) {
    // yay, no type checking!
    person.doStuff();
}

ติดตาม LSP แต่ไม่ใช่ OCP

ให้ดูโค้ดบางตัวที่ตามหลัง LSP แต่ไม่ใช่ OCP มันเป็นสิ่งที่วางแผนไว้ แต่ทนกับฉันในเรื่องนี้มันเป็นความผิดพลาดเล็กน้อย:

public class LiskovBase {
    public void doStuff() {
        System.out.println("My name is Liskov");
    }
}

public class LiskovSub extends LiskovBase {
    public void doStuff() {
        System.out.println("I'm a sub Liskov!");
    }
}

public class Context {
    private LiskovBase base;

    // the good stuff
    public void doLiskovyStuff() {
        base.doStuff();
    }

    public void setBase(LiskovBase base) { this.base = base }
}

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

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

เราสามารถใช้รูปแบบวิธีการแม่แบบนี้ รูปแบบเมธอดเทมเพลตเป็นเรื่องธรรมดาในเฟรมเวิร์กที่คุณอาจใช้โดยไม่ทราบ (เช่นคอมโพเนนต์การแกว่งของ Java, รูปแบบ c # และส่วนประกอบ ฯลฯ ) นี่คือวิธีหนึ่งในการปิดdoStuffวิธีการปรับเปลี่ยนและตรวจสอบให้แน่ใจว่าปิดอยู่โดยทำเครื่องหมายด้วยfinalคำหลักของ java คำหลักนั้นป้องกันไม่ให้ผู้อื่นทำการคลาสย่อยเพิ่มเติม (ใน C # คุณสามารถใช้sealedเพื่อทำสิ่งเดียวกัน)

public class LiskovBase {
    // this is now a template method
    // the code that was duplicated
    public final void doStuff() {
        System.out.println(getStuffString());
    }

    // extension point, the code that "varies"
    // in LiskovBase and it's subclasses
    // called by the template method above
    // we expect it to be virtual and overridden
    public string getStuffString() {
        return "My name is Liskov";
    }
}

public class LiskovSub extends LiskovBase {
    // the extension overridden
    // the actual code that varied
    public string getStuffString() {
        return "I'm sub Liskov!";
    }
}

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

ข้อสรุป

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

โปรดทราบว่าการรวบรวมจากโค้ดตัวอย่างด้านบน:

  • OCP เป็นเรื่องเกี่ยวกับการล็อครหัสการทำงานลง แต่ยังคงเปิดไว้อย่างใดด้วยจุดส่วนขยายบางประเภท

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

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

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


7
นี่เป็นคำอธิบายที่ดีเพราะมันไม่ได้ทำให้ OCP เกินความจริงโดยนัยว่ามันหมายถึงการนำไปใช้โดยการสืบทอด มันเป็นเรื่องที่เกินความจริงที่เข้าร่วม OCP และ SRP ในใจของบางคนเมื่อจริงๆแล้วพวกเขาสามารถเป็นสองแนวคิดที่แยกจากกันโดยสิ้นเชิง
Eric King

5
นี่เป็นหนึ่งในคำตอบที่ดีที่สุดในการแลกเปลี่ยนสแต็คที่ฉันเคยเห็น ฉันหวังว่าฉันจะลงคะแนนได้ 10 ครั้ง ทำได้ดีมากและขอขอบคุณสำหรับคำอธิบายที่ดีเยี่ยม
บ๊อบฮอร์น

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

ในขณะที่คำพูดของคุณจาก Uncle Bob จาก LSP ถูกต้อง (เช่นเดียวกับเว็บไซต์ของเขา) มันควรจะเป็นวิธีอื่นหรือไม่? ไม่ควรระบุว่า "คลาสพื้นฐานควรใช้แทนคลาสที่ได้รับ" บน LSP การทดสอบ "ความเข้ากันได้" จะกระทำกับคลาสที่ได้รับไม่ใช่ฐานที่หนึ่ง ถึงกระนั้นฉันก็ไม่ใช่คนพูดภาษาอังกฤษและฉันคิดว่าอาจมีรายละเอียดบางอย่างเกี่ยวกับวลีที่ฉันอาจขาดหายไป
อัลฟ่า

@Alpha: นั่นเป็นคำถามที่ดี คลาสพื้นฐานจะถูกแทนที่ด้วยคลาสที่ได้รับเสมอมิฉะนั้นการสืบทอดจะไม่ทำงาน คอมไพเลอร์ (อย่างน้อยใน Java และ C #) จะบ่นว่าคุณกำลังออกจากสมาชิก (วิธีการหรือคุณสมบัติ / ฟิลด์) จากชั้นเรียนขยายที่ต้องดำเนินการ LSP มีไว้เพื่อป้องกันไม่ให้คุณเพิ่มวิธีการที่มีเฉพาะในเครื่องบนคลาสที่ได้รับเนื่องจากต้องการให้ผู้ใช้ของคลาสที่ได้รับนั้นต้องทราบเกี่ยวกับพวกเขา เมื่อโค้ดเติบโตขึ้นวิธีการดังกล่าวจะยากต่อการดูแลรักษา
Spoike

15

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

สิ่งที่ OCP พยายามแก้ไข

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

ปัญหาที่เกิดขึ้นนั้น

  1. มันเปลี่ยนการไหลของรหัสการทำงานที่มีอยู่
  2. มันบังคับให้มีการแตกสาขาแบบมีเงื่อนไขใหม่ในทุกกรณี ตัวอย่างเช่นสมมติว่าคุณมีรายชื่อหนังสือและมีหนังสือบางเล่มวางขายและคุณต้องการทำซ้ำทุกเล่มแล้วพิมพ์ราคาเช่นถ้าพวกเขาวางขายราคาที่พิมพ์จะรวมสตริง " (ลดราคา)".

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

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

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

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

แต่ไม่เคยกลัวที่จะวิเคราะห์บริบทและดูว่าข้อบกพร่องที่เกิดขึ้นกับเกินดุลประโยชน์เพราะแม้แต่หลักการเช่น OCP สามารถทำให้เป็นระเบียบ 20 ชั้นออกจากโปรแกรม, 20-line หากไม่ได้รับการรักษาอย่างระมัดระวัง

สิ่งที่ LSP พยายามแก้ไข

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

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

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

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

ตอนนี้คุณควรจะสามารถเข้าใจความหมายได้อย่างสมบูรณ์แบบ LSP กล่าวว่า: อย่าสืบทอดจากคลาสพื้นฐานและใช้งานฟังก์ชันในคลาสย่อยเหล่านั้นที่อื่น ๆ ซึ่งขึ้นอยู่กับคลาสพื้นฐานจะไม่เข้ากัน


1
ฉันลงทะเบียนเพื่อให้สามารถลงคะแนนนี้และคำตอบของ Spoike - งานที่ยอดเยี่ยม
David Culp

7

จากความเข้าใจของฉัน:

OCP กล่าวว่า: "หากคุณจะเพิ่มฟังก์ชันการทำงานใหม่ให้สร้างคลาสใหม่ที่ขยายคลาสที่มีอยู่แทนการเปลี่ยนแปลง"

LSP กล่าวว่า: "ถ้าคุณสร้างคลาสใหม่ที่ขยายคลาสที่มีอยู่ตรวจสอบให้แน่ใจว่าคลาสนั้นสามารถใช้ร่วมกับฐานของมันได้อย่างสมบูรณ์"

ดังนั้นฉันคิดว่าพวกเขาเติมเต็มซึ่งกันและกัน แต่พวกเขาไม่เท่ากัน


4

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

การปรับเปลี่ยนในส่วนที่เกี่ยวกับ OCP เป็นการกระทำทางกายภาพของรหัสการเขียนของนักพัฒนาในคลาสที่มีอยู่

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

ดังนั้นแม้ว่าพวกเขาอาจดูคล้ายกันจากระยะทาง OCP! = LSP ในความเป็นจริงฉันคิดว่าพวกเขาอาจเป็นเพียงหลักการ 2 ข้อที่ไม่สามารถเข้าใจได้ในแง่ของกันและกัน


2

ในคำง่ายๆ LSP ระบุว่าอินสแตนซ์ของ Foo ใด ๆ สามารถถูกแทนที่ด้วยอินสแตนซ์ใด ๆ ของบาร์ซึ่งมาจาก Foo โดยไม่สูญเสียฟังก์ชันการทำงานของโปรแกรม

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

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


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

0

เกี่ยวกับวัตถุที่อาจละเมิด

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

ใครอาจละเมิด LSP

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

ตัวอย่างที่ง่ายที่สุด:

class Container {
    // Should add the object to the container.
    void addObject(object) {
        internalArray.append(object);
    }

    int size() {
        return internalArray.size();
    }
}

class CustomContainer extends Container {
    @Override void addObject(object) {
        System.console.print("Skipping object! Ha-ha!");
    }
}

void fillWithRandomNumbers(Container container) {
    while (container.size() < 42) {
        container.addObject(Randomizer.getNumber())
    }
}

สัญญาระบุไว้อย่างชัดเจนว่าaddObjectควรผนวกการโต้แย้งลงในคอนเทนเนอร์ และCustomContainerทำลายสัญญานั้นอย่างชัดเจน ดังนั้นCustomContainer.addObjectฟังก์ชั่นนี้จึงละเมิด LSP ดังนั้นCustomContainerคลาสละเมิด LSP ผลที่สำคัญที่สุดคือCustomContainerไม่สามารถส่งผ่านไปfillWithRandomNumbers()ได้ ไม่สามารถแทนที่ด้วยContainerCustomContainer

โปรดทราบว่าเป็นจุดสำคัญมาก ไม่ใช่รหัสทั้งหมดที่แบ่ง LSP เป็นเฉพาะCustomContainer.addObjectและโดยทั่วไปCustomContainerที่ทำลาย LSP เมื่อคุณระบุว่ามีการละเมิด LSP คุณควรระบุสองสิ่งเสมอ:

  • หน่วยงานที่ละเมิด LSP
  • สัญญาที่ถูกทำลายโดยนิติบุคคล

แค่นั้นแหละ. เพียงแค่สัญญาและการดำเนินการ การลดลงในรหัสไม่ได้พูดอะไรเกี่ยวกับการละเมิด LSP

ใครอาจละเมิด OCP

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

ฟังดูซับซ้อน ลองตัวอย่างง่ายๆ:

enum Platform {
    iOS,
    Android
}

class PlatformDescriber {
    String describe(Platform platform) {
        switch (platform) {
            case iOS: return "iPhone OS, v10.0.1";
            case Android: return "Android, v7.1";
        }
    }
}

ชุดข้อมูลเป็นชุดของแพลตฟอร์มที่รองรับ PlatformDescriberเป็นส่วนประกอบที่จัดการค่าจากชุดข้อมูลนั้น PlatformDescriberเพิ่มแพลตฟอร์มใหม่ต้องมีการปรับปรุงรหัสที่มาของ ดังนั้นPlatformDescriberคลาสละเมิด OCP

ตัวอย่างอื่น:

class Shop {
    void sellItemToCustomer(item, customer) {
        // some buisiness logic here
        ...
        logger.logItemSold()
    }
}

class Logger {
    void logItemSold() {
        logger.logToStdErr("an item was sold")
        logger.logToRemote("an item was sold")
        logger.logToDatabase("an item was sold")
    }
}

"ชุดข้อมูล" คือชุดของช่องที่ควรเพิ่มรายการบันทึก Loggerเป็นองค์ประกอบที่รับผิดชอบในการเพิ่มรายการในทุกช่อง Loggerการเพิ่มการสนับสนุนสำหรับวิธีการในการเข้าสู่ระบบอีกต้องใช้การอัปเดตรหัสที่มาของ ดังนั้นLoggerคลาสละเมิด OCP

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

ผลักดันขีด จำกัด

ตอนนี้เป็นส่วนที่ยุ่งยาก เปรียบเทียบตัวอย่างด้านบนกับตัวอย่างต่อไปนี้:

enum GregorianWeekDay {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday
}

String translateToRussian(GregorianWeekDay weekDay) {
    switch (weekDay) {
        case Monday: return "Понедельник";
        case Tuesday: return "Вторник";
        case Wednesday: return "Среда";
        case Thursday: return "Четверг";
        case Friday: return "Пятница";
        case Saturday: return "Суббота";
        case Sunday: return "Воскресенье";
    }
}

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

ตอนนี้มันควรจะชัดเจนว่าswitchข้อความที่ใช้ไม่ได้นั้นบ่งบอกถึง OCP ที่เสียหายเสมอไป

ความแตกต่าง

ตอนนี้รู้สึกถึงความแตกต่าง:

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

เงื่อนไขเหล่านี้เป็นฉากที่สมบูรณ์

ตัวอย่าง

ในคำตอบของ @ Spoikeการละเมิดหลักการหนึ่ง แต่การทำตามส่วนอื่นนั้นผิดทั้งหมด

ในตัวอย่างแรกforส่วน -loop นั้นละเมิด OCP อย่างชัดเจนเนื่องจากไม่สามารถขยายได้หากไม่มีการแก้ไข แต่ไม่มีข้อบ่งชี้ว่ามีการละเมิด LSP และมันก็เป็นที่ชัดเจนไม่ได้ถ้าContextทำสัญญาอนุญาตให้ getPersons จะกลับอะไรยกเว้นหรือBoss Peonแม้จะสมมติว่าสัญญาที่อนุญาตให้IPersonส่งคืนคลาสย่อยใด ๆก็ไม่มีคลาสที่จะแทนที่เงื่อนไขการโพสต์นี้และละเมิด ยิ่งกว่านั้นถ้า getPersons จะส่งคืนอินสแตนซ์ของคลาสที่สามบางส่วนfor-loop จะทำงานของมันโดยไม่ล้มเหลว แต่ความจริงนั้นไม่มีส่วนเกี่ยวข้องกับ LSP

ต่อไป. ในตัวอย่างที่สองไม่ใช่ LSP และ OCP จะไม่ถูกละเมิด อีกครั้งContextส่วนที่ไม่มีส่วนเกี่ยวข้องกับ LSP - ไม่มีสัญญาที่กำหนดไม่มีการทำคลาสย่อยไม่มีการแทนที่ทับซ้อน ไม่ใช่Contextว่าใครควรเชื่อฟัง LSP LiskovSubไม่ควรทำผิดสัญญาฐานของมัน เกี่ยวกับ OCP คลาสปิดจริงหรือ - ใช่แล้ว. ไม่จำเป็นต้องทำการดัดแปลงเพื่อขยาย เห็นได้ชัดว่าชื่อของรัฐจุดนามสกุลทำสิ่งที่คุณต้องการไม่ จำกัด ตัวอย่างนี้ไม่มีประโยชน์ในชีวิตจริง แต่ชัดเจนว่าไม่ได้ละเมิด OCP

ลองทำตัวอย่างที่ถูกต้องโดยละเมิด OCP หรือ LSP จริง

ติดตาม OCP แต่ไม่ใช่ LSP

interface Platform {
    String name();
    String version();
}

class iOS implements Platform {
    @Override String name() { return "iOS"; }
    @Override String version() { return "10.0.1"; }
}

interface PlatformSerializer {
    String toJson(Platform platform);
}

class HumanReadablePlatformSerializer implements PlatformSerializer {
    String toJson(Platform platform) {
        return platform.name() + ", v" + platform.version();
    }
}

ที่นี่HumanReadablePlatformSerializerไม่ต้องการการแก้ไขใด ๆ เมื่อมีการเพิ่มแพลตฟอร์มใหม่ ดังนั้นจึงเป็นไปตาม OCP

แต่สัญญาต้องการให้toJsonส่งคืน JSON ที่จัดรูปแบบอย่างถูกต้อง ชั้นเรียนไม่ได้ทำอย่างนั้น เนื่องจากว่ามันไม่สามารถส่งผ่านไปยังส่วนประกอบที่ใช้PlatformSerializerในการจัดรูปแบบเนื้อหาของคำขอเครือข่าย ดังนั้นจึงHumanReadablePlatformSerializerเป็นการละเมิด LSP

ติดตาม LSP แต่ไม่ใช่ OCP

การแก้ไขตัวอย่างก่อนหน้านี้บางส่วน:

class Android implements Platform {
    @Override String name() { return "Android"; }
    @Override String version() { return "7.1"; }
}
class HumanReadablePlatformSerializer implements PlatformSerializer {
    String toJson(Platform platform) {
        return "{ "
                + "\"name\": \"" + platform.name() + "\","
                + "\"version\": \"" + platform.version() + "\","
                + "\"most-popular\": " + isMostPopular(platform) + ","
                + "}"
    }

    boolean isMostPopular(Platform platform) {
        return (platform instanceof Android)
    }
}

serializer ส่งคืนสตริง JSON ในรูปแบบที่ถูกต้อง ดังนั้นไม่มีการละเมิด LSP ในที่นี่

แต่มีข้อกำหนดว่าถ้าส่วนใหญ่จะใช้แพลตฟอร์มแล้วควรมีข้อบ่งชี้ที่สอดคล้องกันใน JSON ในตัวอย่างนี้ OCP ถูกละเมิดโดยHumanReadablePlatformSerializer.isMostPopularฟังก์ชั่นเพราะสักวันหนึ่ง iOS กลายเป็นแพลตฟอร์มยอดนิยม อย่างเป็นทางการหมายความว่าชุดของแพลตฟอร์มที่ใช้มากที่สุดถูกกำหนดเป็น "Android" ในตอนนี้และisMostPopularจัดการชุดข้อมูลนั้นไม่เพียงพอ ชุดข้อมูลไม่คงที่ความหมายและอาจเปลี่ยนแปลงได้อย่างอิสระตลอดเวลา HumanReadablePlatformSerializerต้องมีการอัปเดตซอร์สโค้ดในกรณีที่มีการเปลี่ยนแปลง

คุณอาจสังเกตเห็นการละเมิดความรับผิดชอบเดี่ยวในตัวอย่างนี้ ฉันตั้งใจทำเพื่อที่จะสามารถแสดงให้เห็นถึงหลักการทั้งสองในเอนทิตีเรื่องเดียวกัน ในการแก้ไขปัญหา SRP คุณอาจแยกisMostPopularฟังก์ชั่นบางส่วนภายนอกและเพิ่มพารามิเตอร์เพื่อHelper PlatformSerializer.toJsonแต่นั่นเป็นอีกเรื่อง


0

LSP และ OCP ไม่เหมือนกัน

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

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

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

ฉันจะให้หลักฐานที่เป็นทางการมากขึ้น: การพูดว่า "LSP บอกเป็นนัยถึง OCP" จะหมายถึงเดลต้า (เพราะ OCP ต้องการสิ่งอื่นนอกเหนือจากในกรณีเล็กน้อย) แต่ LSP ไม่ต้องการสิ่งใดเลย เพื่อให้เป็นเท็จอย่างชัดเจน ในทางกลับกันเราสามารถหักล้าง "OCP บอกเป็นนัย LSP" เพียงแค่บอกว่า OCP เป็นคำสั่งเกี่ยวกับ deltas ดังนั้นจึงไม่พูดอะไรเลยเกี่ยวกับคำสั่งเหนือโปรแกรมในสถานที่ ที่ตามมาจากความจริงที่ว่าคุณสามารถสร้างเดลต้าใด ๆ ที่เริ่มต้นด้วยโปรแกรมใด ๆ ในสถานที่ พวกเขาเป็นอิสระอย่างเต็มที่


-1

ฉันจะดูจากมุมมองของลูกค้า ถ้าลูกค้าใช้คุณสมบัติของส่วนต่อประสานและภายในนั้นมีการใช้งานคุณลักษณะดังกล่าวโดย Class A สมมติว่ามีคลาส B ซึ่งขยายคลาส A ในวันพรุ่งนี้ถ้าฉันลบคลาส A ออกจากอินเตอร์เฟสนั้นและใส่คลาส B ดังนั้นคลาส B ควร ยังให้คุณสมบัติเดียวกันกับลูกค้า ตัวอย่างมาตรฐานคือคลาส Duck ที่ว่ายน้ำและหาก ToyDuck ขยาย Duck แล้วก็ควรว่ายน้ำและไม่บ่นว่าไม่สามารถว่ายน้ำไม่เช่นนั้น ToyDuck ไม่ควรขยายคลาส Duck


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

สิ่งนี้ดูเหมือนจะไม่นำเสนออะไรมากมายเกินกว่าที่ทำคะแนนและอธิบายไว้ในคำตอบก่อนหน้า 6 ข้อ
gnat

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