LSP vs OCP / Liskov 교체 VS Open Close


48

나는 OOP의 SOLID 원칙을 이해하려고 노력하고 있으며 LSP와 OCP는 비슷한 점이 더 많다는 결론에 도달했습니다.

개방 / 폐쇄 원칙은 "소프트웨어 엔티티 (클래스, 모듈, 기능 등)는 확장을 위해 개방되어야하지만 수정을 위해 폐쇄되어야한다"고 명시하고 있습니다.

간단히 말해서 LSP는 Foo모든 인스턴스 Bar가 파생 된 인스턴스로 대체 될 수 있으며 Foo프로그램은 동일한 방식으로 작동 한다고 명시합니다 .

나는 프로 OOP 프로그래머가 아니지만 LSP는 Bar에서 파생 된 Foo것이 아무것도 변경되지 않고 확장 만 가능한 경우에만 가능 합니다. 즉, 특히 프로그램 LSP는 OCP가 true 일 때만 true이고 LCP가 true 일 때만 OCP가 true임을 의미합니다. 그것은 그들이 동등하다는 것을 의미합니다.

틀린 점 있으면 지적 해주세요. 나는이 아이디어들을 정말로 이해하고 싶다. 답변 주셔서 감사합니다.


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를 위반하지 않습니다. (당신이 광장 가질 수 있기 때문에 그러나 그것은 나쁜 디자인은 불변의 경우 여전히 아마 Rectangle하지의 Square수학 일치하지 않는)
CodesInChaos

간단한 유추 (라이브러리 라이터 사용자 관점에서). LSP는 (인터페이스 또는 사용자 매뉴얼에서) 말한 내용의 100 %를 구현한다고 주장하는 제품 (라이브러리)을 판매하는 것과 같지만 실제로는 그렇지 않습니다 (또는 말한 것과 일치하지 않음). OCP는 새로운 기능 (예 : 펌웨어)이 나오면 업그레이드 (확장) 될 수 있지만 실제로는 공장 서비스 없이는 업그레이드 할 수 없음을 약속하는 제품 (라이브러리)을 판매하는 것과 같습니다.
rwong

답변:


119

OCP와 LSP에 대한 이상한 오해가 있고 일부는 일부 용어와 혼란스러운 예가 일치하지 않기 때문에 발생합니다. 동일한 방식으로 구현할 경우 두 원칙은 "동일한 것"입니다. 패턴은 일반적으로 거의 예외없이 몇 가지 방식으로 원칙을 따릅니다.

차이점은 아래에서 자세히 설명하지만 먼저 원칙 자체를 살펴 보겠습니다.

공개 원칙 (OCP)

밥 아저씨 에 따르면 :

클래스 동작을 수정하지 않고 확장 할 수 있어야합니다.

이 경우 extend 라는 단어 가 반드시 새로운 동작이 필요한 실제 클래스를 서브 클래스해야한다는 것을 의미하지는 않습니다. 용어의 불일치가 처음 언급 된 것을 참조하십시오. 키워드 extend는 Java에서 서브 클래 싱 만 의미하지만 원칙은 Java보다 오래되었습니다.

원본은 1988 년 Bertrand Meyer에서 나왔습니다.

소프트웨어 엔터티 (클래스, 모듈, 함수 등)는 확장을 위해 열려 있지만 수정을 위해 닫아야합니다.

