The Open-Closed Principle

1.OOP // Open-Closed Principle

객체 지향 디자인에 대한 일반적 지식들이 항상 옳을까?
All member variables should be private. 모든 멤버 변수들은 private 되어야만 한다.
Global variables should be avoided. 전역 변수는 피해야만 한다.
Using run time type identification is dangerous. RTTI는 위험하다.

모든 시스템은 그들의 생명 주기 동안 변하며, 오래 지속될 것을 예상하여 시스템 초기 개발에 염두 하여야 한다.

"SOFTWARE ENTITIES(CLASSES, MODULES, FUNCTIONS, ETC.)
SHOULD BE OPEN FOR EXTENSION, BUT CLOSED FOR MODIFICATION."

"소프트웨어 주요구성요소들은 확장에는 개방되어야 하지만, 변형에는 닫혀있어야 한다."

프로그램은 일반적으로 변형되기 쉽고, 외부 영향을 받으며 예측하기 어렵고, 재사용이 어렵다.
Open-Closed Principle 의 2가지 요소
1. Open for Extension 확장을 위한 개방
2. Closed for Modification 변동을 막는 패쇄

일반적으로 모듈의 동작을 확장하려면 모듈의 변화가 야기된다.

어떻게 두 가지 상반되는 요소를 풀수 있을까?
Abstraction is key
Abstraction 추상적 개념을 도입하면 구현이 가능하다.
Abstraction이란 Base 클래스를 추상화 하고 모든 상속 받는 클래스들에게 나타내어지는 경계선 없는 그룹이다.
Abstraction은 변형에 닫혀 있을 수 있고, 새로운 파생을 상속 하게 함으로 확장에는 열릴 수 있다.

OOD solution to Square/Circle problem. // open-closed principle

class Shape
{
public:
virtual void Draw() const = 0;
};

class Square : public Shape
{
public:
virtual void Draw() const;
};

class Circle : public Shape
{
public:
virtual void Draw() const;
};

void DrawAllShapes(Set<Shape*>& list)
{
for (Iterator<Shape*>i(list); i; i++)
(*i)->Draw();
}

2.Advanced Issue DrawAllShapes with Ordering Issue

DrawAllShapes with Ordering

Shape with ordering methods.

class Shape

{

public:

virtual void Draw() const = 0;

virtual bool Precedes(const Shape&) const = 0;

bool operator<(const Shape& s) {return Precedes(s);}

};


void DrawAllShapes(Set<Shape*>& list)

{

// copy elements into OrderedSet and then sort.

OrderedSet<Shape*> orderedList = list;

orderedList.Sort();

for (Iterator<Shape*> i(orderedList); i; i++)

(*i)->Draw();

}

Ordering a Circle

bool Circle::Precedes(const Shape& s) const

{

if (dynamic_cast<Square*>(s))

return true;

else

return false;

}

 

DrawAllShapes with Ordering - ocp

#include <typeinfo.h>

#include <string.h>

enum {false, true};

typedef int bool;

class Shape

{

public:

virtual void Draw() const = 0;

virtual bool Precedes(const Shape&) const;

bool operator<(const Shape& s) const

{

return Precedes(s);

}

private:

static char* typeOrderTable[];

};

 

char* Shape::typeOrderTable[] = { "Circle", "Square", 0 };

// This function searches a table for the class names.

// The table defines the order in which the

// shapes are to be drawn. Shapes that are not

// found always precede shapes that are found.

 

bool Shape::Precedes(const Shape& s) const

{

const char* thisType = typeid(*this).name();

const char* argType = typeid(s).name();

bool done = false;

int thisOrd = -1;

int argOrd = -1;

for (int i=0; !done; i++)

{

const char* tableEntry = typeOrderTable[i];

if (tableEntry != 0)

{

if (strcmp(tableEntry, thisType) == 0)

thisOrd = i;

if (strcmp(tableEntry, argType) == 0)

argOrd = i;

if ((argOrd > 0) && (thisOrd > 0))

done = true;

}

else // table entry == 0

{

done = true;

}

}

return thisOrd < argOrd;

}

 

