본문 바로가기
개발/Architecture & Design Pattern

C++ - 개방-폐쇄 원칙 (OCP, Open-Closed Principle)

by 피로물든딸기 2024. 2. 12.
반응형

C, C++ 전체 링크

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 함수에 TrianglecalculateArea()도 추가해야 하기 때문에 기존 코드가 변경된다.

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;
}

 

위의 코드에서 새로운 조건이 추가 되더라도 기존의 SpecFilter, 결과 코드는 수정하지 않았다.

	// result
	for (auto& f : myFruits)
		cout << f->getName() << " : " << f->getPrice() << endl;

 

요구 조건에 대해 인터페이스는 수정하지 않고, 새롭게 구현을 통해 필터링 방식을 변경하였다. (OCP 원칙 준수)

반응형

댓글