여기서 원칙이 소프트웨어 엔티티에 적용되는 것이 훨씬 더 명확합니다 . 나쁜 예는 확장 지점을 제공하는 대신 코드를 완전히 수정하면서 소프트웨어 엔터티를 재정의하는 것입니다. 소프트웨어 엔티티 자체의 동작은 확장 가능해야하며 이것의 좋은 예는 전략 패턴의 구현입니다 (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잠겨 있습니다. 대부분의 프로그래머는 클래스를 확장하기 위해 클래스를 서브 클래스로 만들고 싶을 것입니다. 그러나 여기서는 인터페이스 를 구현하는 모든 것을 통해 동작이 변경 될 수 있다고 가정하지 않습니다 IBehavior.

즉, 컨텍스트 클래스는 수정을 위해 닫히지 만 확장을 위해 열려 있습니다 . 상속 대신 객체 구성을 사용하여 동작을 수행하기 때문에 실제로 다른 기본 원칙을 따릅니다.

" ' 클래스 상속 ' 보다 ' 좋아하는' 객체 구성 '." (Gang of Four 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는 이것에 대해서도 언급하지 않습니다.

상속 대신 믹스 인으로 확장

서브 클래 싱 이외의 OCP를 수행하는 다른 방법이 있습니다. 한 가지 방법은 mixin을 사용하여 클래스를 확장 할 수 있도록하는 것입니다 . 이것은 예를 들어 클래스 기반이 아닌 프로토 타입 기반의 언어에서 유용합니다. 아이디어는 필요에 따라 더 많은 메소드 나 속성으로 동적 객체를 수정하는 것입니다. 즉, 다른 객체와 혼합 또는 "혼합"하는 객체입니다.

다음은 앵커에 대한 간단한 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와 관련하여 "mixins"는 확장입니다. 위의 예에서 YoutubeLink소프트웨어 엔터티는 수정을 위해 닫히지 만 믹스 인을 사용하여 확장을 위해 열립니다. 객체 계층 구조가 평평 해져 유형을 확인할 수 없습니다. 그러나 이것은 실제로 나쁜 것은 아니며 유형을 확인하는 것은 일반적으로 나쁜 생각이며 다형성으로 아이디어를 깨뜨린다는 것을 더 자세히 설명하겠습니다.

대부분의 extend구현에서 여러 객체를 혼합 할 수 있으므로이 메서드를 사용하여 여러 상속을 수행 할 수 있습니다.

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

명심해야 할 것은 이름을 충돌시키지 않는 것입니다. 즉, 믹스 인은 재정의 될 때와 같은 일부 속성 또는 메소드의 이름을 정의합니다. 겸손한 경험에서 이것은 문제가 아니며 발생하면 결함이있는 디자인을 나타냅니다.

리스 코프의 대체 원칙 (LSP)

Bob 아저씨는 간단히 다음과 같이 정의합니다.

파생 클래스는 기본 클래스로 대체 가능해야합니다.

이 원칙은 오래되었지만 사실 Bob 아저씨의 정의는 위의 전략 예에서 동일한 수퍼 타입이 사용된다는 사실에 의해 LSP가 OCP와 여전히 밀접하게 관련되어 있다는 원칙을 차별화하지 않습니다 IBehavior. 따라서 Barbara Liskov의 원래 정의를 살펴보고 수학적 정리처럼 보이는이 원리에 대해 다른 것을 찾을 수 있는지 확인하십시오.

여기에서 원하는 것은 다음과 같은 대체 속성 같은 것입니다 : 각 개체의 경우 o1유형의 S객체가 o2유형은 T모든 프로그램에 대한 있도록 P측면에서 정의 T의 행동이 P때 변경되지 않습니다 o1대체됩니다 o2다음 S의 하위 유형입니다 T.

클래스를 전혀 언급하지 않기 때문에 잠시 동안이 문제를 해결하십시오. JavaScript에서는 명시 적으로 클래스 기반이 아니더라도 실제로 LSP를 따를 수 있습니다. 프로그램에 최소한 다음 두 가지 JavaScript 객체 목록이있는 경우 :

  • 같은 방식으로 계산해야합니다
  • 같은 행동을하고
  • 그렇지 않으면 어떤 식 으로든 완전히 다른 것입니다

그런 다음 객체는 동일한 "유형"을 갖는 것으로 간주되며 실제로 프로그램에는 중요하지 않습니다. 이것은 본질적으로 다형성 입니다. 일반적인 의미에서; 인터페이스를 사용하는 경우 실제 하위 유형을 알 필요가 없습니다. OCP는 이에 대해 명시 적으로 언급하지 않습니다. 또한 대부분의 초보자 프로그래머가하는 설계 실수를 정확하게 지적합니다.

객체의 하위 유형을 확인하려는 충동을 느낄 때마다 잘못했을 가능성이 큽니다.

좋아, 그래서 모든 시간을 잘못하지 않을 수 있지만, 당신은 몇 가지 할 수있는 충동이있는 경우 유형 검사 와 함께 instanceof또는 열거 형을, 당신은 필요 이상으로 자신을 위해 좀 더 복잡한 프로그램을 수행 할 수 있습니다. 그러나 항상 그런 것은 아닙니다. 솔루션이 충분히 작 으면 무자비한 리팩터링 을 연습 하면 변경 사항이 필요할 때 개선 될 수 있습니다.

실제 문제에 따라이 "디자인 실수"를 해결할 수있는 방법이 있습니다.

  • 수퍼 클래스는 전제 조건을 호출하지 않으므로 호출자가 대신해야합니다.
  • 수퍼 클래스에 호출자가 필요로하는 일반 메소드가 누락되었습니다.

둘 다 일반적인 코드 디자인 "실수"입니다. 풀업 방법 또는 방문자 패턴 과 같은 패턴으로 리팩토링 과 같이 수행 할 수있는 여러 가지 리팩토링이 있습니다 .

나는 실제로 if 문 스파게티를 처리 할 수 ​​있으며 기존 코드에서 생각하는 것보다 구현하는 것이 더 간단하므로 방문자 패턴을 많이 좋아합니다. 다음과 같은 맥락이 있다고 가정 해 봅시다.

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");
    }
}