3.Advanced Issue RTTI is Dangerous.


참고 1.

RTTI violating the open-closed principle.

class Shape {};

class Square : public Shape

{

private:

Point itsTopLeft;

double itsSide;

friend DrawSquare(Square*);

};

class Circle : public Shape

{

private:

Point itsCenter;

double itsRadius;

friend DrawCircle(Circle*);

};

void DrawAllShapes(Set<Shape*>& ss)

{

for (Iterator<Shape*>i(ss); i; i++)

{

Circle* c = dynamic_cast<Circle*>(*i); // violating open-closed principle

Square* s = dynamic_cast<Square*>(*i);

if (c)

DrawCircle(c);

else if (s)

DrawSquare(s);

}

}

 

The difference between these two is that the first, Listing 9, must be changed whenever a new type of Shape is derived.

 

Listing 10

RTTI that does not violate the open-closed Principle.

class Shape

{

public:

virtual void Draw() cont = 0;

};

class Square : public Shape

{

// as expected.

};

void DrawSquaresOnly(Set<Shape*>& ss)

{

for (Iterator<Shape*>i(ss); i; i++)

{

Square* s = dynamic_cast<Square*>(*i);

if (s)

s->Draw(); // not violating open-closed principle

}

}

 

if a use of RTTI does not violate the open-closed principle, it is safe.

SRP:

The Single Responsibility Principle / 단일 책임 원칙

None but Buddha himself must take the responsibility of giving out occult secrets...

— E. Cobham Brewer 1810–1897.

Dictionary of Phrase and Fable. 1898.
이 원칙은 1.Tom DeMarco와 2.Meilir Page-Jones에 작업에 의해 기술 되었고,
그들은 이것을 응집력이라고 불렀으며, 21챕터에서 볼 것으로써, 패키지 레벨에 응집력의 보다 명확한 정의를 
다룰 것이지만 클래스 레벨상에 정의는 거의 비슷하다.

SRP: The Single Responsibility Principl

클래스를 변경하기 위한 하나 이상의 이유가 있어선 안된다.챕터 6에서 다룬 볼링 게임을 예를 들면 게임 클래스의 개발에 대부분을 위하여 2가지 책임을 분리해
다뤘는데, 현재 프레임에 트랙을 유지하는 것과 스코어를 계산하는 것이었다. 마지막으로는 RCM과
RSK라는 두개의 책임들을 두개의 클래스로 분리 해냈다.
Game은 프레임의 트렉 유지 책임을, Scorer는 스코어를 계산하는 책임을 유지했다. (85페이지)

1. [DeMarco79], p310
2. [PageJones88], Chapter 6, p82. 
중요한 이러한 두 개의 책임들을 각각 분리된 클래스로 다루는 것이 왜 중요할까?
왜냐하면 각각의 책임들은 변동의 축이 되기 때문이다. 어떤 변동이 요구가 될 때, 그 변화는 클래스들 사이에 
변동의 책임이라는것을 통해 명백하게 될 것이다. 만약 한 클래스가 한 개의 책임 이상을 가정한다면, 변동를 위해서는 한 개
이상의 이유가 있어야 할 것이다.

만약 한 클래스가 한 개 이상의 책임을 가진다면, 책임들은 상호간 결속되어 지는데,
한 개의 책임이 변하게 되면 다른 클래스들을 만나는 클래스적 기능이 감소되거나 방해가 될수 있으며,
이러한 종류의 상호간 결속력은 변동이 생길 때에 기대하지 않은 방향으로 쉽게 깨져버리게 된다.

예를들면, 9-1에서 사각형클래스에 두개의 메쏘드들을 보유하고 있고, 한개는 화면상에 사각형을 그리고, 다른 하나는 사각형 
영역을 계산한다.

