반응형
개방-폐쇄 원칙(OCP)

"소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다."

소프트웨어 개발 작업에 이용된 많은 모듈 중에 하나에 수정을 가할 때 그 모듈을 이용하는 다른 모듈을 줄줄이 고쳐야 한다면, 이와 같은 프로그램은 수정하기가 어렵다.

개방-폐쇄의 원칙은 시스템의 구조를 올바르게 재조직(리펙토링)하여 나중에 이와 같은 유형의 변경이 더 이상의 수정을 유발하지 않도록 하는 것이다.

개방-폐쇄 원칙이 잘 적용되면, 기능을 추가하거나 변경해야 할 때 이미 제대로 작동하고 있던 원래 코드를 변경하지 않아도, 기존 코드에 새로운 코드를 추가함으로써 기능의 추가나 변경이 가능하다.

 

개방-폐쇄 원칙의 두 가지 속성
  • 확장에 대해 열려 있다.

- 이것은 모듈의 동작을 확장할 수 있다는 것을 의미한다.

- 애플리케이션의 요구 사항이 변경될 때, 이 변경에 맞게 새로운 동작을 추가해 모듈을 확장할 수 있다.

- 즉, 모듈이 하는 일을 변경할 수 있다.

  • 수정에 닫혀 있다.

- 모듈의 소스 코드나 바이너리 코드를 수정하지 않아도 모듈의 기능을 확장하거나 변경할 수 있다.

- 그 모듈의 실행 가능한 바이너리 형태나 링크 가능한 라이브러리를 건드릴 필요가 없다.

 

추상화를 통한 개방-폐쇄 원칙

객체 지향 프로그래밍 언어에서는 고정되기는 해도 제한되는 않은, 가능한 동작의 묶음을 추상화가 가능하다.

모듈은 추상화를 조작할 수 있다. 이런 모듈은 고정된 추상화에 의존하기 때문에 수정에 대해 닫혀 있을 수 있고, 반대로 추상화의 새 파생 클래스를 만드는 것을 통해 확장도 가능하다.

따라서 추상화는 개방-폐쇄 원칙의 핵심 요소이다.

 

C/C++예제
// open closed principle

// open for extension, closed for modification
#include <string>
#include <vector>
#include <iostream>
using namespace std;

enum class Color { red, green, blue };
enum class Size { small, medium, large };

struct Product
{
    string name;
    Color  color;
    Size    size;
};
// 특정 기준에 따라 필터링을 수행
class ProductFilter
{
private:
    typedef vector<Product*>    Items;
public:
    Items by_color(Items items, Color color)
    {
        Items result;
        for (auto& i : items)
            if (i->color == color)
                result.emplace_back(i);
        return result;
    }

    Items by_size(Items items, Size size)
    {
        Items result;
        for (auto& i : items)
            if (i->size == size)
                result.emplace_back(i);
        return result;
    }

    Items by_size_and_color(Items items, Size size, Color   color)
    {
        Items result;
        for (auto& i : items)
            if (i->size == size && i->color == color)
                result.emplace_back(i);
        return result;
    }
};

int main()
{
    Product apple{ "Apple", Color::green, Size::small };
    Product tree{ "Tree", Color::green, Size::large };
    Product house{ "House", Color::blue, Size::large };

    const vector<Product*> all{ &apple, &tree, &house };
    return 0;
}

 

상품을 분류 하는 프로그램을 만들어본다고 가정합니다.

그러면 Product 구조체 내에 상품명(name), 색상(color), 크기(size)가 존재합니다.

그리고 상품을 분류(ProductFilter) class가 존재합니다.

여기서 분류하는 2가지 기준(color, size)이 있고 3가지 기능(by_size, by_color, by_size_and_color)가 있습니다.

개방-폐쇄 원칙은 기본적으로 시스템이 확장에 개방되어 있어야한다고 이야기합니다.

예를 들면 상속을 통해 시스템을 확장할 수 있어야 수정에 닫히게 됩니다.

그러나, 위의 예시의 문제점은 상속하는 대신 기존 코드를 수정해야합니다.

따라서 위의 예시를 시스템을 확장할 수 있도록 수정되어야 합니다.

template <typename T> struct AndSpecification;

template <typename T> struct Specification
{
    virtual ~Specification() = default;
    virtual bool is_satisfied(T* item) const = 0;

    // new: breaks OCP if you add it post-hoc
    /*AndSpecification<T> operator&&(Specification<T>&& other)
    {
      return AndSpecification<T>(*this, other);
    }*/
};

// new: 
template <typename T> AndSpecification<T> operator&&
(const Specification<T>& first, const Specification<T>& second)
{
    return { first, second };
}

template <typename T> struct Filter
{
    virtual vector<T*> filter(vector<T*> items,
        Specification<T>& spec) = 0;
};

struct BetterFilter : Filter<Product>
{
    vector<Product*> filter(vector<Product*> items,
        Specification<Product>& spec) override
    {
        vector<Product*> result;
        for (auto& p : items)
            if (spec.is_satisfied(p))
                result.push_back(p);
        return result;
    }
};

struct ColorSpecification : Specification<Product>
{
    Color color;

    ColorSpecification(Color color) : color(color) {}

    bool is_satisfied(Product* item) const override {
        return item->color == color;
    }
};

struct SizeSpecification : Specification<Product>
{
    Size size;

    explicit SizeSpecification(const Size size) : size{ size } {    }

    bool is_satisfied(Product* item) const override {
        return item->size == size;
    }
};

template <typename T> struct AndSpecification : Specification<T>
{
    const Specification<T>& first;
    const Specification<T>& second;

    AndSpecification(const Specification<T>& first, const Specification<T>& second)
        : first(first), second(second) {}

    bool is_satisfied(T* item) const override {
        return first.is_satisfied(item) && second.is_satisfied(item);
    }
};

// new:

int main()
{
    Product apple{ "Apple", Color::green, Size::small };
    Product tree{ "Tree", Color::green, Size::large };
    Product house{ "House", Color::blue, Size::large };

    const vector<Product*> all{ &apple, &tree, &house };

    BetterFilter bf;
    ColorSpecification green(Color::green);
    auto green_things = bf.filter(all, green);
    for (auto& x : green_things)
        cout << x->name << " is green\n";

    SizeSpecification large(Size::large);
    AndSpecification<Product> green_and_large(green, large);

    auto spec = green && large;
    for (auto& x : bf.filter(all, spec))
        cout << x->name << " is green and large\n";

    return 0;
}

이렇게 작성하면 확장을 위해 열리게 됩니다.

즉, 상속을 통해 확장하여 항상 새로운 사양을 만들 수 있습니다.

효율적이지만 수행을 위해 닫혀있습니다.

상호작용을 위해 인터페이스나 사양으로 돌아갈 필요가 없어 변경할 필요가 없을 것입니다.

따라서 매우 유연합니다.

그리고 템플릿(template)를 사용하면서 제품(Product)에 제한되지 않습니다.

반응형

+ Recent posts