디자인 패턴
- 2000년대 초 로버튼 마틴(Robert C. Martin)에 의해 소개됨
- SOLID 디자인 패턴은, 우리가 앞으로 살펴볼 디자인 패턴에 전반적으로 녹아져 있음
SOLID 디자인 패턴
1) 단일 책임 원칙(Single Responsibility Principle, SRP)
- 단일 책임 원칙에서 각 클래스는 단 한 가지의 책임을 부여받아, 수정할 이유가 단 한가지여야 한다.
- 전지전능한 객체 (여러 기능을 담고 있는)는 SPR를 위배한다.
- 기록을 위한 메모장 클래스가 있을 때, 이 클래스는 vector에 파라미터로 주어지는 값을 추가하는 함수가 존재한다.
- 이때, 추가로 영구적인 파일을 저장하는 기능을 만든다고 할 때 디스크에 파일을 쓰는 기능 또한 메모장 클래스의 역할일까?
- 작은 수정을 여러 클래스에 걸쳐서 해야 하나다면 아키텍처에 뭔가 문제가 있다는 징조이다.
2) 열림-닫힘 원칙 (Open-Closed Principle, OCP)
- 열림-닫힘 원칙은 타입이 확장에는 열려 있지만, 수정에는 닫혀 있도록 강제하는 것을 뜻한다.
- 따라서, 기존 코드의 수정없이 기능을 확장할 수 있어야 한다.
- 색상과 크기로 구별되는 상품들이 존재하고, 이를 필터링하는 기능을 만들 때 코드는 다음과 같다.
enum class Color { Red, Green, Blue };
enum class Size { Small, Medium, Large };
struct Product
{
string name;
Color color;
Size size;
}
struct ProductFiler
{
typedef vector<Product *> Items;
}
ProductFilter::Items ProductFilters::by_color(Items item, Color color)
{
Item result;
for (auto &i : items)
if (i->color == color)
result.push_back(i);
return result;
}
- 만약 색상과 크리를 모두 지정해서 필터링해야 하는 요구 상황이 생긴다면?
- by_color_and_size와 같은 새로운 함수를 또 추가해서 구현할 수도 있다.
- 하지만, 요구사항은 언제든 변경될 가능성이 있어서 기존의 코드 수정없이 필터링을 확장할 수 있는 방법이 필요하다.
- 필터링 절차를 두 개의 부분으로 나눈다. (필터와 명세)
template <typename T> struct Specification
{
virtual bool is_saisfied(T* item) = 0;
};
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_saisfied(p))
result.push_back(p);
return result;
}
};
- 여기서, 크기와 색상을 동시에 피렅링 조건으로 하는 경우는 어떻게 만들 수 있을까?
- 복합 명세를 만들면 된다. 여기서는 C++의 강력한 연산자 오버로딩을 활용해서 훨씬 더 단순하게 구현하겠다.
- && 연산자를 이용하면 두 개 이상의 Specification<T> 객체를 대단히 쉽게 복합 명세로 엮을 수 있다.
template <typename T> struct AndSpecification : Specification<T>
{
Specification<T>& first;
Specification<T>& second;
AndSpecification(Specification<T>& first, Specification<T>& second)
: first(first), second(second) {}
bool is_satis
};
template<typename T> struct Specification
{
virtual bool is_satisfied(T* item) = 0;
AndSpecification<T> operator &&(Specification && other)
{
return AndSpecification<T>(*this, other);
}
};
SizeSpecification large(Size::Large);
ColorSpecification green(Color::Green);
AndSpecification<Product> green_and_large{ large, green };
auto big_green_things = ColorSpecification(Color::Green) && SizeSpecification(Size::Large);
for (auto& x : big_green_things)
cout << x->name << " is large and green" << endl;
3) 리스코프(Liskov) 치환 원칙(Liskov Subsititution Principle, LSP)
- 어떤 자식 객체에 접근할 때, 그 부모 객체의 인터페이스에 접근하더라도 아무 문제가 없어야 하는 원칙이다.
- 예를 들어, 부모 객체인 Rectangle class(사각형)과 자식 객체인 Squre(정사각형)가 있다고 가정한다.
- 사각형내에 넓이를 구하는 함수에서 get_width(), set_width(), get_height(), set_heigh()를 구현했다.
- 그러나 정사각형 내 상속받은 함수인 set_width()와 set_height()는 width나 height를 set하면서, 동시에 height나 width도 동일하게 set하도록 구현했다.
- 여기서, 이 객체를 그 부모인 Rectangle 객체로 접근해서 area(width*height)를 구하면, 의도치 않은 상황이 발생한다.
void process(Rectangle& r)
{
int w = r.get_width();
r.set_height(10);
cout << "expected area = " << ( w*10 )
<< ", got " << r.area() << endl;
}
Square{5};
process(s); // 기대된 결과 = 50, 구해진 값 = 25
- 여기서 해결책은, 애당초 서브 클래스를 만들지 않아야 한다.
- 서브 클래스를 만드는 대신 아래와 같이 Factory 클래스를 두어 직사각형과 정사각형을 따로따로 생성한다.
struct RectangleFactory
{
static Rectangle create_rectangle(int w, int h);
static Rectangle create_squre(int size);
}
4) 인터페이스 분리 원칙(Interface Segregation Principle, ISP)
- 인터페이스 분리 원칙이 의미하는 바는 필요에 따라 구현할 대상을 선별할 수 있도록 인터페이스를 별개로 두어야 한다는 것이다.
- 한 덩어리의 복잡한 인터페이스를 목적에 따라 구분하여, 인터페이스 모든 항목에 대한 구현을 강제하지 않고 실제 필요한 인터페이스만 구현할 수 있도록 하는 것이다.
- 예를 들어, 복합 기능 프린터를 구현할 떄 프린트, 스캔 팩스 기능이 합쳐져 있다.
struct IMachine
{
virtual void print(vector<Document*> docs) = 0;
virtual void fax(vector<Document*> docs) = 0;
virtual void scan(vector<Document*> docs) = 0;
}
- 여기서, 만약 프린트와 스캔 기능만 가진 프린터를 구현하고 싶을 땐, fax를 빈 함수로 구현해야 한다.
struct IPrinter
{
virtual void print(vector<Document*> docs) = 0;
}
struct IScanner
{
virtual void scan(vector<Document*> docs) = 0;
}
struct IMachine : IPrinter, IScanner
{
};
struct Machine : IMachine
{
IPrinter& printer;
IScanner& scanner;
Machine(IPrinter& printer, IScanner& scnaner)
: printer(printer),
scanner(scanner)
{
}
void printer(vector<Document*> docs) override {
printer.print(docs);
}
void scan(vector<Document*> docs) override {
scanner.scan(docs);
}
};
5) 의존성 역전 원칙(Dependency Inversion Principle, DIP)
- 상위 모듈이 하위 모듈에 종속성을 가져서는 안된다. 양쪽 모두 추상화에 의존해야 한다.
- 추상화가 세부 사항에 의존해서는 안된다. 세부 사항이 추상화에 의존해야 한다.
- 오늘날, 의존성 역전 원칙을 구현하는 가장 인기 있는 방법은 종속성 주입 테크닉을 활용하는 것이다.
- 종속성 주입은 Boost.DI와 같은 라이브러를 사용하면, 어떤 컴포넌트의 종속성 요건이 자동적으로 만족되게 한다는 의미이다.
- 예를 들어, 자동차는 엔진과 로그 기능을 필요하다. 이때, 두 기능에 자동차가 의존성을 가진다.
#include <iostream>
#include <memory>
#include <string>
using namespace std;
struct Engine
{
float volume = 5;
int horse_power = 400;
friend ostream& operator<< (ostream& os, const Engine& obj)
{
return os
<< "volume: " << obj.volume
<< " horse_power: " << obj.horse_power;
}
};
struct ILogger
{
virtual ~ILogger() {}
virtual void Log(const string& s) = 0;
};
struct ConsoleLogger : ILogger {
ConsoleLogger() {}
void Log(const string& s) override
{
cout << "LOG: " << s << endl;
}
};
struct Car
{
unique_ptr<Engine> engine;
shared_ptr<ILogger> logger;
Car(unique_ptr<Engine> engine,
const shared_ptr<ILogger>& logger)
: engine{move(engine)},
logger{logger}
{
logger->Log("making a car");
}
friend ostream& operator<<(ostream& os, const Car& obj)
{
return os << "car with engine: " << *obj.engine;
}
};
int main() {
auto engine = make_unique<Engine>();
auto logger = make_shared<ConsoleLogger>();
Car car(move(engine), logger);
cout << car << endl;
return 0;
}
- 여기서 "종속성 주입"인 Boost.DI를 이용하면, ILogger를 ConsoleLogger에 연결하는 bind를 정의한다.
- 이 정의는 누구든 ILogger를 요청하면 ConsoleLogger를 전달하라"라는 의미이다.
- 아래의 코드는 온전히 인스턴스화된 Car객체를 가리키는 shared_ptr<Car>를 만든다.
- 사용할 ILogger 인스턴스의 타입을 바꿀 때, 즉 bind가 수행되는 부분만 수정하면 자동으로 ILogger를 사용하는 모든 곳에서 적용된다는 점이다.
- 이것은 단 한줄만 수정하여 종속성이 있는 객체에 실제 동작하는 구현객체를 사용할 수 있고, 테스트용 더미 객체를 사용하게 바꿀 수도 있다.
auto injector = di::make_injector(
di::bind(ILogger)().to<ConsoleLogger>()
);
// injection이 설정된 후, 아래와 같이 Car를 생성해서 이용할 수 있다.
auto car = injector.create<shared_ptr<Car>>();
모던 c++ 디자인 패턴 책을 보면서 공부한 내용을 정리했습니다.
'프로그래밍공부 > design pattern' 카테고리의 다른 글
[모던 c++의 디자인 패턴] 3장. 팩터리 (0) | 2024.04.10 |
---|---|
[모던 c++의 디자인 패턴] 2장. 빌더 (0) | 2024.04.09 |