두개의 다른 응용프로그램이 사각형 클래스에서 사용된다.

우선 하나는 기하학적 계산을 하는데, 기하학 모형에 수학적 연산을 돕기위해 사각형클래스를 사용하여 사각형을 화면상에
그리지는 않는다.
다른 응용프로그램은 그래픽적으로 작용하며 또한 기하학적인 연산처리를 하지만 명확한것은 화면상에 사각형을 그리는
역활을 한다.
이 클래스 디자인은 SRP(Single Responsibility Principle)을 어기고 있다.
사각형 클래스는 2개의 책임을 지고 있는데, 첫번째 책임은 사각형의 기하학적 모델의 수학적 부분을 제공하고,
두번째 책임은 그래픽적인 사용자 인터페이스 로써 사각형을 그리는 것이다.
SRP를 어기는 것은 몇몇 귀찮은 문제들을 야기한다.
우선적으로, 기하학적 연산처리 응용 프로그램내에서 GUI를 포함해야만 하며, 
만약 C++응용 프로그램이라면, GUI는 내부적으로, 메모리 추적, 컴파일 시간, 링크소비 시간을 링크를 해야 할 것이다.
자바 응용 프로그램이라면, .class파일들을 위한 GUI는 목표 플랫폼에 맞게 전개 되어야 한다.
두번째로, 만약 몇가지 이유로 GraphiclaApplication의 변동이 사각형클래스의 변동을 야기 한다면, 그 변동은 강제적인 리빌드,
재검사, 
  ComputationalGeometryApplication 이동을 요구 할 수도 있다.
만약 이것을 잊어버린다면, 그 응용 프로그램은 예측 불가능한 방향으로  돌변 할 수도 있다.
더 나은 디자인은 참조 9-2에서 보여주는것처럼 완벽하게 다른 클래스들로 두개의 책임을 각각 분리 하는것이다.
이 디자인으로 사각형의 연산처리부분은 GeometricRectangle 클래스로 이동 되었다.
이제 변동으로 렌더링되는 사각형은 ComputationalGeometryApplication에 영향을 줄 수 없는 방법이 적용되었다.
 

여기서 이제 책임이 무엇인가?

구문상에 있어 단일 책임 원칙(SRP-Singlt Responsibility Principle)을 "변동을 위한 하나의 동기"를 책임으로 정의하였다.

 

만약 독자가 한클래스에 변동을 위한 한개의 동기 이상을 생각 한다면, 그 클래스는 한개 책임 이상을 가지게 된다.

 

이것은 때때로 어렵다. 여기 그룹내 책임을 생각하기 위해 익혀보도록 하자. 예로써 모뎀 인터페이스를 간주하였다.

 

Listing 9-1

Modem.java -- SRP Violation

interface Modem

{

public void dial(String pno);

public void hangup();

public void send(char c);

public char recv();

}

2개의 책임들이 여기서 보여진다. 첫째는 연결 관리이고, 두번째는 데이터 상호간 통신이다. dial과 hangup 함수는 모뎀의
연결을 관리하고 send, recv 함수는 통신을 관리한다. 
이 두개의 책임들은 분리 되어야만 할까? 명확하게 그렇다.  
두개의 셋 함수들은 거의 상호간 공통분모가 없다.

그것들은 각각 명확히 다른 이유의 변동이며, 거기다가 각각의 함수들을 사용하는 응용프로그램 부분까지 명확히 구획이 

 

나누어져 호출된다. 이 두개의 서로다른 단위는 서로 다른 이유에 잘 변동이 될 것이다.

그러함으로 참조 9-3은 더 나은 클래스디자인을 보여준다. 그것은 두개의 분리된 인터페이스로 들어가는 책임들로 구분되어졌다.
이것은 적어도, 두개의 책임들의 연관성으로부터 클라이언트 프로그램을 유지시켜준다. 

