4 분 소요

개방-폐쇄 원칙확장에는 열려 있되, 수정에는 닫혀 있게 작성하라 라는 원칙입니다.

조금 풀어 쓰면,

  1. 기존 코드의 수정없이 기능 추가나 변경이 가능하게 설계하여,
  2. 요구사항 추가나 변경에 유연하게 대처하라.

라는 뜻입니다. !!!유연하게!!! 말이죠.

이 원칙을 준수하면,

  1. 기능 수정 및 추가가 용이하도록 작성되어 유연성이 향상됩니다.
  2. 모듈 사용에 실수가 적어져 코딩 계약 준수에 따른 사용성이 향상됩니다.

다만 가독성이 떨어지거나 사용성이 떨어지면, 누군가가 생각지도 못한 창의력으로 창문을 깨버리기 마련이니 창문이 깨지지 않도록 함께 노력해야 합니다.

개방 폐쇄
* 상속을 이용한 확장
* 인터페이스를 이용한 확장
* 가상 함수를 이용한 확장
* 포함을 이용한 확장(Bridge, Decorator, State)
* 의존성 주입을 이용한 확장(Strategy)
* Visitor를 이용한 확장
* private를 이용한 가시성으로 폐쇄
* 코딩 계약으로 폐쇄
* Non-Virtual로 상속 폐쇄
* 단일 책임으로 변경 최소화
* 문서화 안하기
* 알기 힘들게 꼭꼭 숨기기
* 고치기 무섭게 복잡하게 하기

준수 방법 : 유연한 함수

SaveFile() 함수로 파일을 저장한다고 했을때, 함수 내부에서 경로명을 지정했다고 생각해 보시죠.

1
2
3
4
void SaveFile() const {
    std::wstring pathName = L"c:/data/my_data.txt";
    ...
}

경로명이 함수내에 있기 때문에, 파일명이나 경로가 바뀐다면 SaveFile()함수를 수정해야 합니다.

하지만, pathName인자로 요청한다면, 파일명이나 경로가 바꼈을때 SaveFile()함수를 수정할 필요가 없습니다. 인자로 전달하면 되니까요. 경로명을 변경하는 확장에는 열려있고, SaveFile()함수 수정에는 닫혀 있게 됩니다.

1
void SaveFile(const std::wstring& pathName) const {}

준수 방법 : 다형성을 이용한 유연한 개체

다형성을 이용하여 부모 개체로부터 다양한 자식 개체를 추가할 수 있습니다.

하기의 구조에서 새로운 도형 개체가 필요하다면, Shape을 상속한 자식 개체를 추가하면 됩니다.

image

준수 방법 : Visitor 패턴을 이용한 유연한 기능 추가

만약 개체에 새로운 기능을 추가하고 싶다면, 일반적으로는 멤버 함수를 추가하면 됩니다. 하기는 개체를 확대하는 Scale() 을 추가한 예입니다.

image

부모 클래스인 Shape가상 함수Scale()을 만들고 자식 클래스에서 이를 override하였습니다. 일반적인 방법입니다만, 부모 클래스가 뚱뚱해 질 수 있으므로(혹은, Shape이 외부 라이브러리라 인터페이스 수정을 못할 수 있으므로), 상황에 따라 Visitor 패턴으로 부모 클래스 인터페이스 수정없이 기능들을 추가할 수 있습니다.

IVisitor를 이용한 클래스 구성은 다음과 같습니다.

image

IVisitorShape의 자식 개체를 방문하면서 개체 종류에 따라 VisitRectangle() 이나 VisitCircle()을 호출할 수 있게 합니다.

개체에서 이 함수들을 호출하면, ScaleVisitor의 경우는 크기조정을 수행하는 코드를 구현하고, RotateVisitor는 회전을 수행하는 코드를 구현하면 됩니다.