이 시점에서 프로그래머가 방문자 패턴에 대해 알지 못하면 대신 Context 클래스를 구현하여 특정 유형인지 확인합니다. 방문자 클래스에는 부울 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를 따르지만 둘 다 서로 다른 점을 찾아냅니다. 그렇다면 코드가 원칙 중 하나를 위반하면 어떻게 보일까요?

한 원칙을 위반하지만 다른 원칙을 따름

원칙 중 하나를 어기는 방법이 있지만 다른 규칙을 따르십시오. 아래 예제는 좋은 이유로 생각했지만 실제로는 프로덕션 코드에서 이러한 팝업이 나타나는 것을 보았습니다.

LCP가 아닌 OCP를 따릅니다.

주어진 코드가 있다고 가정 해 봅시다.

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

컨텍스트는 실제 유형을 몰라도 LiskovBase를 사용할 수 있기 때문에 코드는 LSP를 수행합니다. 이 코드도 OCP를 따른다고 생각하지만 면밀히 살펴보면 클래스가 실제로 닫혀 있습니까? doStuff메소드가 단순히 행을 인쇄하는 것 이상을 수행 한 경우 어떻게 합니까?

OCP를 따르는 경우의 대답은 단순히 : NO 입니다.이 객체 디자인에서 코드를 다른 것으로 완전히 재정의해야하기 때문은 아닙니다. 기본 클래스에서 코드를 복사하여 작업을 수행해야하므로 잘라 내기 및 붙여 넣기 웜 캔이 열립니다. doStuff방법은 확실히 연장 열린 상태이지만 완전히 수정 폐쇄되지 않았습니다.