그러나, 주목할 것은 단일 ModemImplementation 클래스 내에 두개의 책임들이 재연관을 맺었다는 것 이다.

 

이것은 바람직하지 않지만 필요가 있다.

 

종종 어떤 하드웨어나 운영체제의 세부적인 사항들을 처리를 하기 위한 이유 등으로 인해 어쩔 수 없이 상호 연관을 가지게 되는

 

경우가 생긴다.

 

그러나, 그들의 인터페이스를 나눔으로 응용프로그램이 관련된 나머지에 관하여 개념적인 분리를 할 수가 있다.
ModemImplamentation 클래스는 적합하지 않을지도 모른다. 그러나 주목할 것은 모든 비독립적인 것들로 부터 멀리하라는 

 

 것이다. 

 

 

 

Conclusion / 결론

단일 책임 원칙(Single Responsibility Principle) 은 하나의 단순한 원칙이고 올바르게 구현하기가 가장 어려운 디자인들 중
하나이다.

 

책임들을 결합 하는 것은 자연스러운 결과이며, 다른 하나로부터 여러 책임들을 찾아내고 분리해내는 것이 실제적인

 

소프트웨어 디자인의 상당 부분을 차지한다. 나머지 원칙들에 대해서 다루다 보면 한가지 또는 다른 방식으로 이 이슈가

 

다시 논의 될 것이다.

Bibliography

[DeMarco79]: Structured Analysis and System Specification, Tom DeMarco, Yourdon

Press Computing Series, 1979

[PageJones88]: The Practical Guide to Structured Systems Design, 2d. ed., Meilir PageJones, Yourdon Press Computing Series, 1988

OVERRIDING INTERFACE METHODS IN SUBCLASSES
// 상속클래스에서 재정의 되는 인터페이스 메서드

내용에 앞서:

언급된 관련 서적 :

1. Don Box's Essential .NET Volume 1

번역 :
1.Polymorphism : 다형성, 다형적

2.Mathod signature : 메서드 서명, 메서드 식별자
(이를테며 void a(int i), void a(int i, float f) 오버로딩 함수라면 반환타입, 인자갯수, 인자 타입등..)

3.SubClass : 상속된 클래스

4.Override : 재정의

5.Implementation : 인터페이스의 최종 구현부 //(Java의 final)

내용 :

C#(일반적으로 닷넷)은 상속된 클래스 내부의 메소드들의 재정의을 위한 virtual/abstract와 계층적 구조를

 이용한 다형성을 제공합니다.

얼핏 보기에, C#은 그 개념에 있어서 C++과 Java 둘다의 특징을 보이지만, 그것은 피상에 불과합니다. 

C#에서 인터페이스를 정의는 다음과 같습니다.

public interface IFoo {

void doSomething();

}
코드참고1.

 


클래스A를 정의하여 인터페이스로 구현을 하게 됩니다.

public class A : IFoo {

public void doSomething() {

Console.WriteLine("In A.doSomething()");

}

}
코드참고2.

 

A를 IFoo 인터페이스로써 사용할 수 있습니다.

 

IFoo foo = new A();

foo.doSomething(); //outputs: In A.doSomething()
코드참고3.


기본적인 예로써, 이것이 가능한 것은 닷넷 런타임이 IFoo.doSomething()을 참조하는 곳에 A.doSomething()
을 찾아내며 오브젝트 참조의 메모리 출력되는 주소 공간을 조작하여 이루어 집니다.

이것은 언어에 따라 다르지만, 일반적으로 (간단하게) 개체에서 지원하는 각 유형의 구현에 매핑 개체에서 사용할 각 방법에 대한 주소 위치의 목록을 하나 이상 vtables를 사용하는것을 포함합니다.