이와 같이 새로운 기능 추가시, 기존 코드의 수정 없이 새로운 Visitor만 만들면 되므로, 코드가 좀더 유연해 집니다.(아쉬운 점은 IVsitorShape상호 참조 되는 점인데, 기능 추가와 의존성 부패 간의 트레이드 오프가 필요해 집니다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Shape; // 상호 참조가 되어 전방 선언이 필요합니다.

// 개체를 방문하여 개체별로 작업합니다.
class IVisitor {
protected:
    ~IVisitor() {}  // 인터페이스여서 protected non-virtual(상속해서 사용하고, 다형 소멸 안함) 입니다.

public:
    // object는 Rectangle입니다. 필요하면 dynamic_cast를 합니다.
    virtual void VisitRectangle(Shape* object) = 0;

    // object는 Circle입니다. 필요하면 dynamic_cast를 합니다.
    virtual void VisitCircle(Shape* object) = 0;
};

// 개체를 Scale합니다.
class ScaleVisitor : public IVisitor {
public:
    virtual void VisitRectangle(Shape* object) override {
        std::cout << "Scale Rectangle" << std::endl; 
    };
    virtual void VisitCircle(Shape* object) override {
        std::cout << "Scale Circle" << std::endl; 
    }
};

// 개체를 Rotate합니다.
class RotateVisitor : public IVisitor {
public:
    virtual void VisitRectangle(Shape* object) override {
        std::cout << "Rotate Rectangle" << std::endl; 
    };
    virtual void VisitCircle(Shape* object) override {
        std::cout << "Rotate Circle" << std::endl; 
    }
};

ShapeIVisitorAccept()에서 전달받고 각 개체는 자신에 해당하는 함수를 호출합니다.(RectangleVisitRectangle()을 호출하고, CircleVisitCircle() 을 호출합니다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class Shape {
private:
    int m_Left;
    int m_Top;
public:
    Shape(int l, int t) :
        m_Left(l),
        m_Top(t) {
    }
    virtual ~Shape() {} // 다형 소멸 하도록 public virtual
public:
    // m_Left, m_Top으로 자식 클래스들이 알아서 그려야 함
    virtual void Draw() const = 0; 

    // visitor 방문을 허용합니다.
    virtual void Accept(IVisitor& visitor) = 0;
};

class Rectangle : public Shape {
private:
    int m_Width;
    int m_Height;
public:    
    Rectangle(int l, int t, int w, int h) : 
        Shape(l, t), 
        m_Width(w), m_Height(h) {}
    virtual ~Rectangle() override {}   

    virtual void Draw() const override {}

    // visitor 방문을 허용합니다.
    virtual void Accept(IVisitor& visitor) override {
        visitor.VisitRectangle(this);   
    }
};

class Circle : public Shape {
private:
    int m_Diameter;
public:    
    Circle(int l, int t, int diameter) : 
        Shape(l, t), 
        m_Diameter(diameter) {}
    virtual ~Circle() override {}   

    virtual void Draw() const override {}

    // visitor 방문을 허용합니다.
    virtual void Accept(IVisitor& visitor) override {
        visitor.VisitCircle(this);
    }
};    

다음과 같이 테스트할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
Rectangle rect(0, 0, 10, 20);
Circle circle(0, 0, 10);

ScaleVisitor scaleVisitor; // 방문하여 개체들을 Scale 합니다.
rect.Accept(scaleVisitor);
circle.Accept(scaleVisitor);

RotateVisitor rotateVisitor; // 방문하여 개체들을 Rotate 합니다.
rect.Accept(rotateVisitor);
circle.Accept(rotateVisitor);

준수 방법 : 의존성 주입을 이용한 유연한 알고리즘 변경

Strategy 패턴을 이용한 의존성 주입(의존성 역전 원칙 참고) 을 활용하여, 원하는 알고리즘으로 손쉽게 변경할 수 있습니다.

하기 그림처럼 구성하고, ShapeSetWriter()에 어떤 Writer(XmlWriter 혹은 JsonWriter)를 전달하는지에 따라 다른 알고리즘을 사용할 수 있습니다.

image

댓글남기기