이것에 템플릿 메소드 패턴 을 적용 할 수 있습니다 . 템플릿 메소드 패턴은 프레임 워크에서 너무 일반적이므로이를 모르는 상태에서 사용했을 수도 있습니다 (예 : Java 스윙 구성 요소, C # 양식 및 구성 요소 등). 다음은 doStuff수정을 위해 메소드 를 닫고 java의 final키워드 로 표시하여 닫히는 방법입니다 . 이 키워드는 모든 사람이 클래스를 더 이상 서브 클래 싱하지 못하게합니다 (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를 따르며 어리석은 것처럼 보이지만 더 많은 코드를 사용하여 확장했다고 상상해보십시오. 하위 클래스가 모든 것을 완전히 재정의하고 재정의 된 코드가 구현 사이에 잘려 붙여지는 프로덕션 환경에서 코드가 계속 배포되는 것을 계속보고 있습니다. 작동하지만 모든 코드 복제와 마찬가지로 유지 관리 악몽을위한 설정입니다.

결론

이 모든 것이 OCP와 LSP에 관한 몇 가지 질문과 이들의 차이점 / 유사성을 없애기를 바랍니다. 동일하게 무시하는 것은 쉽지만 위의 예는 그렇지 않다는 것을 보여 주어야합니다.

위의 샘플 코드에서 수집하면 다음과 같습니다.

  • OCP는 작업 코드를 잠그는 것에 관한 것이지만 여전히 어떤 종류의 확장 점으로 계속 열어 둡니다.

    이는 템플릿 메소드 패턴의 예와 같이 변경되는 코드를 캡슐화하여 코드 중복을 피하기위한 것입니다. 또한 변경 사항을 깨는 것이 고통스럽기 때문에 빠르게 실패 할 수 있습니다 (예 : 한 곳을 변경하고 다른 곳에서 중단). 유지 관리를 위해 변경 캡슐화 개념은 변경이 항상 발생 하기 때문에 좋은 것 입니다.

  • LSP는 사용자가 실제 유형을 확인하지 않고 수퍼 유형을 구현하는 다른 객체를 처리하도록하는 것입니다. 이것은 본질적으로 다형성 에 관한 것입니다.

    이 원칙은 유형 검사 및 유형 변환을 수행 할 수있는 대안을 제공합니다.이 유형은 유형의 수가 증가함에 따라 손을 can 수 있으며 풀업 리팩토링 또는 방문자와 같은 패턴을 적용하여 달성 할 수 있습니다.


7
이것은 상속에 의한 구현을 항상 의미한다는 것을 의미하여 OCP를 지나치게 단순화하지 않기 때문에 좋은 설명입니다. 두 사람이 완전히 분리 된 개념 일 수있는 일부 사람들의 마음에 OCP와 SRP를 결합시키는 것은 지나치게 단순화 된 것입니다.
Eric King

5
이것은 내가 본 것 중 최고의 스택 교환 답변 중 하나입니다. 나는 그것을 10 번 공표 할 수 있으면 좋겠다. 훌륭한 설명을 해주셔서 감사합니다.
Bob Horn

거기에서 클래스 기반 프로그래밍 언어는 아니지만 여전히 LSP를 따르고 텍스트를 편집하여 더 유창하게 읽을 수 있도록 Javascript에 대한 설명을 추가했습니다. 휴!
Spoike

LSP의 Bob Uncle의 인용문은 정확하지만 (웹 사이트와 동일) 다른 방법으로 사용해서는 안됩니까? "기본 클래스는 파생 클래스를 대신 할 수 있어야합니다"라고 말해서는 안됩니까? LSP에서 "호환성"테스트는 기본 클래스가 아닌 파생 클래스에 대해 수행됩니다. 아직도, 나는 영어를 모국어로 사용하는 사람이 아니며 내가 빠뜨릴 수있는 문구에 대한 세부 사항이있을 수 있다고 생각합니다.
Alpha

@Alpha : 좋은 질문입니다. 기본 클래스는 항상 파생 클래스로 대체 가능합니다. 그렇지 않으면 상속이 작동하지 않습니다. 구현 해야하는 확장 클래스에서 멤버 (메소드 또는 속성 / 필드)를 제외하면 컴파일러 (적어도 Java 및 C #)가 불평합니다. LSP는 파생 클래스에서 로컬로만 사용할 수있는 메서드를 추가하지 못하도록하기 위해 파생 클래스의 사용자가 해당 클래스를 알아야합니다. 코드가 커짐에 따라 그러한 방법을 유지하기가 어려울 것입니다.
Spoike

15

이것은 많은 혼란을 야기하는 것입니다. 나는 이러한 원리들을 철학적으로 고려하는 것을 선호한다. 왜냐하면 그것들에 대한 많은 다른 예들이 있기 때문이며 때로는 구체적인 예들이 그들의 본질을 완전히 포착하지 못한다.

OCP가 고치려고하는 것

주어진 프로그램에 기능을 추가해야한다고 가정 해 봅시다. 특히 절차 적으로 생각하도록 훈련받은 사람들에게 가장 쉬운 방법은 if ​​절을 추가하는 것입니다.

그 문제는

  1. 기존 작업 코드의 흐름을 변경합니다.
  2. 모든 경우에 새로운 조건 분기를 강제합니다. 예를 들어 책 목록이 있고 그 중 일부가 판매 중이고 모든 책을 반복하고 가격을 인쇄하려고한다고 가정하면 인쇄 된 가격에 " (판매 중) ".

당신은 "is_on_sale"라는 이름의 모든 책에 추가 필드를 추가하여이 작업을 수행 할 수 있고, 어떤 책의 가격을 인쇄 할 때 당신은 그 필드를 확인하실 수 있습니다, 또는 양자 택일로 , 당신은 인쇄 다른 유형을 사용하여 데이터베이스에서의 판매 책을 인스턴스화 할 수 있습니다 가격 문자열에서 "(판매 중)"(완벽한 디자인은 아니지만 포인트를 제공함)

첫 번째 절차 적 솔루션의 문제점은 각 책에 대한 추가 필드이며 많은 경우에 여분의 중복 복잡성입니다. 두 번째 솔루션은 실제로 필요한 경우에만 논리를 적용합니다.

이제 서로 다른 데이터와 논리가 필요한 경우가 많을 수 있다는 점을 고려하고 클래스를 디자인하거나 요구 사항의 변화에 ​​대응하는 동안 OCP를 염두에 두는 것이 좋은 아이디어임을 알게 될 것입니다.

이제 주요 아이디어를 얻어야합니다. 절차 상 수정이 아니라 새로운 코드를 다형성 확장으로 구현할 수있는 상황에 처해보십시오.

그러나 OCP와 같은 원칙조차도 신중하게 다루지 않으면 20 라인 프로그램에서 20 클래스를 망칠 수 있기 때문에 상황을 분석하고 단점이 이점을 능가하는지 확인하는 것을 두려워하지 마십시오 .

LSP가 고치려고하는 것

우리 모두는 코드 재사용을 좋아합니다. 뒤 따르는 질병은 많은 프로그램이 코드를 완전히 이해하지 못한다는 것입니다. 공통 코드 라인을 맹목적으로 고려하여 몇 줄의 코드 이외의 모듈 사이에서 읽을 수없는 복잡성과 중복적인 긴밀한 결합을 만들기 만합니다. 개념적으로해야 할 일이 진행되는 한 공통점이 없습니다.

이것의 가장 큰 예는 인터페이스 재사용 입니다. 당신은 아마 그것을 직접 목격했을 것입니다. 클래스는 인터페이스의 논리적 구현 (또는 구체적인 기본 클래스의 경우 확장)이 아니라 인터페이스를 구현하지만 그 시점에서 선언되는 메소드는 관련있는 한 올바른 서명을 갖습니다.

그러나 문제가 발생합니다. 클래스가 선언 한 메소드의 시그니처를 고려하여 인터페이스를 구현하는 경우 하나의 개념적 기능에서 완전히 다른 기능을 요구하는 장소로 클래스의 인스턴스를 전달할 수 있으며, 비슷한 시그니처에만 의존합니다.

그것은 끔찍하지는 않지만 많은 혼란을 초래하며, 우리는 이러한 실수를 저지르는 기술을 보유하고 있습니다. 우리가해야 할 일은 인터페이스를 API + 프로토콜 로 취급하는 것입니다 . API는 선언에서 명백하며 프로토콜은 기존 인터페이스 사용에서 명백합니다. 동일한 API를 공유하는 2 개의 개념적 프로토콜이있는 경우 2 개의 다른 인터페이스로 표현해야합니다. 그렇지 않으면 우리는 DRY 교리에 빠지고 아이러니하게도 코드를 유지하기가 더 어려워집니다.

이제 정의를 완벽하게 이해할 수 있어야합니다. LSP의 말 : 기본 클래스에서 상속받지 말고 기본 클래스에 의존하는 다른 곳에서는 잘 어울리지 않는 하위 클래스에서 기능을 구현하지 마십시오.


1
나는 이것과 Spoike의 답변에 투표 할 수 있도록 서명했습니다.
David Culp 2016 년

7

내 이해에서 :

OCP는 "새로운 기능을 추가 할 경우 기존 클래스를 변경하지 않고 확장하는 새로운 클래스를 만듭니다"라고 말합니다.

LSP는 "기존 클래스를 확장하는 새 클래스를 만들면 기본 클래스와 완전히 호환 가능한지 확인하십시오."라고 말합니다.

그래서 나는 그들이 서로 보완한다고 생각하지만 평등하지 않습니다.


4

OCP와 LSP가 모두 수정과 관련이있는 것은 사실이지만, OCP가 말하는 수정의 종류는 LSP가 말하는 것이 아닙니다.

OCP와 관련하여 수정 하는 것은 기존 클래스에서 코드작성 하는 개발자의 실제 작업입니다 .

LSP는 파생 클래스가 기본 클래스와 비교 한 동작 수정 및 수퍼 클래스 대신 서브 클래스를 사용하여 발생할 수있는 프로그램 실행 의 런타임 변경을 처리합니다.

따라서 거리 OCP! = LSP와 비슷하게 보일 수 있습니다. 실제로 나는 이것이 서로의 관점에서 이해 될 수없는 유일한 2 가지 SOLID 원칙 일 수 있다고 생각합니다.


2

간단히 말해서 LSP는 Foo의 모든 인스턴스가 프로그램 기능의 손실없이 Foo에서 파생 된 Bar의 인스턴스로 대체 될 수 있다고 말합니다.

이것은 잘못이다. LSP는 Bar가 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()입니다. Container로 대체 할 수 없습니다 CustomContainer.

매우 중요한 점을 명심하십시오. LSP를 중단시키는 것은이 전체 코드가 아니며, 구체적 CustomContainer.addObject이고 일반적으로 CustomContainerLSP를 중단시키는 것입니다. 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 "Воскресенье";
    }
}