가상 메서드들이 vtable로 컴파일 되는 시점에, 일반적인 메서드들은 자신들의 주소들을 가지는 코드안으로 컴파일이 되며,
이 포인터는 런타임 시점에서 언어가 가지는 다형성 규칙과 특정 참조에 따라 실제 메서드의 위치로 변환됩니다.
 일단
virtual 표기 되면, 함수의 메서드 서명은 타입 계층을 통한 virtual 유지됩니다

 virtual 표기 되면 메소드 서명에 대한 호출은 항상 어디 유형 계층 구조에 있는 참조 점에 관계없이 조회 vtable list 통해 이동 합니다.

 

인터페이스도 타입이므로, 정의된 인터페이스 메서드는 가상이기 때문에 이로써, 정의된 인터페이스 메서드는

재 정의가 될 수가 있습니다.

 C#에서(C++동일, Java제외) 정의된 가상 메서드들은 일반적으로 그들의 서명으로 표기되는 'virtual'키워드가 요구 됩니다.
 (그래서 컴파일러는 메서드의 실제 주소값 대신 추가된 vtable 진입점을 알고 있게 됩니다.)

 구현을 위해 인터페이스 메서드 서명을 상속하는 클래스들이 확실하게 요구하는 동안 인터페이스들의 요청은 잠시

보류가 됩니다.

