[모던 C++ 디자인 패턴] 1. 개요 #2 SOLID
SOLID 디자인 원칙
- 단일 책임 원칙(Single Responsibility Principle, SRP)
- 열림-닫힘 원칙(Open-Closed Principle, OCP)
- 리스코프 치환 원칙(Liskov Substitution Principle, LSP)
- 인터페이스 분리 원칙(Interface Segregation Principle, ISP)
- 의존성 역전 원칙(Dependency Invension Principle, DIP)
1. 단일 책임 원칙(SRP)
단일 책임 원칙(Single Responsibility Principle, SRP)은 개별 클래스가 단 한 가지의 책임을 부여받아 수정할 이유가 단 한 가지여야 한다는 원칙을 말한다.
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
struct Journal
{
string title;
vector<string> entries;
explicit Journal(const string& title) : title {}
};
void Journal::add(const string& entry)
{
static int count = 1;
entries.push_back(boost::lexical_cast<string>(count++) + ": " + entry);
}
struct PersistenceManager
{
static void save(const Journal& j, const string& filename)
{
ofstream ofs(filename);
for (auto& s : j.entries)
{
ofs << s << endl;
}
}
};
// 저널 이용 시
// Journal j{"Dear Diary"};
// j.add("I cried today");
// j.add("I ate a bug");
-
Add
:Journal
클래스에 포함 ⭕- 각 항목을 기록할 수 있게 하고 관리할 책임이 메모장에 있음
-
save
:Jounal
클래스에 포함 ❌-
메모장의 책임은 메모 항목을 디스크에 쓰는 것이 아님
- 데이터 저장 방식(eg.로컬/클라우드)이 바뀔 때마다 일일이 클래스들을 수정해야 함
-
작은 수정을 위해 여러 클래스를 건드린다면 아키텍쳐가 문제일 수 있음
잘못된 예시
```c++ void Journal::save(const string& filename) { ofstream ofs(filename); for (auto& s : entries) { ofs << s << endl; } } ```
=> 파일 저장 기능은 메모장과 별도인 클래스로 만드는 것이 적합하다! (단일 책임 원칙) -
2. 열림-닫힘 원칙(OCP)
열림-닫힘 원칙은 타임이 확장에는 열려 있지만 수정에는 닫혀 있도록 강제하는 것을 의미한다.
따라서 기존 코드의 수정 없이 필터링을 확장할 수 있는 방법이 필요하다.
데이터베이스에 저장된 제품을 색상과 크기로 검색하는 기능을 만들어보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum class Color { Red, Green, Blue };
enum class Size { Small, Medium, Large };
struct Product
{
string name;
Color color;
Size size;
};
struct ProductFilter
{
typedef vector<Product*> Items;
};
제품 필터링 기능 (열림-닫힘 원칙을 따르지 않을 경우)
각각 색상, 크기, 색상과 크기를 필터링하는 함수다. 코드가 반복되어 사용된다는 문제가 있다.
범용적으로 임의의 조건을 지정받는 함수를 만들지 못하는 이유는 아래와 같다. * 필터 조건마다 처리 형태가 다를 수 있음 * 인덱싱으로 접근해야 하는 탐색해야 하는 경우, 병렬로 탐색이 가능한 경우 등 ```c++ // 색상을 기준으로 제품을 구분하는 멤버 함수 ProductFilter::Items ProductFilter::by_color(Items items, Color color) { Items result; for (auto& i : items) { if (i->color == color) { result.push_back(i); } return result; } } // 크기를 기준으로 제품을 구분하는 멤버 함수 ProductFilter::Items ProductFilter::by_size(Items items, Size size) { Items result; for (auto& i : items) { if (i->size == size) { result.push_back(i); } return result; } } // 크기와 색상을 기준으로 제품을 구분하는 함수 ProductFilter::Items ProductFilter::by_color_and_size(Items items, Size size, Color color) { Items result; for (auto& i : items) { if (i->color == color && (i->size == size) { result.push_back(i); } return result; } } ```
열림-닫힘 원칙을 따라 기존 코드의 수정 없이 필터링을 확장할 수 있는 방법은 아래와 같다.
-
필터링 절차를 개념적으로 두 개의 부분으로 분리 (SRP)
-
필터: 항목 집합을 넘겨받아 그 일부를 리턴
-
명세: 데이터 항목을 구분하기 위한 조건 정의
-
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
// 필터 정의
template <typename T> struct Filter
{
virtual vector<T*> filter(vector<T*> items, Specification<T>& spec) = 0;
};
// 명세 정의
template <typename T> struct Specification
{
virtual bool is_satisfied(T* item) = 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;
explicit ColorSpecification(const Color color) : color{color} {}
bool is_satisfied(Product* item) override
{
return item->color == color;
}
};
// 위 명세를 활용한 제품 목록 필터링
// Product apple{ "Apple", Color::Green, Size::Small };
// Product tree{ "Tree", Color::Green, Size::Large };
- 정의
- 필터
Specification<T>
에 기반해 필터링 수행- 전체 항목 집합과 명세를 인자로 받아 명세에 합치되는 항목들을 리턴
- 항목들은
vector<T*>
에 저장
- 항목들은
- 명세
- 전체적인 접근 방법을 재사용 가능하게 만듦
- 필터
- 구현
- 개선된 필터
Specification<T>
타입이 강하게 규정된 함수
- 개선된 필터
Leave a comment