translateToRussianOCP를 위반한다고 생각할 수 있습니다 . 그러나 실제로는 그렇지 않습니다. GregorianWeekDay정확한 이름으로 정확히 7 주일로 제한됩니다. 중요한 것은 이러한 한계가 시간이 지남에 따라 의미 적으로 변경 될 수 없다는 것입니다. gregorian week에는 항상 7 일이 있습니다. 월요일, 화요일 등은 항상 있습니다.이 데이터 세트는 의미 상 고정되어 있습니다. translateToRussian의 소스 코드를 수정해야하는 것은 불가능합니다 . 따라서 OCP가 위반되지 않습니다.

이제 소진 switch진술이 항상 OCP 고장을 나타내는 것은 아님을 분명히해야합니다 .

차이점

이제 차이점을 느끼십시오.

  • LSP의 주제는 "인터페이스 / 계약 구현"입니다. 구현이 계약을 준수하지 않으면 LSP가 중단됩니다. 확장 가능한지 여부에 따라 구현이 시간이 지남에 따라 변경 될 수 있는지 여부는 중요하지 않습니다.
  • OCP의 주제는 "요구 사항 변경에 대응하는 방법"입니다. 새로운 유형의 데이터를 지원하기 위해 해당 데이터를 처리하는 구성 요소의 소스 코드를 변경해야하는 경우 해당 구성 요소가 OCP를 중단합니다. 구성 요소가 계약을 위반하는지 여부는 중요하지 않습니다.