(엄밀히 말해서 인터페이스들은 구현되는 클래스들의 기본 타입은 아니며, 이에 대한 자세한 정보는 
 Don Box's Essential .NET Volume 1 책의 70페이지를 활용바랍니다.)

   

또한 C#(C++동일,Java제외)에서 또다른 키워드 'override'는 메서드 서명의 vtable진입점으로 변환되기 위해 하위 클래스에서 요청을 하게 됩니다.

결과적으로, C#의 'sealed' 키워드(Java의 'final'과 동일)는 비-가상 메서드를 만들수 없게 만들지는 못해도,
하위 클래스에서 'sealed' 키워드가 선언된 인터페이스를 재 정의를 못하게 할 수는 있습니다.

 virtual 메서드 서명들은 상속 클래스에 vitual로 남게 되고, 인터페이스 메서드들이 virtual가 되면 인터페이스 메서드로써

상속 클래스들의 클래스들에서 구현 부 인터페이스들로써의 재정의가 가능하게 됩니다.
 이하 코드는 이상 내용의 참고:

public class A : IFoo {

public void doSomething() {

Console.WriteLine("In A.doSomething()");

}

}
코드참고4.

public class B : A {

public override void doSomething() {

Console.WriteLine("In B.doSomething()");

}

}
코드참고5.

C#에서 동작하지 않고 다음 에러문구를 출력합니다:

B.doSomething()' : cannot override inherited member 'A.doSomething()'

because it is not marked virtual, abstract, or override

//B.doSomething() : A.doSomething()를 구성요소로써 재정의 할 수 없습니다.
A.doSomething()에 virtual, abstract 또는 override 표시가 없기 때문입니다.//

A를 간단히 해서 'override' 를 메서드 정의에서 제외 시키면 다음과 같이 출력됩니다.

public class B : A {

public void doSomething() {

Console.WriteLine("In B.doSomething()");

}

}
코드참고6.


컴파일 되지만 원하는 결과가 또한 아니며 다음의 경고 메세지를 알려주게 됩니다.:

The keyword new is required on 'B.doSomething()' because it hides inherited member 'A.doSomething()'
new 키워드가 'B.doSomething()'에 필요합니다. 상속 상위 개체 함수'A.doSomething()'에 의해 숨겨졌습니다.

"상속 상위 개체 맴버에의해 숨겨지다" 라는 의미를 해석해 보자면
 C#(C++동일,Java제외)은 저러한 기반 클래스들의 동일한 서명을 가진 상속클래스들의 메소드들을 허용합니다.
 여기서 다시 상기 해보자면 virtual 키워드로 구현된 것이 아니라면 'vtable'목록에 포함되지 않습니다. 
 쉽게 말하자면 B는 두 개의 doSomething()메서드를 가지게 됩니다.
 결과 인식 메서드는A로부터 상속 받은 것, 그리고 B에서 새로 정의한 것 2개가 되며, 그런 이유로 new 키워드가
새로 필요하다는 경고 메세지를 출력하게 됩니다.
 또한 다음에서 보듯, IFoo로써 B를 사용하는 것은 의도하는 바가 아닙니다.

IFoo foo = new B();

foo.doSomething(); //outputs: In A.doSomething() // 원하는 결과가 아님.

한가지 해결점은 'virtual' 메서드를 A.doSomething()앞에 표시하는 것이지만 만약 A 코드에 접근할 수 없는 상황이라면
어떻게 해야할까요?





C#상에서 모든 것을 충족시키는 IFoo의 구현부인 B로써는 다음과 같습니다:

public class B : A, IFoo {

void IFoo.doSomething() {

Console.WriteLine("In B.doSomething()");

}

}

IFoo foo = new B();

foo.doSomething(); //outputs: In B.doSomething()

코드참고7.

그러나 위 사항은 저장 영역으로 B는 다른 syntax임을 주목 해야 합니다.
'public' 키워드를 제외시킴으로써 IFoo.doSomething의 구현을 명확하게 할 수가 있습니다. 
 이전과 같은 경고와 에러가 뜨지 않게 하려면, A에서 했던 것과 같은 단순화된 메소드를 구현할 수는 없습니다. 
이것은 외부 구현부에서 변경하여, 이 클래스 선언을 통해 B를 private 하게 하고 public 키워드를 삭제할 필요가 있습니다.
B에서 구현된 doSomething()은 인스턴스화 된 B에서는 접근 안되며, IFoo에서만 가능합니다.

   

B b=newB();
A a =b;

b.doSomething(); //outputs: In A.doSomething() !!
a.doSomething(); // 역시나 위와 동일한 결과


Essential .NET Volume 1 page 169:

...the implementation of an interface method is implicitly 'final', just as if
the 'sealed' and 'override' modifiers were present.

...인터페이스 메서드의 구현부 에서 선언은 'final'이며, 'sealed'나 'override' 또한 제공됩니다.

인터페이스 메서드를 구현하기 위해서는 sealed 키워드를 사용 해야 하며,
 다르게 보자면 C#에서는 첫 번째 인터페이스 마지막 구현부 에서는 override 로 연결되는 것입니다.

 
C#(.NET)은 뚜렷한 base class designer(기반 클래스 디자이너)의 디자인 결정을 해야 하는 일종에 디자인 철학을

준수합니다.

 어떤 클래스들이 확장 되고자 한다면 명백하게 표기가 되어야 합니다.
반대로 말하면, C++은 무엇인가가 명백화 해줄 필요가 있는, 또한 자바가 그러한, 최종 결과를 지어줄 수 있는 그러한

클래스 디자이너가 제공되지 않고 기본적으로 모든 메서드 들은 가상화 됩니다.
 이것은 보다 많은 토론의 여지가 있는 주제 이지만 필자의 생각으로는 이것은 가능한 것과 직관적인 것 사이에 있어 디자인적인 선호도로 부터 파생된 결과라고 생각합니다.
 필자는 기술적으로 가능한 디자인의 사용을 더욱 선호 합니다.

그러나, 언어 디자인 선택에 관해 논쟁을 펼치는 것은 "돈키호테"와 같다고 믿기 때문에, 근래 들어 개인적으로 선호 하는 것 은 진행해 보고, 함정들을 표시하고, 왜 그것들이 거기 있는지 이해하고, 그것들을 옮기는 방식입니다.

'Programming > Design' 카테고리의 다른 글

OCP : The Open-Closed Principle  (0) 2011.08.05
SRP : The Single Responsibility Principle  (0) 2011.07.18

+ Recent posts