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 수 있으며 풀업 리팩토링 또는 방문자와 같은 패턴을 적용하여 달성 할 수 있습니다.