이러한 조건은 완전히 직교합니다.

에서 @ Spoike의 대답 하나 개의 원칙을 위반 그러나 다른 다음과 같은 부분은 완전히 잘못된 것입니다.

첫 번째 예에서 for-loop 부분은 수정없이 확장 할 수 없으므로 OCP를 분명히 위반합니다. 그러나 LSP 위반의 징후는 없습니다. 그리고 Context계약이 getPersons가 Boss또는 이외의 것을 반환하도록 허용하는지 여부는 명확하지 않습니다 Peon. IPerson하위 클래스를 반환 할 수있는 계약을 가정하더라도이 사후 조건을 무시하고 위반하는 클래스는 없습니다. 또한 getPersons가 일부 세 번째 클래스의 인스턴스를 리턴하면 -loop는 for실패없이 작업을 수행합니다. 그러나 그 사실은 LSP와 관련이 없습니다.

다음. 두 번째 예에서는 LSP 나 OCP가 모두 위반되지 않습니다. 다시 말하지만, Context부품은 LSP와 아무런 관련이 없습니다. 정의 된 계약이없고, 서브 클래 싱이없고, 우선 적용이 없습니다. ContextLSP에 복종해야하는 사람 은 아니며 , 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네트워크 요청 본문을 형식화 하는 데 사용되는 구성 요소로 전달할 수 없습니다 . 따라서 HumanReadablePlatformSerializerLSP를 위반합니다.

OSP가 아닌 LSP를 따르십시오

이전 예의 일부 수정 사항 :

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

시리얼 라이저는 올바른 형식의 JSON 문자열을 반환합니다. 따라서 여기에 LSP 위반이 없습니다.

그러나 플랫폼이 가장 많이 사용되는 경우 JSON에 해당 표시가 있어야한다는 요구 사항이 있습니다. 이 예에서 HumanReadablePlatformSerializer.isMostPopular언젠가 iOS가 가장 인기있는 플랫폼이되기 때문에 OCP는 기능에 위배됩니다 . 공식적으로 이는 가장 많이 사용되는 플랫폼 세트가 현재 "Android"로 정의되어 isMostPopular해당 데이터 세트를 부적절하게 처리 함을 의미합니다. 데이터 세트는 의미 적으로 고정되어 있지 않으며 시간이 지남에 따라 자유롭게 변경 될 수 있습니다. HumanReadablePlatformSerializer변경시 소스 코드를 업데이트해야합니다.

