Architecture & Design Pattern 전체 링크
참고
개방-폐쇄 원칙 (OCP, Open-Closed Principle)
- Open : 소프트웨어의 기능을 추가할 때, 기존의 코드를 변경하지 않고 확장할 수 있어야 한다.
- Closed : 이미 동작하는 코드에 대한 변경이 필요 없이 새로운 기능을 추가할 수 있어야 한다.
- 클래스는 확장에 대해서는 열려 있어야 하고, 코드 변경에 대해서는 닫혀 있어야 한다.
먼저 개방-폐쇄 원칙 위반 사례를 보자.
아래 코드는 사각형과 원의 넓이를 출력하는 예시다.
#include <iostream>
using namespace std;
class Rectangle
{
public:
Rectangle(double w, double h) : width(w), height(h) {}
double calculateArea() { return width * height; }
private:
double width;
double height;
};
class Circle
{
public:
Circle(double r) : radius(r) {}
double calculateArea() { return 3.14 * radius * radius; }
private:
double radius;
};
int main()
{
Rectangle* rectangle = new Rectangle(5, 4);
Circle* circle = new Circle(3);
cout << "Rectangle area: " << rectangle->calculateArea() << endl;
cout << "Circle area: " << circle->calculateArea() << endl;
return 0;
}
여기서 Triangle 클래스를 확장해 보자.
class Triangle
{
public:
Triangle(double b, double h) : base(b), height(h) {}
double calculateArea() { return 0.5 * base * height; }
private:
double base;
double height;
};
main 함수에 Triangle의 calculateArea()도 추가해야 하기 때문에 기존 코드가 변경된다.
int main()
{
Rectangle* rectangle = new Rectangle(5, 4);
Circle* circle = new Circle(3);
Triangle* trianle = new Triangle(1, 2);
cout << "Rectangle area: " << rectangle->calculateArea() << endl;
cout << "Circle area: " << circle->calculateArea() << endl;
cout << "Triangle area: " << trianle->calculateArea() << endl;
return 0;
}
여기서는 간단한 예시기 때문에 calculateArea만 추가하면 되지만,
복잡한 클래스일수록 중복해서 추가해야 하는 코드가 많이 늘어나게 된다.
개방-폐쇄 원칙 적용
calculateArea는 중복되는 코드이기 때문에 추상화하고 재사용할 수 있도록 만든다.
Shape 인터페이스를 만들어서 각 도형은 Shape를 상속하여 calculateArea를 구현하도록 한다.
class Shape
{
public:
virtual ~Shape() {}
virtual double calculateArea() = 0;
};
예를 들면 Rectangle은 아래와 같이 코드가 변경된다.
class Rectangle : public Shape // Shape 상속
{
public:
Rectangle(double w, double h) : width(w), height(h) {}
double calculateArea() override { return width * height; } // 오버라이딩
private:
double width;
double height;
};
이제 각 도형은 모두 Shape 인터페이스를 구현하였기 때문에 도형의 인스턴스를 Shape로 묶을 수 있다.
Rectangle* rectangle = new Rectangle(5, 4);
Circle* circle = new Circle(3);
Triangle* trianle = new Triangle(1, 2);
vector<Shape*> shapes;
shapes.push_back(rectangle);
shapes.push_back(circle);
shapes.push_back(trianle);
이제 새로운 도형이 추가되더라도 calculateArea를 계산하는 부분은 코드 수정이 불필요하다.
for (Shape* shape : shapes)
cout << "Area: " << shape->calculateArea() << endl;
여기서 각 클래스마다 getName을 구현하도록 하면 "도형 이름 area : " 로 출력하게 만들 수도 있다.
전체 코드는 다음과 같다.
#include <iostream>
#include <vector>
using namespace std;
class Shape
{
public:
virtual ~Shape() {}
virtual double calculateArea() = 0;
};
class Rectangle : public Shape
{
public:
Rectangle(double w, double h) : width(w), height(h) {}
double calculateArea() override { return width * height; }
private:
double width;
double height;
};
class Circle : public Shape
{
public:
Circle(double r) : radius(r) {}
double calculateArea() override { return 3.14 * radius * radius; }
private:
double radius;
};
class Triangle : public Shape
{
public:
Triangle(double b, double h) : base(b), height(h) {}
double calculateArea() override { return 0.5 * base * height; }
private:
double base;
double height;
};
int main()
{
Rectangle* rectangle = new Rectangle(5, 4);
Circle* circle = new Circle(3);
Triangle* trianle = new Triangle(1, 2);
vector<Shape*> shapes;
shapes.push_back(rectangle);
shapes.push_back(circle);
shapes.push_back(trianle);
for (Shape* shape : shapes)
cout << "Area: " << shape->calculateArea() << endl;
for (Shape* shape : shapes) delete shape;
return 0;
}
과일 분류
이제 과일의 특징과 분류 방법을 인터페이스로 구현하여 OCP 원칙을 준수하면서 과일을 분류해 보자.
과일 클래스의 멤버로 이름과 가격이 있다.
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Fruit
{
public:
Fruit(string n, int p) : name(n), price(p) {};
string getName() { return name; }
int getPrice() { return price; }
private:
string name;
int price;
};
int main()
{
vector<Fruit*> fruits = {
new Fruit({ "Apple", 100 }),
new Fruit({ "Banana", 150 }),
new Fruit({ "Orange", 120 }),
new Fruit({ "Grapes", 200 }),
new Fruit({ "Pineapple", 180 }),
new Fruit({ "Watermelon", 300 }),
new Fruit({ "Strawberry", 250 }),
new Fruit({ "Mango", 170 }),
new Fruit({ "Peach", 190 }),
new Fruit({ "Kiwi", 130 })
};
for (auto& f : fruits)
cout << f->getName() << " : " << f->getPrice() << endl;
return 0;
}
이제 과일의 이름의 길이가 5 이하인 과일만 필터링한다고 가정해 보자.
필터링의 기준이 되는 Spec 인터페이스는 isValid를 구현하도록 한다.
LengthSpec은 생성자에서 length를 전달받아, isValid에서 length보다 작은 값을 분류한다.
template <typename T>
class Spec
{
public:
virtual bool isValid(T* item) = 0;
};
class LengthSpec : public Spec<Fruit>
{
public:
LengthSpec(int l) : length(l) {}
bool isValid(Fruit* item) override { return item->getName().size() <= length; }
private:
int length = 0;
};
이제 Filter 인터페이스를 만들어서 filter 메서드를 구현하자.
문자열 기준으로 필터링을 하는 MyFilter는 넘어오는 Spec을 기준으로 과일을 분류하게 된다.
template <typename T>
class Filter
{
public:
virtual vector<T*> filter(vector<T*> items, Spec<T>& spec) = 0;
};
class MyFilter : public Filter<Fruit>
{
public:
vector<Fruit*> filter(vector<Fruit*> items, Spec<Fruit>& spec) override
{
vector<Fruit*> fruits;
for (auto& f : items)
if (spec.isValid(f)) fruits.push_back(f);
return fruits;
}
};
예시 코드는 다음과 같다.
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Fruit
{
public:
Fruit(string n, int p) : name(n), price(p) {};
string getName() { return name; }
int getPrice() { return price; }
private:
string name;
int price;
};
template <typename T>
class Spec
{
public:
virtual bool isValid(T* item) = 0;
};
class LengthSpec : public Spec<Fruit>
{
public:
LengthSpec(int l) : length(l) {}
bool isValid(Fruit* item) override { return item->getName().size() <= length; }
private:
int length = 0;
};
template <typename T>
class Filter
{
public:
virtual vector<T*> filter(vector<T*> items, Spec<T>& spec) = 0;
};
class MyFilter : public Filter<Fruit>
{
public:
vector<Fruit*> filter(vector<Fruit*> items, Spec<Fruit>& spec) override
{
vector<Fruit*> fruits;
for (auto& f : items)
if (spec.isValid(f)) fruits.push_back(f);
return fruits;
}
};
int main()
{
vector<Fruit*> fruits = {
new Fruit({ "Apple", 100 }),
new Fruit({ "Banana", 150 }),
new Fruit({ "Orange", 120 }),
new Fruit({ "Grapes", 200 }),
new Fruit({ "Pineapple", 180 }),
new Fruit({ "Watermelon", 300 }),
new Fruit({ "Strawberry", 250 }),
new Fruit({ "Mango", 170 }),
new Fruit({ "Peach", 190 }),
new Fruit({ "Kiwi", 130 })
};
MyFilter stringFilter;
LengthSpec ls(5);
vector<Fruit*> myFruits = stringFilter.filter(fruits, ls);
// result
for (auto& f : myFruits)
cout << f->getName() << " : " << f->getPrice() << endl;
return 0;
}
조건 추가하기
이제 이름의 길이가 5 이하이고 가격이 150원 이하인 과일을 분류하는 요구사항이 추가되었다.
class PriceSpec : public Spec<Fruit>
{
public:
PriceSpec(int p) : price(p) {}
bool isValid(Fruit* item) override { return item->getPrice() <= price; }
private:
int price = 0;
};
위에서 구체적으로 구현한 PriceSpec만 추가하면 된다.
MyFilter myFilter;
LengthSpec ls(5);
PriceSpec ps(150);
vector<Fruit*> myFruits = myFilter.filter(myFilter.filter(fruits, ls), ps);
// result
for (auto& f : myFruits)
cout << f->getName() << " : " << f->getPrice() << endl;
필터를 아래와 연쇄적으로 쓰는 것이 불편할 수 있다.
stringFilter.filter(stringFilter.filter(fruits, ls), ps);
이 경우 조건을 하나로 합치면 된다.
조건을 하나로 합치는 AndSpec 클래스를 아래와 같이 정의하자.
template <typename T>
class AndSpec : public Spec<T>
{
public:
AndSpec(Spec<T>& c1, Spec<T>& c2) : cond1(c1), cond2(c2) { }
bool isValid(T* item) override
{
return cond1.isValid(item) && cond2.isValid(item);
}
private:
Spec<T>& cond1;
Spec<T>& cond2;
};
이제 filter 코드가 조금 더 깔끔해졌다.
MyFilter myFilter;
LengthSpec ls(5);
PriceSpec ps(150);
AndSpec<Fruit> stringAndPrice(ls, ps);
vector<Fruit*> myFruits = myFilter.filter(fruits, stringAndPrice);
또는 연산자 오버로딩을 이용해서 && 연산을 이용해 조건을 합칠 수 있다.
template <typename T>
class AndSpec;
template <typename T>
class Spec
{
public:
virtual bool isValid(T* item) = 0;
AndSpec<T> operator &&(Spec&& other)
{
return AndSpec<T>(*this, other);
}
};
이제 && 연산자로 조건을 만들어보자.
MyFilter myFilter;
LengthSpec ls(5);
PriceSpec ps(150);
AndSpec<Fruit> stringAndPrice = ls && ps;
vector<Fruit*> myFruits = myFilter.filter(fruits, stringAndPrice);
위의 모두 아래의 결과가 나온다.
전체 코드는 다음과 같다.
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Fruit
{
public:
Fruit(string n, int p) : name(n), price(p) {};
string getName() { return name; }
int getPrice() { return price; }
private:
string name;
int price;
};
template <typename T>
class AndSpec;
template <typename T>
class Spec
{
public:
virtual bool isValid(T* item) = 0;
AndSpec<T> operator &&(Spec<T>& other)
{
return AndSpec<T>(*this, other);
}
};
template <typename T>
class AndSpec : public Spec<T>
{
public:
AndSpec(Spec<T>& c1, Spec<T>& c2) : cond1(c1), cond2(c2) { }
bool isValid(T* item) override
{
return cond1.isValid(item) && cond2.isValid(item);
}
private:
Spec<T>& cond1;
Spec<T>& cond2;
};
class LengthSpec : public Spec<Fruit>
{
public:
LengthSpec(int l) : length(l) {}
bool isValid(Fruit* item) override { return item->getName().size() <= length; }
private:
int length = 0;
};
class PriceSpec : public Spec<Fruit>
{
public:
PriceSpec(int p) : price(p) {}
bool isValid(Fruit* item) override { return item->getPrice() <= price; }
private:
int price = 0;
};
template <typename T>
class Filter
{
public:
virtual vector<T*> filter(vector<T*> items, Spec<T>& spec) = 0;
};
class MyFilter : public Filter<Fruit>
{
public:
vector<Fruit*> filter(vector<Fruit*> items, Spec<Fruit>& spec) override
{
vector<Fruit*> fruits;
for (auto& f : items)
if (spec.isValid(f)) fruits.push_back(f);
return fruits;
}
};
class PriceFilter : public Filter<Fruit>
{
public:
vector<Fruit*> filter(vector<Fruit*> items, Spec<Fruit>& spec) override
{
vector<Fruit*> fruits;
for (auto& f : items)
if (spec.isValid(f)) fruits.push_back(f);
return fruits;
}
};
int main()
{
vector<Fruit*> fruits = {
new Fruit({ "Apple", 100 }),
new Fruit({ "Banana", 150 }),
new Fruit({ "Orange", 120 }),
new Fruit({ "Grapes", 200 }),
new Fruit({ "Pineapple", 180 }),
new Fruit({ "Watermelon", 300 }),
new Fruit({ "Strawberry", 250 }),
new Fruit({ "Mango", 170 }),
new Fruit({ "Peach", 190 }),
new Fruit({ "Kiwi", 130 })
};
MyFilter myFilter;
LengthSpec ls(5);
PriceSpec ps(150);
AndSpec<Fruit> stringAndPrice = ls && ps;
vector<Fruit*> myFruits = myFilter.filter(fruits, stringAndPrice);
// result
for (auto& f : myFruits)
cout << f->getName() << " : " << f->getPrice() << endl;
return 0;
}
위의 코드에서 새로운 조건이 추가 되더라도 기존의 Spec과 Filter, 결과 코드는 수정하지 않았다.
// result
for (auto& f : myFruits)
cout << f->getName() << " : " << f->getPrice() << endl;
요구 조건에 대해 인터페이스는 수정하지 않고, 새롭게 구현을 통해 필터링 방식을 변경하였다. (OCP 원칙 준수)
'개발 > Architecture & Design Pattern' 카테고리의 다른 글
C++ - 팩토리 메서드 패턴 (Factory Method Pattern) (1) | 2024.02.12 |
---|---|
C++ - 단일 책임 원칙 (SRP, Single Responsibility Principle) (0) | 2024.02.12 |
C++ - 의존 역전 원칙 (DIP, Dependency Inversion Principle) (0) | 2024.02.12 |
C++ - 데코레이터 패턴 (Decorator Pattern) (1) | 2024.02.10 |
C++ - 복합체, 컴포지트 패턴 (Composite Pattern) (1) | 2024.02.09 |
댓글