2023. 5. 15. 21:30ㆍDevelopers 공간 [Basic]/Software Basic
기존에 PP(Procedural Programming, 절차 지향 프로그래밍)에서는 일부분의 코드를 고치기 위해 전체를 다시 설계해야하는 단점이 있었습니다. 따라서 유연성과 재사용성을 높이기위해 OOP(Object Oriented Programming, 객체 지향 프로그래밍)가 등장했고, Java, C++, C#과 같은 객체 지향 프로그래밍 언어들의 중요성이 강조되고 있습니다.
먼저 간단히 OOP의 3요소 5원칙에 대해 소개하겠습니다. 먼저, 3요소는 아래와 같습니다.
- 캡슐화(Encapsulation) : 정보를 은닉하기 위해 속성을 숨기고, 기능을 공개하는 것.
- 상속(Inheritance) : 부모 클래스를 재사용하고 확장하는 것.
- 다형성(Polymorphism) : 사용편의를 위해 하나의 객체가 Overriding, Overloading을 활용해 여러가지 형태를 가지는 것.
** overriding : 부모 클래스의 Method를 자식 클래스 에서 재정의하여 사용
** overloading : 같은 이름을 가진 Method의 인자 값에 따라 다른 기능을 구현하도록 정의
다음으로 "SOLID원칙"이라고도 불리는 OOP의 5원칙은 아래와 같습니다.
- SRP(Single Responsibility Principle, 단일 책임 원칙) : 하나의 클래스는 하나의 책임만을 가집니다.
- OCP(Open Closed Principle, 개방 폐쇄 원칙) : 기능확장에는 열려 있고( Open, 클래스가 추가되어도), 코드수정에는 닫혀 있어야(Close, 기존 코드는 수정되지 않도록) 해야 한다는 이론입니다. 개인적으로 가장 중요한 원칙이라고 생각합니다.
- LSP(Liskov Substitution Principle, 리스코프 치환 원칙) : 상속에 있어 자식이 자식클래스는 부모클래스를 대체할 수 있도록 재정의하지 않고 확장만 수행해야합니다. OCP의 "다형성 구현"을 도와주는 원칙입니다.
- ISP(Interface Segregation Principle, 인터페이스 분리 원칙) : 입력과 출력으로 저수준 모듈이 고수준 모듈에 의존하기 위해, 하나의 인터페이스 보다 여러개의 범용 인터페이스가 더 낫다는 원칙입니다.
** 고수준 모듈 : 입출력과 가까운 추상화된 모듈
** 저수준 모듈 : 입출력과 거리가 있는 실제 기능 구현 모듈 - DIP(Dependency Inversion Principle, 의존관계 역전 원칙) : 구체적인 클래스보다 Interface & 추상클래스를 통해 변화하기 어려운 것에 의존해야 합니다. OCP의 "변화되는 부분을 추상화"할 수 있도록 도와주는 원칙입니다.
** 추상적인(Abstract) 클래스 : 기저 클래스, 부모 클래스
** 구체적인(Concrete) 클래스 : 파생 클래스, 자식 클래스
OOP에서의 프로그램이란 ①객체를 만들고, ②객체간의 관계를 설정하고, ③객체 간의 통신 방법을 정하는 것인데, 이를 설계하는 데 있어서의 중요한 점은 유연성과 성능간의 Trade-Off입니다.
이런 것들을 고려한 다양한 Design Pattern이 등장했는데 이를 Design Pattern이라고 부릅니다. Design Pattern은 총 23가지로, C++을 활용한 다양한 예를 통해 살펴보고자 합니다.
<구성>
1. Fundamental
a. 생성자(Consturctor)
b. 선언(Declarations)과 정의(Definitions)
c. 가상함수(Virtual Function)
d. 정적 객체(static object)
2. Design Patterns1 : Based on Implementation
a. 변하는 것을 분리하기
b. 재귀적 포함
c. 중간층을 넣기
3. Design Patterns2 : Based on Objective
a. 객체를 만들기
b. 구조를 변경
c. 동작을 변경
글효과 분류1 : 코드
글효과 분류2 : 폴더/파일
글효과 분류3 : 용어설명
글효과 분류4 : 글 내 참조
글효과 분류5 : 글 내 참조2
글효과 분류6 : 글 내 참조3
1. Fundamental
Design Pattern을 설명함에 앞서서, C++에서 이해에 필요한 기초지식 몇가지를 먼저 정리하고자 합니다.
a. 생성자(Consturctor)
생성자에 대해 다들 아시겠지만 상속에 있어서의 생성자의 특징을 설명하고자 합니다. 먼저, 코드를 아래에 설명드립니다.
#include <string>
#include <iostream>
class Parent {
std::string member1;
int member2;
public:
Parent(const std::string& member1, int member2)
: member1(member1), member2(member2) {}
};
class Child : public Parent {
int member3;
public:
Child(const std::string& member1, int member2, int member3)
: Parent(member1, member2), member3(member3) {}
};
//*************************************************
int main()
{
Child child( "kim", 20, 15);
Parent parent("kim", 20);
}
1. Child의 생성자
- 당연히 부모의 default 생성자가 수행
ex) default 생성자 O : Child(){} 실제로 컴파일러가 Child():Parent(){}로 변환
ex) default 생성자 X : Child(int a){ } 실제로 컴파일러가 Child(int a) : Parent(){ } 로 변환 - 따라서 부모의 default 생성자가 없는 경우, 초기화를 위와 같이 구체적으로 해주어야합니다.
2. Protected 용도
- 자식(파생 클래스)의 객체는 상속받아 생성할 수 있지만, 부모가 자기 자신은 생성할 수 없도록
b. 선언(Declarations)과 정의(Definitions)
1. 선언(Declarations) : 컴파일러가 참조할 식별자(identifier)와 이름 정보를 알려주는 행위입니다. 추상적으로 존재하는 객체(Object)가 생성되며, 메모리의 실체는 없습니다.
- 함수 선언 : 함수의 본문 없이 선언만 하는 경우를 선언이라고 합니다.
- 클래스 선언 : 아래와 같이 내용 없이 선언하는 "전방선언"의 경우 클래스의 선언 이라고 합니다.
- 변수 선언 : 변수가 존재할 수 있음을 알리는 경우를 변수 선언이라고 합니다. Primitive Built-in Types의 경우 일반 extern이 아닌 경우, 정의에 선언이 포함됩니다.
// Function Declaration
int add(int a, int b);
// Class Declaration
class MyClass;
// Variable Declaration1 : static member variable
class MyClass
{
public:
static int m_value;
}
// Variable Declaration2
extern int value;
2. 정의(Definitions)
- 함수 정의 : 함수의 본체를 만들어내는 과정으로, 어떤 내용을 가지는지 정의합니다.
** 호이스팅(Hoisting) : 함수는 아래쪽에 선언했지만 그 위에 호출을 해도 함수가 실행되도록 하는 것 - 클래스 정의 : 클래스의 본체를 만들어내는 과정으로, 어떤 내용을 가지는지 정의합니다.
- 변수 정의 : 객체(Object)가 가진 실제 메모리를 할당해 물리적으로 인스턴스(Instance)화 하는 과정입니다.
** 이때 인스턴스화 하더라도 "객체"라고 부르기도 합니다.
// Function Definition
int add(int a, int b){
return a+b;
}
// Class Definition
class MyClass {
int a;
int b;
};
// Variable Definition1 : static member variable
int MyClass::m_value;
// Variable Definition2
int value;
// Variable Definition3 : Class
MyClass objName;
3. 초기화(initialization) : 할당(Assignment)을 통해 메모리에 실제 원하는 값을 넣는 것을 초기화라고 하며, 초기화를 하지 않으면, default값(0 혹은 쓰레기 값)으로 정의됩니다. 정의 당시에 초기화가 포함되는 경우도 있습니다.
// Variable Initialization 1 : static member variable
int Something::m_value = 1;
// Variable Initialization2
int value = 1;
// Variable Initialization3 : Class
MyClass class_value(5);
c. 가상함수(Virtual Function)
가상함수에 대한 개념이 Design Pattern에 많이 나오기 때문에 살펴보도록 합니다.
class Parent
{
public:
// non-virtual : 포인터 타입으로 함수 호출을 결정. 빠르다.
void cry1() {}
// virtual : 포인터 타입이 아닌 메모리 조사후 함수를 결정
// 느리다.
virtual void cry2() {}
};
class Child : public Parent
{
public:
void cry1() {}
virtual void cry2() {}
};
//*************************************************
int main()
{
Child child;
Parent* parent = &child;
parent->cry1(); // call Parent::cry1
parent->cry2();
}
1. Upcasting
- 자식(파생 클래스)의 객체를 부모(Base 클래스) Pointer로 Casting하는 경우입니다.
- Upcasting한 경우 Base 클래스의 변수에만 접근이 가능합니다.
2. Downcasting
Parent *parent = new Child;
// Downcasting
static_cast<Child*>(parent)->cry2();
dynamic_cast<Child*>(parent)->cry2();
- 다시 파생 클래스의 Pointer로 casting하는 경우, 개발자가 "원래 정의할때의 객체 타입에 대해 이미 알고 있거나", "원래 정의한 타입이 뭔지 모르고 있거나" 일 것 입니다.
- static_cast<자식*> vs dynamic_cast<자식*>
- static_cast : 컴파일 타임에 타입을 결정합니다. 타입 변환이 강제되기 때문에, 메모리 상의 Child의 타입에 대해 확실한 경우에 사용합니다.
- dynamic_cast : 런타임에 타입을 결정합니다. 타입을 확인 후에 진행하기 때문에, 어떤 타입으로 처음에 정의했는지 모를 때 사용합니다. static_cast보다는 느리기 때문에, 추천드리지 않습니다.
3. Virtual Function
- 함수가 override 된 경우, 부모 클래스에 Virtual이 붙었는지에 따라 클래스 Pointer에서 실행할 함수가 달라집니다.
- virtual로 실행하는 경우, 정의 시의 메모리를 조사한 다음 실행하므로 상대적으로 느립니다.
부모 클래스의 함수 | 자식 클래스의 함수 | 동작 | 특징 |
No Virtual | No Virtual/Virtual | Pointer Type에 맞게 실행 | |
Virtual | No Virtual/Virtual | 정의 시의 객체의 Type을 동적으로 확인해 실행 | Virtual을 적지 않아도 자동으로 Virtual이 됩니다. |
No Virtual/Virtual | X(No Function) | 상속되므로, 부모 클래스의 동작 그대로 실행 |
- override라는 키워드를 적어주면 virtual 상속을 의미합니다. 적지 않으면 새로운 가상함수의 정의라고 알 수도 있으므로, 적는 것을 권장합니다.
ex) virtual function() override{}
ex) function override{} - 소멸자의 경우 (소멸자 함수 호출 → 메모리삭제) 이므로, 위와 같은 함수로 동작합니다. 따라서 Base Class의 함수는 가상함수로 항상 선언 해주어야, 소멸자 호출시 객체의 Type을 확인하고 제대로 소멸자를 부를 수 있습니다.
class Parent
{
public:
virtual ~Parent() {}
};
class Child : public Parent
{
public:
Child() {}
~Child() {}
};
int main() {
Parent* p = new Child;
delete p;
}
- 위에 설명한 바와 같이, 가상함수를 통해 다양한 클래스가 다양한 기능을 할 수 있는 것을 Polymorphism(다형성) 이라고 합니다. 이렇게 구현한다면, 새로운 것이 추가해도 if(case==30)과 같이 특정 코드를 만들어내는 것이 아니기 때문에 OCP를 만족하기도 합니다.
3. Interface와 추상클래스
- Abstract 클래스 : 순수 가상 함수가 하나 이상인 것
** 순수 가상함수 : 구현부가 없고 =0 으로 표기된 것
class Parent
{
public:
virtual void MyFunction1(){};
virtual void MyFunction2() = 0;
protected
virtual ~Parent() {}
};
- 인터페이스(Interface) : 순수 가상함수로만 이루어진 Abstract 클래스입니다. 보통 편의를 위해 struct로 표현합니다.
struct Parent
{
virtual void MyFunction1() = 0;
virtual void MyFunction2() = 0;
virtual ~Parent() {}
};
- 두개 다 객체를 만들 수는 없으며, 포인터 변수는 당연히 만들 수 있습니다.
- 순수 가상함수를 통해 구현하면 구현이 "필수"이기 때문에 아래와 같이 다른 방법을 사용하기도 합니다. 이렇게 하면, 재정의 안하면 사용할 수 없지만, 매번 사용 안하는데도 재정의를 할 필요는 없어집니다.
class not_implementation {};
class Parent {
public:
virtual Parent* clone()
{
throw not_implementation();
}
virtual ~Shape() {}
};
4. 강한 결합과 약한결합
- 강한 결합(tightly coupling) : 어떤 클래스가 다른 클래스를 활용할 때 해당 클래스의 이름을 직접적으로 사용하는 것을 강한 결합이라고 합니다.
- 약한 결합(loosely coupling) : 어떤 클래스가 다른 클래스를 활용할 때 Abstract 클래스 혹은 Interface를 활용해 파생 클래스들을 호출 하는 것을 약한 결합이라고 합니다. 아래 코드는 약한 결합을 활용해 구현한 MyClass의 CallFunction입니다.
struct Parent {
virtual void InnerFunction() = 0;
virtual ~Parent() {}
};
class Child1 : public Parent {
public:
void InnerFunction() {}
};
class Child2 : public Parent {
public:
void InnerFunction() {}
};
class MyClass {
public:
void CallFunction(Parent* p) { p->InnerFunction(); }
};
d. 정적 객체(static object)
역시나 많이 활용될 Static의 개념에 대해 설명하고자합니다. static 변수 자체에 대해 먼저 살펴보고, static 멤버 함수와 static 멤버 변수를 살펴봅니다.
1. Static 변수 vs Global 변수
- static 변수와 global 변수는 모두 프로그램이 죽을 때 까지가 Life time 입니다.
- 접근 범위는 static 변수는 해당 파일에서만 접근이 가능하며, global 변수는 다른 파일에서도 접근이 가능합니다.
** global의 경우 extern으로 얻을 수 있습니다. - Static 변수의 경우 초기화 시점에 메모리에 올립니다.
2. Static 변수 - C문법 : 함수 내부에 선언해도 한번만 선언 & 정의 & 초기화 되며, 프로그램 종료시까지 유지됩니다.
class Parent{
public:
Parent(){
static int Number;
}
}
3. Static 멤버 변수 - C++문법 : 동일한 객체 간의 공유 가능한 전역 변수처럼 사용할 수 있습니다. 이는 클래스 외부에서 초기화 해주어야 합니다. Private이더라도 외부에서 초기화가 가능하며,
class Parent{
public:
static int Number;
Parent(){}
}
int Parent::Number;
4. Static 멤버 함수 - C++문법 : 객체를 선언하지 않고도 Namespace를 가지고 호출이 가능해집니다. 단, static 멤버 변수가 아닌, 멤버 변수에의 접근이 불가능합니다.
class Parent{
public:
Parent(){}
static MyFunction(){}
}
int main(){
Parent::MyFunction();
}
2. Design Patterns1 : Based on Implementation
1994년 4명의 저자가 만든 책이름인 "GOF's Design Pattern"에서 언급된 용어로, 자주 사용되는 코딩 스타일 23가지를 정리한 개념입니다.
이런 Design Pattern 중 구현이 비슷한 개념끼리 구분하기가 어려운 경우가 있어, 구현이 비슷한 개념끼리 모아 11가지를 먼저 소개드리려고 합니다.
a. 변하는 것을 분리하는 기술 (5가지)
- Virtual Function으로 구현하는 경우 : template method, factory method
- 다른 Class를 활용해 구현하는 경우 :
- 객체의 알고리즘을 교체 : strategy
- 객체의 모든 동작을 교체 : state
- 다른 표현을 가질 수 있는 복잡한 객체 만들기 : builder
b. 재귀적 포함을 사용하는 기술 (2가지)
- 복합 객체 : composite
- 객체에 기능을 추가 : decorator
c. 중간층을 넣어서 문제를 해결하는 것 (4가지)
- 인터페이스를 교체 : adapter
- 구현부와 인터페이스 분리 : bridge
- 다수 객체에 대한 하나의 인터페이스 : facade(파사드)
- 중간 대리 역할 : proxy
a. 변하는 것을 분리하기
공통성을 가지는 부분과 가변성을 가지는 부분을의 분리해서, 가변성이 있는 부분에 자유도를 주는 방법입니다. 실행시간에 변경될 필요가 없는 정책은 Virtual Function을 활용해 구현하고("상속" 활용), 실행시간에 변경될 필요가 있는 정책은 다른 클래스를 활용해 구현하는 방법("포함" 활용, "약한 결합" 활용)으로 나뉩니다.
1. Virtual Function - ① : template method
Virtual Function을 활용해 가변성을 구분하는 가장 기본적인 패턴입니다. 실행시간에 변경될 수 없습니다.
아래 코드와 같이 Virtual Function을 활용해 Mutable 한부분을 분리해서, 자식이 결정할 수 있게 남겨 두었습니다.
#include <iostream>
#include <string>
#include <conio.h>
class Parent {
public:
virtual bool MutableFunction(char c) {
return true;
}
bool ImmutableFunction() {
char c;
return Mutable(c);
}
};
class Child : public Parent {
public:
bool MutableFunction(char c) override {
return isdigit(c);
}
};
int main() {
Child e;
e.ImmutableFunction();
}
2. Virtual Function - ② : factory method
(factory 구현 기법과 무관합니다)
제품의 군을 만들기 위한 인터페이스를 만들고, 어떤 제품들을 만들지는 파생클래스가 결정하도록 하는 것인데, 이중에 파생 클래스가 결정하는 것을 factory method 패턴이라고 합니다. template method 와 동일한 구조이지만(사실상 동일), "알고리즘, 정책"의 변경이 아닌 "객체의 종류"를 결정한다는 것이 다르다.
class Parent {
public:
void init() {
IButton* btn = CreateButton();
IClip* clip = CreateClip();
}
virtual IButton* CreateButton() = 0;
virtual IClip* CreateClip() = 0;
};
class Child : public Parent {
public:
IButton* CreateButton() { return new MyButton; }
IClip* CreateClip() { return new MyClip; }
};
3. Another Class - ① : strategy
1번 template method 패턴과 다르게, 정책을 담은 클래스를 전달함으로써 가변성을 구분하는 방법입니다. Interface 클래스를 기반으로 설계 되어야 하며, 위에 언급한 바와 같이 약한 결합으로 구현합니다.
아래 코드와 같이 MyClass를 따로 구현해서 내부의 Mutable한 부분을 val로 구분할 수 있습니다.
#include <iostream>
#include <string>
#include <conio.h>
struct Parent {
virtual bool MutableFunction1(const std::string& s) = 0;
virtual bool MutableFunction2(const std::string& s) { return true; }
virtual ~Parent() {}
};
class Child : public Parent {
int policy;
public:
Child(int n) : policy(n) {}
bool MutableFunction1(const std::string& s) override {
return s.size() < policy;
}
bool MutableFunction2(const std::string& s) override {
return s.size() == policy;
}
};
class MyClass {
Parent* val = nullptr;
public:
void setPolicy(Parent* p) { val = p; }
std::string ImmutableFunction() {
std::string data;
val->MutableFunction1(data);
val->MutableFunction2(data);
return data;
}
};
int main() {
MyClass e;
Child v(5);
e.setPolicy(&v);
e.ImmutableFunction();
Child v2(15);
e.setPolicy(&v2);
e.ImmutableFunction();
}
-----------------------------------------------------------------------------------------------------
<단위 전략(Policy-Base Design) >
하지만 이 방식의 경우 ImmutableFunction()을 불렀을 때 가상함수를 활용해 동작하기 때문에, 빈번하게 호출되는 경우에는 Overhead가 발생합니다.
이외에도 디자인 패턴이 아닌 C++ Idioms에는 단위 전략(Policy-Base Design) 혹은 매개변수화 타입(Parametrized Type)을 이용한 재사용 기법 이라는 것이 있습니다. (참조 : 2002년 발매된 "Modern C++ Design" 서적)
Strategy 패턴처럼 다른 클래스를 활용하지만, Interface 클래스를 활용하는 것이 아닌 아래와 같은 Inline 함수를 가진 클래스를 활용해, template으로 할당하는 방법입니다. Template method 패턴 처럼 실행시간에 정책 교체가 안되지만 빠릅니다.
C++ STL은 대부분 이것으로 구현되어있습니다.
class Parent
{
public:
inline void MutableFunction1() { };
inline void MutableFunction2() { };
};
template<typename T, typename MutableModel = Parent>
class List
{
MutableModel val;
public:
void ImmutableFunction(const T& a){
val.MutableFunction1();
val.MutableFunction2();
}
};
-----------------------------------------------------------------------------------------------------
4. Another Class - ② : state
객체가 사용하는 알고리즘이나 정책을 런타임에 교체하기 위한 방법이라면, State패턴은 상태에 따라 동작을 변경하는 패턴입니다. 포함을 활용하기 때문에 객체는 변하지 않지만 모든 동작이 변경되어 마치 다른 클래스를 사용하는 것 처럼 보이게됩니다.
// 1_State1 - 182 page
#include <iostream>
struct IAction {
virtual void function() = 0;
virtual ~IAction() {}
};
class State1 : public IAction {
public:
void function() {}
};
class State2 : public IAction {
public:
void function() {}
};
class MyClass {
IAction* action = nullptr;
public:
void set_action(IAction* a) { action = a; }
void function() { action->function(); }
};
int main() {
State1 ed;
State2 ni;
MyClass* p = new MyClass;
p->set_action(&ed);
p->function();
p->set_action(&ni);
p->function();
}
5. Another Class - ③ : builder
동일한 방법으로 객체들을 만들 수 있지만, 내용이 조금씩 다른 객체들을 만들 때 사용하는 패턴입니다. Strategy가 알고리즘&정책, state가 동작이라면 builder는 공정을 변경하는 방법입니다. 예를 들어 캐릭터의 옷,모자,신발 들을 만드는 것은 모든 캐릭터가 똑같지만 여러가지 종류의 캐릭터가 있을때, 옷,모자,신발을 만드는 공정을 인터페이스로 교체하는 방법입니다.
#include <iostream>
#include <string>
class Thing1{}
class Thing2{}
class Thing3{}
struct IBuilder {
virtual Thing1 makeThing1() = 0;
virtual Thing2 makeThing2() = 0;
virtual Thing3 makeThing3() = 0;
virtual ~IBuilder() {}
};
class thingBuilder : public IBuilder {
public:
Thing1 makeThing1() override { return Thing1(); }
Thing2 makeThing2() override { return Thing2(); }
Thing3 makeThing3() override { return Thing3(); }
};
class Target{
Thing1 t1;
Thing1 t2;
Thing1 t3;
}
class TargetBuilder {
IBuilder* builder = nullptr;
public:
void set_builder(IBuilder* b) { builder = b; }
Target construct() {
Target c;
c.t1 = builder->makeThing1();
c.t2 = builder->makeThing2();
c.t3 = builder->makeThing3();
return c;
}
};
int main() {
thingBuilder k;
TargetBuilder d;
d.set_builder(&k);
Target c = d.construct();
}
b. 재귀적 포함
복합객체가 개별객체와 복합객체 모두를 보관 가능하도록 구현하는 것을 "재귀적 포함"이라고 합니다. 예를 들어, 폴더는 파일을 포함할 수도 있고, 또다시 폴더를 포함할 수 있습니다. 이름에서 나타나듯이 "상속"이 아닌 "포함"이므로, strategy 패턴 처럼 다른 클래스를 활용합니다.
1. composite
공통의 부모 클래스를 통해 사용법을 동일화 하는 재귀적 포함의 가장 기본적인 패턴입니다.
아래와 같이 동일한 추상 클래스 Parent를 상속한 두개의 Child 중 복합객체인 ChildBig과 개별객체인 ChildSmall을 구현한 내용입니다.
#include <string>
#include <vector>
class Parent
{
std::string title;
public:
virtual ~Parent() {}
Parent(const std::string& title) : title(title) {}
virtual void function() = 0;
};
class ChildSmall : public Parent
{
public:
ChildSmall(const std::string& title) : Parent(title){}
void function() {}
};
class ChildBig : public Parent
{
std::vector<Parent*> v;
public:
ChildBig(const std::string& title) : Parent(title) {}
void add_big_small(Parent* m) { v.push_back(m); }
void function(){}
};
int main()
{
ChildBig* root = new ChildBig("root");
ChildBig* pm1 = new ChildBig("Bigstring1");
ChildBig* pm2 = new ChildBig("Bigstring2");
root->add_big_small(pm1);
root->add_big_small(pm2);
pm1->add_big_small(new ChildSmall("Smallstring1"));
pm1->add_big_small(new ChildSmall("Smallstring2"));
pm1->add_big_small(new ChildSmall("Smallstring3"));
root->function();
}
2. decorator
일반적으로는 기능을 추가하고 싶을 때 상속을 활용하지만, 객체에 동적으로 기능을 추가하고 싶을 때 사용하는 패턴입니다. 기능의 재귀적 포함을 사용하며, Interface가 필요합니다.
C# Stream Decorator가 대표적인 Decorator 패턴입니다(+Adapter패턴).
아래 코드를 보시면 Child 클래스에 기능을 추가한 ChildAdded를 구현하기 위한 Interface인 Parent가 필요합니다.
#include <iostream>
#include <string>
#include <thread>
#include <chrono>
using namespace std::literals;
struct Parent {
virtual void function() = 0;
virtual ~Parent() {}
};
class Child : public Parent {
std::string name;
public:
Child(const std::string& name) : name(name) {}
void function() {/*--basic--*/}
};
class ChildAdded : public Parent {
Parent* parent;
public:
ChildAdded(Parent* p) : parent(p) {}
void function() {
std::cout << "===============" << std::endl;
parent->function();
std::cout << "===============" << std::endl;
}
};
int main() {
Child pic("abc");
ChildAdded val(&pic);
val.function();
}
c. 중간층을 넣기
클래스간의 연결에 있어서 뭔가 변경을하고 싶을 때의 패턴을 모았습니다. 아래 4가지 패턴은 중간층을 넣는 다는 것이 비슷하지만, 목적에 따라 여러가지로 분류되며 각각 포함(다른 Class)을 활용하거나 상속을 활용합니다.
1. adapter
이미 존재하는 객체의 인터페이스(I/O)를 변경하는 패턴이며, 상속과 포함 두가지 adapter가 존재합니다.
- Class Adapter : 상속을 활용하는 방법입니다.
- Object Adapter : 포함을 활용하는 방법입니다.
** 상속보다는 유연성이 좋은 포함을 활용하는 방법을 항상 먼저 고려하는 것을 추천합니다.
아래는 Class Adapter를 간단히 보였습니다. 위에서 언급한 바와 같이 C# Stream Decorator은 Adapter와 Decorator 패턴을 활용했으며, STL의 vector, list, deque 같은 Sequence Container는 Class Adapter을 활용하고 있습니다.
class FromClass {
public:
FromClass(){}
void FromFunction() {}
};
class ToClass {
public:
virtual ~ToClass() {}
virtual void ToFunction() = 0;
};
class MyClass : public FromClass, public ToClass
{
public:
MyClass(){}
void ToFunction() override { FromClass::FromFunction(); }
};
-----------------------------------------------------------------------------------------------------
<Private 상속>
부모 클래스의 public들을 private로 상속해 자식 클래스가 쓸 것이지만, 인터페이스로는 노출하지 않고 싶을 때 사용합니다.
아래 예에서 stack은 list의 push_back은 쓰고 싶지않지만, push라는 명령어를 직접 만들고 싶을 때입니다.
template<typename T>
class stack : private std::list<T>
{
public:
void push(const T& a) { std::list<T>::push_back(a); }
void pop() { std::list<T>::pop_back(); }
T& top() { return std::list<T>::back(); }
};
-----------------------------------------------------------------------------------------------------
2. bridge
상속과 포함을 모두 활용해 부모 클래스의 추상화 개념의 독립적 변형 혹은 업데이트를 가능하게 하는 방법입니다. 이로인해 부모 클래스의 수정이 필요한 경우 쉽게 대응 가능하며, 새로운 기능의 추가도 가능합니다.
즉, 아래 그림의 경우 MyClass는 Child2를 활용하고 있는데, Parent가 변경되어도 MyClass를 수정할 일은 없으며, 새로운 요구사항이 있더라도 Child2가 새로운 함수를 통해 만들어낼 수 있습니다. 이를 "컴파일 방화벽"이라고 하기도 하는데, 인터페이스와 구현부의 상호 연관성을 약하게 함으로써 다시 컴파일 되는 부분이 적어져 컴파일 속도가 빨라지고, 정보 은닉에 유리해집니다. C++ Idioms 의 PIMPL(Pointer to IMPLmentation)이라는 기법도 참조 바랍니다.
아래는 위 그림의 예시입니다.
#include <iostream>
struct Parent {
virtual void function1() = 0;
virtual void function2() = 0;
virtual ~Parent() {}
};
class Child : public Parent {
public:
void function1() {}
void function2() {}
};
class Child2 {
Parent* val;
public:
Child2(Parent* p) : val(p) {
if (val == 0)
val = new Child;
}
void function1() { val->function1(); }
void function2() { val->function2(); }
void NewFunction() {
function1();
function2();
}
};
class MyClass {
public:
void use(Child2* p) { // Bridge Pattern
p->function1();
p->function2();
p->NewFunction();
}
};
int main() {
MyClass p;
Child2 use;
p.Use(&use);
}
3. facade(파사드)
주로 포함을 활용해 어떤 System의 Sub-System에 대한 다양한 객체에 대해 하나의 interface를 제공하는 방법입니다.
예를 들어 기존에 TCP서버를 구현하는 경우 아래와 같이 주절주절 너무 함수가 많은 것 같습니다.
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <iostream>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
using namespace std;
int main()
{
// 1. 네트워크 라이브러리 초기화
WSADATA w;
WSAStartup(0x202, &w);
// 2. 소켓 생성
int sock = socket(PF_INET, SOCK_STREAM, 0); // TCP 소켓
// 3. 소켓에 주소 지정
struct sockaddr_in addr = { 0 };
addr.sin_family = AF_INET;
addr.sin_port = htons(4000);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
bind(sock, (SOCKADDR*)&addr, sizeof(addr));
// 4. 소켓을 대기 상태로변경
listen(sock, 5);
// 5. 클라이언트가 접속할때 까지 대기
struct sockaddr_in addr2 = { 0 };
int sz = sizeof(addr2);
accept(sock, (SOCKADDR*)&addr2, &sz);
// 6. socket 라이브러리 cleanup
WSACleanup();
}
위를 몇개의 클래스로 정리하면 아래와 같이 간결하게 만들 수 있습니다. 이와 같이 하위 클래스의 복잡함을 단순화하는 상위 클래스를 제공하는 것을 파사드 패턴이라고 합니다.
int main() {
TCPServer server;
server.Start("127.0.0.1", 4000);
}
-----------------------------------------------------------------------------------------------------
<Class Member 초기화>
아래는 일반적으로 가능
int main() {
int a = 0;
int b(0);
int c{0};
}
아래는 클래스 멤버에서 가능. 두번째가 안되는 이유는 함수로 인식하기 때문입니다.
class {
int a = 0;
//int b(0); // Not Possible
int c{0};
}
-----------------------------------------------------------------------------------------------------
4. proxy
주로 상속을 활용해 다른 객체에 접근하기 위해 중간 대리 역할을 하는 객체를 만드는 방법입니다. Proxy의 종류에는 4가지가 있습니다.
- Remote Proxy : 원격 서버에 대한 중간 대리
- Smart Reference Proxy : Smart Pointer와 같이 참조에 대한 중간 대리
- Virtual Proxy : 생성하기 힘든 복잡한 자원에 대한 중간 대리
- Protect Proxy : 접근 권한이 필요한 자원에 대한 중간 대리
이 중 Remote Proxy를 예로 보이면 아래와 같습니다. 아래와 같이 구현하는 경우 서버가 교체될때 Proxy가 동적으로 변경되어 업데이트 되는 특징이 필요하기도 합니다.
아래 코드는 Proxy의 내용을 담은 독립적인 myfile.cpp를 나타냅니다. find_server()는 remote에 있는 server를 ID를 활용해 찾는 임의의 코드이며, send_server()는 서버에 API를 통해 IPC(Inter-Process Communication)하는 과정입니다. Proxy는 서버의 대행자이므로, 해당 객체를 만들기 위한 함수 또한 createProxy()와 같이 제공되어야합니다.
** extern "C" : C++ 소스에서 선언한 전역 변수나 함수를 C에서 사용해야 할 경우
** __declspec(dllexport) : 함수,변수 클래스를 Windows 기반 dll파일에서 공개할 때 필요합니다.
** std::atomic<int> : 산술 연산들을 atomic하게 수행할 수 있도록 해 주는 템플릿 클래스
** 멤버함수의 delete this; : 나의 클래스 객체를 없앨 수 있습니다.
#include <atomic>
struct Parent {
virtual int ServerAPI(int a, int b) = 0;
virtual void Atomicfunction() = 0;
virtual void Release() = 0;
virtual ~Parent() {}
};
class Child : public Parent {
int server;
std::atomic<int> something = 0;
public:
~Child() {}
Child() { server = find_server("ServerID"); }
int ServerAPI(int a) { return send_server(server, a); }
void Atomicfunction() { ++something; }
void Release() { if (--something == 0) delete this; }
};
extern "C" __declspec(dllexport)
Parent * createProxy()
{
return new Child;
}
-----------------------------------------------------------------------------------------------------
<정적 라이브러리 와 동적 라이브러리>
1. 정적 라이브러리(*.a, *.lib) : link 단계에서 라이브러리(*.lib 파일)를 실행 바이너리에 포함. 불필요하게 바이너리의 용량이 커지며, Main Memory 효율이 떨어지지만 독립적이게 구현할 수 있습니다/.
2. 동적 라이브러리(*.so, [*.lib, *.dll]) : link 단계에서 사용하고자 하는 실행 바이너리에서 필요시 사용할 수 있도록 최소한의 정보만 포함.
-----------------------------------------------------------------------------------------------------
windows를 활용하기 위한 Dynamic Link Library를 만들기 위해 cl myfile.cpp /LD /link user32.lib gdi32.lib kernel32.lib 혹은 g++ -shared myfile.cpp -o myfile.dll 명령어를 사용해 컴파일 할 수 있습니다.
Client에서는 위의 코드를 아래와 같이 불러 들여 사용할 수 있습니다. 아래의 load_function은 해당 DLL에서 createProxy함수를 얻어내는 임의의 함수를 나타냅니다.
** 사실 아래와 같이 구현하려면 Parent 클래스에 대한 interface를 따로 header로 뽑아주어야 하지만, 이미 따로 만들었다고 가정하고 만들었습니다.
아래의 예를 활용하면 while()문 등을 통해 proxy 서버를 동적으로 update하며 유지할 수 있습니다.
Parent* load_proxy() {
using F = Parent* (*)();
F f = (F)load_function("myfile.dll", "createProxy");
return f();
}
int main() {
Parent* found_proxy = load_proxy();
int n1 = found_proxy->ServerAPI(10);
}
3. Design Patterns2 : Based on Objective
이번엔 23개의 디자인 패턴을 "목적"에 따라 구분하면 아래와 같습니다. 위에 설명한 것을 파란색으로 표현했습니다.
a. 객체를 만들 때 사용하는 생성 패턴 (5가지)
- builder, factory method, abstract factory, singleton, prototype,
b. 구조를 변경하는 패턴 (7가지)
- 재귀적 포함 : composite, decorator
- 간접층 : adapter, bridge, facade, proxy
- 공유 : flyweight
c. 동작을 변경하기 위한 패턴 (11가지)
- 변하는 것 분리 : state, strategy, template method
- 명령의 객체화 : command
- 열거, 통보, 방문, 전가 : iterator, visitor, observer, chain of responsibility
- 객체 저장 : memento
- 중재자 : mediator
- 컴파일러 :
interpreter
interpreter의 경우 컴파일러등의 문맥 분석기 만들때 사용한다고 하는데, 지금은 사용되지 않는 패턴이기 때문에 제외하고 위에 설명한 11가지를 배제한 11가지를 추가로 설명드리겠습니다.
a. 객체를 만들기
1. builder : 위 설명
2. factory method : 위 설명
3. abstract factory
factory를 만들 때도 추상 클래스(부모 클래스) 혹은 Interface를 활용해, 같은 Interface를 가지는 다양한 Factory를 만들는 패턴입니다. 제품의 군을 만들기 위한 인터페이스를 만들고, 어떤 제품들을 만들지는 파생클래스가 결정하도록 하는 것인데, 이중에 Interface를 만드는 것을 abstract factory 패턴이라고 합니다.
** factory의 create를 활용하면 Child1 object 혹은 new Child1 과 같이 제약이 많은 방법 외에도 다양한 방법으로 구현할 수 있습니다.
4. singleton
오직 한개의 어디서든 동일한 방법으로 접근 가능한 객체를 만드는 방법입니다(전역 변수와 비슷).
아래 코드는 "Mayer's Singleton"라고 불리며 Static 지역변수를 활용한 방법입니다. Static Member Data는 밖에서 초기화 해주어야 하기 때문에 프로그램이 최초 실행시 생성되지만, 아래의 경우는 Lazy Singleton형식으로 생성됩니다.
** Singleton(const Singleton&) = delete; : 복사생성자를 컴파일러가 자동으로 만들지 못하게 하는 방법입니다.(singleton 이므로)
** Singleton& operator=(const Singleton&) = delete; : 대입연산자를 컴파일러가 자동으로 만들지 못하게 하는 방법입니다. (singleton 이므로)
#include <iostream>
class Singleton {
private:
Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
static Singleton& get_instance() {
static Singleton instance;
return instance;
}
};
int main(){
Singleton& c1 = Singleton::get_instance();
}
-----------------------------------------------------------------------------------------------------
<Mutex를 반영한 코드>
Concurrent System에서 Singleton이 만들어 질 때, 동시 접근하면 오류가 생길 수 있어 아래와 같이 해주어야 합니다.
** class MyClass: public Singleton< Mouse > : C++ IDioms에 포함된 CRTP(Curiously Recurring Template Pattern) 기술로, 부모 클래스에서 미래에 만들어질 파생 클래스 이름을 사용할수 있게 하는 기술입니다.
** lock_guard 구현 : 자원의 할당과 반납을 직접하지 않고, 안전하게 생성자와 소멸자에 의존하는 방법으로 RAII(Resource Acquisition Is Initialization)라는 기술입니다.
** [참조] DCLP(Double Check Locking Pattern)라는 패턴이 있는데, lock 전에 null인지 하번더 확인하는 방법이 있지만, 실제 컴파일러 버그로 현재는 사용되지 않습니다.
#include <iostream>
#include <mutex>
template<typename T> class lock_guard {
T& mtx;
public:
lock_guard(T& m) : mtx(m) { mtx.lock(); }
~lock_guard() { mtx.unlock(); }
};
template<typename T>
class Singleton {
protected:
Singleton() {}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static std::mutex mtx;
static T* instance;
public:
static T& get_instance() {
lock_guard<std::mutex> g(mtx);
if (instance == nullptr)
instance = new T;
return *instance;
}
};
template<typename T> T* Singleton<T>::instance = nullptr;
template<typename T> std::mutex Singleton<T>::mtx;
class MyClass : public Singleton< Mouse > {
public:
};
int main(){
MyClass& c1 = MyClass::get_instance();
}
-----------------------------------------------------------------------------------------------------
5. prototype
견본객체를 만들고 복사를 통해서 객체를 생성하는것으로, 기존에 객체를 만드려면 클래스의 이름이 직접적으로 필요한데, 이런 것들을 미리 함수 포인터 견본형태로 만들어 새로운 객체를 생성하는 방법입니다. 예를 들어, "폰트 크기4인 빨간색 글씨"와 같이 내가 자주 쓰는 내용을 저장해두는 것입니다.
아래는 prototype을 활용한 factory를 구현한 코드입니다.
** factory의 create를 활용하면 Child1 object 혹은 new Child1 과 같이 제약이 많은 방법 외에도 다양한 방법으로 구현할 수 있습니다.
** return new Child1(*this) : 나의 클래스를 복사생성자를 활용해 return 합니다.
#include <iostream>
#include <vector>
#include <map>
class Parent {
public:
virtual void draw() = 0;
virtual ~Parent() {}
virtual Parent* Clone() = 0;
};
class Child1 : public Parent {
public:
void draw() override {}
static Parent* Create() { return new Child1; }
Parent* Clone() override { return new Child1(*this); }
};
class ParentFactory {
MAKE_SINGLETON(ParentFactory)
std::map<int, Parent*> storage;
public:
void Register(int key, Parent* prototype) {
storage[key] = prototype;
}
Parent* Create(int type) {
Parent* p = nullptr;
auto ret = storage.find(type);
if (ret != storage.end()) {
p = ret->second->Clone();
}
return p;
}
};
int main() {
std::vector<Parent*> v;
ParentFactory& factory = ParentFactory::getInstance();
Child1* object = new Child1;
factory.Register(1, object);
Parent* s = factory.Create(1);
}
b. 구조를 변경
1. 재귀적 포함-① : composite : 위 설명
2. 재귀적 포함-② : decorator : 위 설명
3. 간접층-① : adapter : 위 설명
4. 간접층-② : bridge : 위 설명
5. 간접층-③ : facade : 위 설명
6. 간접층-④ : proxy : 위 설명
7. 공유 : flyweight
사용 객체들이 어떤 작은 객체들을 공유하게 만드는 방법입니다. 예를 들어 아래 예의 경우, Word같은 곳에서 Font와 같이 자주 쓰는 객체들을 공유하게 하는 방법입니다.
아래는 자기 자신 객체를 만드는 static 멤버함수를 활용해서 storage라는 map에 저장해두고 사용합니다. IOS 개발에 사용되는 Cocoa touch 가 아래 처럼 사용합니다.
아래처럼 구현하는 방법외에도, 상속을 활용해 자식 클래스에서 map을 유지하는 경우도 있습니다.
#include <iostream>
#include <string>
#include <map>
class Flyweight {
std::string key;
Flyweight(const std::string& key) : key(key){}
public:
static std::map<std::string, Flyweight*> storage;
static Flyweight* Create(const std::string& key) {
Flyweight* img = nullptr;
auto p = storage.find(key);
if (p == storage.end()) {
img = new Flyweight(key);
storage[key] = img;
} else {
img = p->second;
}
return img;
}
};
std::map<std::string, Flyweight*> Flyweight::storage;
int main() {
Flyweight* object = Flyweight::Create("mykey");
}
-----------------------------------------------------------------------------------------------------
<Const 메모리에 대해서>
아래 코드에서 sa[]에 저장되는 "abcd"의 경우 stack에 저장되지만, p1이 가리키는 "efgh"는 상수메모리라는 곳에 저장됩니다. 따라서 해당 c++에서는 const pointer로 가리키지 않으면 에러가 납니다. 또한 map형태로 유지되므로 같은 내용의 const string은 공유됩니다.
int main() {
char sa[] = "abcd";
const char* p1 = "efgh";
}
<friend 클래스>
아래 코드와 같이 Friend1이 Friend2를 friend class 로 지정하면 Friend2에서 Friend1의 private을 사용할 수 있게 됩니다.
class Friend1 {
private :
string name;
friend class Friend2;
};
class Friend2 {
public :
void function(Friend1& f, string s) {
f.name = s;
}
};
-----------------------------------------------------------------------------------------------------
c. 동작을 변경
1. 변하는 것 분리-① : state : 위 설명
2. 변하는 것 분리-② : strategy : 위 설명
3. 변하는 것 분리-③ : template method : 위 설명
4. 명령의 객체화 : command
Event가 발생했을 때 처리하는 방식으로 모든 명령을 캡슐화하는 방법입니다. 호출하는 객체와 수행하는 객체를 분리해 제공하며, C#의 WPF, QT의 undo manager 등에서 사용되는 기술입니다.
아래는 ICommand라는 Interface를 통해 행위 자체를 만들고, 해당 Command의 상속클래스를 구현했습니다. 호출 객체는 command, 수행방법 구현 객체는 MyClass v입니다. 이런 행위들은 저장과 복구가 가능하도록 구현 할 수도 있습니다.
#include <iostream>
#include <vector>
#include <stack>
class MyClass {
public:
virtual void function(){/*--------*/}
virtual ~MyClass() {}
};
struct ICommand {
virtual void execute() = 0;
virtual bool can_undo() { return false; }
virtual void undo() {}
virtual ~ICommand() {}
};
class DerivedCommand : public ICommand
{
std::vector<MyClass*>& v;
public:
DerivedCommand(std::vector<MyClass*>& v) : v(v) {}
void execute() {
for (auto p : v)
p->function();
}
bool can_undo() { return true; }
void undo() { system("cls"); }
};
int main() {
std::vector<MyClass*> v;
ICommand* command = new DerivedCommand(v);
command->execute();
std::stack<ICommand*> cmd_stack;
cmd_stack.push(command);
}
-----------------------------------------------------------------------------------------------------
<이외에 Event를 처리하는 방식>
- Interface 기반 Listener 방식 : Android, Java에서 활용
ex) Java addEventListener()
** using IMenuListener::IMenuListener; : 생성자를 상속하는 코드입니다.
#include <iostream>
#include <string>
#include <vector>
#include <conio.h>
struct IMenuListener
{
virtual void on_command(int id) = 0;
virtual ~IMenuListener() {}
};
class Dialog : public IMenuListener
{
public:
using IMenuListener::IMenuListener;
void on_command(int id) override {
switch (id) {
case 11: break;
case 12: break;
}
}
void close() {}
};
class MyClass {
int id;
IMenuListener* listener = nullptr;
public:
MyClass(int id) : id(id) {}
void set_listener(IMenuListener* p) { listener = p; }
void command() {
if (listener)
listener->on_command( id );
}
};
int main()
{
Dialog dlg;
MyClass m1(11);
m1.set_listener(&dlg);
m1.command();
}
- Function Pointer 기반 방식 : C#, Objective-C, QT등이 활용
** using HANDLER = void(*)(); : typedef void(*HANDLER)() 와 같습니다.
#include <iostream>
#include <string>
#include <vector>
#include <conio.h>
void foo() { std::cout << "foo" << std::endl; }
class MyClass {
int id;
using HANDLER = void(*)();
HANDLER handler = nullptr;
public:
MyClass(int id):id(id) {}
void set_handler(HANDLER h) { handler = h; }
void command() {
if (handler)
handler();
}
};
int main()
{
MyClass m1(11);
m1.set_handler(&foo);
m1.command();
}
** Member Function을 표현하는 방법 : 아래와 같이 실행할 수 있으며, "pointer to member" 혹은 ".*"라는 연산자를 활용해 실행할 수 있습니다. 멤버함수와 일반함수를 같이 다루고 싶은 경우 이 "pointer to member" 연산자를 활용해도 좋고, 같은 Interface로 묶은 수행 개체를 만들어 활용할 수도 있습니다.
class Dialog {
public:
void close() { std::cout << "Dialog close" << std::endl; }
};
int main(){
void(Dialog::*f2)() = &Dialog::close;
Dialog dlg;
(dlg.*f2)();
}
-----------------------------------------------------------------------------------------------------
5. 열거, 통보, 방문, 전가-① : iterator
복합객체의 내부구조를 노출하지 않고도, 모든 요소를 동일한 방법으로 순차적으로 접근하는 객체를 의미합니다. 보통 C++ STL Container나 Java Collection을 iterate하기 위해 사용합니다.
아래를 구현할 때 interface와 virtual function을 활용해 구현하면, 여러번 호출되므로 느리고 virtual function을 사용하기 때문에 iterator가 pointer return을 해주어야 합니다(생성시는 new로 return). pointer를 return하면 나중에 사용자가 delete를 직접 해주어야합니다. 따라서 virtual function을 사용하지 않기 때문에 빠르고 자동으로 delete되게 할 수 있습니다.
** Java는 Garbage Collection(GC)때문에 저절로 delete됩니다.
** 멤버함수의 return *this; : 나의 클래스 객체를 return 하는 방법으로, 알아서 delete됩니다.
-----------------------------------------------------------------------------------------------------
<Pointer Return과 Value Return>
1. Pointer Return
- 불가능한 방법
Point* foo() {
Point pt;
return &pt;
}
- 가능한 방법
Point* foo() {
Point pt;
return new Point;
}
2. Value Return
- 가능한 방법1
Point foo() {
return Point();
}
- 가능한 방법2
Point foo() {
Point pt;
return pt;
}
-----------------------------------------------------------------------------------------------------
아래 코드는 slist Container와 이를 순환하기 위한 slist_iterator를 구현하기 위한 코드입니다. 앞서 언급했듯이, 다양한 Container들이 동일한 방법으로 순차적 접근이 가능해야하므로 기본 list와 같이 operator를 구현해주어야 합니다.
#include <iostream>
template<typename T> struct Node {
T data;
Node* next;
Node(const T& d, Node* n) : data(d), next(n) {}
};
template<typename T>
class slist_iterator {
Node<T>* current = nullptr;
public:
slist_iterator(Node<T>* p) : current(p) {}
inline slist_iterator& operator++() {
current = current->next;
return *this;
}
inline T& operator*() { return current->data; }
inline bool operator==(const slist_iterator& it) {
return current == it.current;
}
inline bool operator!=(const slist_iterator& it) {
return current != it.current;
}
};
template<typename T> struct slist
{
Node<T>* head = 0;
public:
void push_front(const T& a) { head = new Node<T>(a, head); }
slist_iterator<T> begin() {
return slist_iterator<T>(head);
}
slist_iterator<T> end() {
return slist_iterator<T>(0);
}
};
6. 열거, 통보, 방문, 전가-② : visitor
복합객체의 내부요소를 동일한 방법으로 순차적으로 연산하는 객체를 의미합니다. iterator와 다른 것은 "동일방식으로 방문하는 것이 아니라 연산한다" 정도가 다른 것 같습니다.
기존에 전통적인 객체지향 디자인에서는 기능을 추가하기 위해서 보통 상속을 활용해 기능을 추가하기 때문에 Interface에서 상속하는 Virtual Function을 추가하기는 어려울 때가 많습니다(포함을 활용하면 가능하지만). Visitor 패턴을 활용하면 어떤 연산자를 Visitor로 추가해줄 수 있어 Virtual Function을 추가하기 쉬워집니다.
- acceptor 객체에 visitor 들을 accept() 하면 acceptor 객체에서 visitor들을 바로 연산 실행합니다.
- 이때 accept()하면서, visitor들을 실행하는 것이 특징입니다.
** *this : 자신 클래스 객체를 의미합니다.
#include <iostream>
#include <list>
#include <vector>
template<typename T> struct IVisitor {
virtual void visit(T& e) = 0;
virtual ~IVisitor() {}
};
template<typename T>
class Visitor1 : public IVisitor<T> {
public:
void visit(T& e) override { e *= 2; }
};
template<typename T>
class Visitor2 : public IVisitor<T> {
public:
void visit(T& e) override { e *= 3; }
};
template<typename T> struct IAcceptor {
virtual void accept(IVisitor<T>* visitor) = 0;
virtual ~IAcceptor() {}
};
template<typename T>
class Acceptor : public std::list<T>, public IAcceptor<T> {
public:
using std::list<T>::list;
void accept(IVisitor<T>* visitor) override {
for (auto& e : *this)
visitor->visit(e);
}
};
int main() {
Acceptor<int> s = { 1,2,3,4,5,6,7,8,9,10 };
Visitor1<int> tv;
s.accept(&tv);
Visitor2<int> sv;
s.accept(&sv);
}
7. 열거, 통보, 방문, 전가-③ : observer
1:N의 의존관계를 정의해 중심 객체가 다른 객체들에게 notify하는 패턴입니다. 아래는 간단한 그림과 코드를 제공합니다.
- subject 객체에 observer 객체를 attach()하면, subject에 observer를 추가하고, observer에 subject를 등록합니다.
- subject의 edit()함수를 통해 수정하면 notify()함수를 통해 observer들에게 통보해줍니다.
- 이때 observer의 update() 함수를 활용합니다.
- observer는 update() 함수를 활용해 새로운 Subject의 값을 처리합니다.
- 아래 예중 ObserverChild2는 subject의 get_data()함수를 통해 subject의 다른 데이터들을 참조하기도 합니다.
** 이런 경우 실제 객체를 가리키기 위해 Observer 패턴 활용시 Down-casting이 많이 등장합니다. 사실 Down-Casting은 위험하기 때문에 좋지 않지만, 어쩔 수 없습니다.
#include <iostream>
#include <vector>
class SubjectParent {
std::vector<IGraph*> v;
public:
void attach(IGraph* p) {
v.push_back(p);
p->subject = this;
}
void detach(IGraph* p) {}
void notify(int data) {
for (auto p : v)
p->update(data);
}
};
class SubjectChild : public SubjectParent {
int value;
int inner_value;
public:
void edit(int v) {
value = v;
notify(value);
}
int get_data() { return inner_value; }
};
//----------------------
struct ObserverParent {
virtual ~ObserverParent() {}
virtual void update(int data) = 0;
SubjectParent* subject = nullptr;
};
class ObserverChild1 : public ObserverParent {
public:
void update(int n) override {
}
};
class ObserverChild2 : public ObserverParent
{
public:
void update(int n) override {
Table* table = static_cast<Table*>(subject);
int* data = table->get_data();
}
};
int main()
{
SubjectChild t;
ObserverChild1 pg;
t.attach(&pg);
ObserverChild2 bg;
t.attach(&bg);
t.edit(3);
}
8. 열거, 통보, 방문, 전가-④ : chain of responsibility
"책임의 전가"라고도 불리는이 패턴은 어떤 요청이 왔을 때 여러개의 객체를 순차적으로 돌아다니며 처리 당하는 패턴입니다. 예를 들어, 일반적인 GUI의 경우 버튼을 눌렀을 때, 부모(Handler)에게 전달해서 요청을 처리하도록 하는데, 이 또한 chain of responsibility 패턴의 예시입니다.
아래는 간단한 chain of responsibility 패턴의 코드입니다.
#include <iostream>
struct HandlerParent {
HandlerParent* next = nullptr;
virtual bool HandleRequest(int problem) = 0;
void Handle(int problem) {
if (HandleRequest(problem) == true)
return;
if (next != 0)
next->Handle(problem);
}
};
class Handler1 : public HandlerParent {
public:
bool HandleRequest(int problem) override {
return false;
}
};
class Handler2 : public Handler {
public:
bool HandleRequest(int problem) override {
return false;
}
};
class Handler3 : public Handler {
public:
bool HandleRequest(int problem) override {
return false;
}
};
int main() {
Handler1 t1;
Handler2 t2;
Handler3 t3;
t1.next = &t2;
t2.next = &t3;
t3.next = nullptr;
t1.Handle(12);
}
9. 객체 저장 : memento
memento는 "기억"이라는 뜻으로 객체의 캡슐화를 위배하지 않으면서, 스스로 상태를 저장했다가 복구 하는 패턴입니다. Prototype과 Memento 모두 이미 만들어진 객체들의 사본을 유지하는 것이지만 prototype은 클래스가 과다하게 많아지는 것을 방지하기 위해서, 객체들의 상태를 유지하기 위해서 사본을 만드는 것이 다릅니다.
#include <iostream>
#include <vector>
#include <map>
class MyClass {
int state = 1;
struct Memento {
int state = 1;
Memento(int w) : state(w) {}
};
std::map<int, Memento*> memento_map;
int key = 0;
public:
int Save() {
Memento* m = new Memento(state);
memento_map[++key] = m;
return key;
}
void Restore(int k) {
state = memento_map[k]->state;
}
void SetState(int c) { state = c; }
};
int main() {
MyClass g;
g.SetState(0);
int token = g.Save();
g.SetState(1);
g.Restore(token);
}
10. 중재자 : mediator
객체간의 관계가 너무 복잡한 경우, 중재자를 도입해서 N:N의 관계를 N:1의 관계로 바꾸는 것을 mediator패턴이라고 합니다. 예를 들어 4개의 Object가 서로의 존재를 알아야하는 경우, 너무 복잡하기 때문에 중재자를 두어 해결하는 방법입니다.
#include <iostream>
#include <conio.h>
struct IMediator {
virtual void ChangeState() = 0;
virtual ~IMediator() {}
};
class ComplexObject1 {
bool state;
IMediator* pMediator;
public:
void SetMediator( IMediator* p ) { pMediator = p; }
ComplexObject1() : state(false) {}
void SetCheck(bool b) {
state = b;
pMediator->ChangeState();
}
bool GetCheck() { return state; }
};
class ComplexObject2 {
bool state;
IMediator* pMediator;
public:
void SetMediator( IMediator* p ) { pMediator = p; }
ComplexObject2() : state(false) {}
void SetCheck(bool b) {
state = b;
pMediator->ChangeState();
}
bool GetCheck() { return state; }
};
class RealMediator : public IMediator {
ComplexObject1* c1;
ComplexObject1* c2;
ComplexObject2* r1;
ComplexObject2* r2;
public:
RealMediator( CheckBox* a, CheckBox* b, RadioBox* c, RadioBox* d)
: c1(a), c2(b), r1(c), r2(d) {
c1->SetMediator(this);
c2->SetMediator(this);
r1->SetMediator(this);
r2->SetMediator(this);
}
void ChangeState() {
if ( c1->GetCheck() && c2->GetCheck() && r1->GetCheck() && r2->GetCheck()) {
// State Full!
}
}
};
int main(){
ComplexObject1 c1, c2;
ComplexObject2 r1, r2;
RealMediator m(&c1, &c2, &r1, &r2);
c1.SetCheck(true);
c2.SetCheck(true);
r1.SetCheck(true);
r2.SetCheck(true);
}
11. 컴파일러 : interpreter
이후에 OOP의 개념을 발전시키기 위한 AOP(Aspect Oriented Programming, 관점지향 프로그래밍)의 등장으로, 기능별로 모듈화해서 분리시키는 개념도 나왔습니다.
'Developers 공간 [Basic] > Software Basic' 카테고리의 다른 글
[Python] Multi-process와 Multi-thread 구현하기 (0) | 2024.01.21 |
---|---|
[Pytorch] Attention Layer 분석 및 구축하기 (0) | 2023.06.20 |
[PyTorch] DDP(Distributed Data Parallel) 셋팅하기 (0) | 2022.12.27 |
[AWS] SMDDP(Sagemaker's DDP) 기본 환경 셋팅 (0) | 2022.12.27 |
[Python] Python 및 Custom 패키지 관리하기 (0) | 2022.12.21 |