이 예에서 단일 책임 위반이 있음을 알 수 있습니다. 나는 같은 주제의 실체에 대해 두 원칙을 모두 보여줄 수 있도록 의도적으로 만들었습니다. SRP를 수정하려면 isMostPopular함수를 외부로 추출하고 Helper에 매개 변수를 추가하십시오 PlatformSerializer.toJson. 그러나 그것은 또 다른 이야기입니다.


0

LSP와 OCP는 동일하지 않습니다.

LSP는 프로그램의 정확성에 대해 이야기 는 의미로 . 하위 유형의 인스턴스가 상위 유형의 코드로 대체 될 때 프로그램 정확성을 깨뜨리는 경우 LSP 위반을 시연 한 것입니다. 이것을 보여주기 위해 테스트를 조롱해야 할 수도 있지만 기본 코드베이스를 변경할 필요는 없습니다. 프로그램 자체의 유효성을 검사하여 LSP를 충족하는지 확인합니다.

OCP 는 한 소스 버전에서 다른 소스 버전으로의 델타 프로그램 코드 변경 의 정확성에 대해 설명합니다 . 동작을 수정해서는 안됩니다. 확장해야합니다. 전형적인 예는 필드 추가입니다. 기존의 모든 필드는 이전과 같이 계속 작동합니다. 새로운 필드는 기능성을 추가합니다. 그러나 필드를 삭제하면 일반적으로 OCP를 위반하는 것입니다. 여기에서는 프로그램 버전 델타 가 OCP를 충족하는지 확인합니다.

이것이 LSP와 OCP의 주요 차이점입니다. 전자는 코드베이스 만 그대로 확인 하고, 후자 는 한 버전에서 다음 버전으로 코드베이스 델타 만 검증합니다 . 따라서 그것들은 같은 것이 될 수 없으므로 다른 것을 검증하는 것으로 정의 됩니다.

좀 더 공식적인 증거를 드리겠습니다. "LSP는 OCP를 암시합니다"라는 말은 델타를 암시한다고 말하지만 (OCP는 사소한 경우가 아닌 다른 것을 요구하기 때문에) LSP는이를 요구하지 않습니다. 따라서 그것은 분명히 거짓입니다. 반대로 우리는 OCP가 델타에 대한 진술이라고 말함으로써 "OCP는 LSP를 암시한다"라는 반증을 할 수있다. 따라서 그것은 현장 프로그램에 대한 진술에 대해서는 아무 것도 말하지 않는다. 이는 ANY 프로그램부터 시작하여 ANY 델타를 만들 수 있다는 사실에서 비롯됩니다. 그들은 완전히 독립적입니다.


-1

나는 고객의 관점에서 볼 것이다. 클라이언트가 인터페이스의 기능을 사용하고 내부적으로 해당 기능이 클래스 A에 의해 구현 된 경우 클래스 A가 확장되는 클래스 B가 있다고 가정합니다. 내일 해당 인터페이스에서 클래스 A를 제거하고 클래스 B를 넣으면 클래스 B가 있어야합니다. 또한 클라이언트에게 동일한 기능을 제공합니다. 표준 예제는 수영하는 Duck 클래스이며, ToyDuck이 Duck을 확장하면 수영하고 수영 할 수 없다는 불평도하지 않습니다. 그렇지 않으면 ToyDuck은 Duck 클래스를 확장해서는 안됩니다.


사람들이 답을 투표하지 않고 의견을 남기면 매우 건설적인 일이 될 것입니다. 결국 우리 모두는 지식을 공유하기 위해 여기 있으며, 합당한 이유없이 판단을 전달하는 것은 어떤 목적에도 도움이되지 않습니다.
AKS

이것은 이전의 6 가지 답변에서 제시되고 설명 된 포인트를 넘어서는 실질적인 내용을 제공하지 않는 것 같습니다
gnat

1
내가 생각하는 원칙 중 하나를 설명하는 것 같습니다. 그것은 괜찮지 만 질문은 두 가지 다른 원칙의 비교 / 대조를 요구했습니다. 아마 누군가가 그것을 하향 조정 한 이유 일 것입니다.
StarWeaver
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.