디자인 패턴이란 개발시 반복적으로 등장하는 문제를 해결 하기 위한 일반화 된 솔루션
즉 자주 보이는 문제 처리 방법이라고생각합니다.
디자인 패턴은 크게 3개로 분리됩니다.
생성 패턴
새로운 것을 만들어 내는 방법과 관련된 패턴 |
구조 패턴
여러 코드를 조립하고 연결하는 방법에 대한패턴 |
행동 패턴
서로 어떻게 상호 작용할지에 대한패턴 |
생성 패턴(싱글톤 패턴)
인스턴스를 여러 개 만들 필요없이 하나만 존재해야하는 경우에 사용
예) A라는 사람은 세상에 본인 하나뿐
#include <iostream>
using namespace std;
class AHuman {
private:
static AHuman* instance; // 유일한 A 객체를 가리킬 정적 포인터
int positionX; // A X 위치
int positionY; // A Y 위치
// private 생성자로 외부에서 객체 생성 금지하게 해줍니다.
AHuman() : positionX(0), positionY(0) {
cout << "A플레이어 위치 (" << positionX << ", " << positionY << ")" << endl;
}
public:
// 복사 생성자와 대입 연산자를 삭제하여 복사 방지
// AHuman a; AHuman b = a; 등을 방지함
AHuman(const AHuman&) = delete;
AHuman& operator=(const AHuman&) = delete;
// 정적 메서드: 유일한 A 인스턴스를 반환
// A를 부를때 만약 A가 없었다면 A만들어주기
static AHuman* getInstance() {
if (instance == nullptr) {
instance = new AHuman();
}
return instance;
}
// A 위치 이동
void move(int deltaX, int deltaY) {
positionX += deltaX;
positionY += deltaY;
cout << "AHuman 위치 이동 (" << positionX << ", " << positionY << ")" << endl;
}
// 현재 위치 출력
void getPosition() const {
cout << "AHuman 위치: (" << positionX << ", " << positionY << ")" << endl;
}
};
int main() {
// 유일한 A 인스턴스를 가져옴
AHuman* ahuman = AHuman::getInstance();
ahuman->move(10, 20); // A 이동
ahuman->getPosition();
// 또 다른 요청도 같은 인스턴스를 반환
AHuman* sameAhuman = Airplane::getInstance();
sameAhuman->move(-5, 10); // A 이동
sameAhuman->getPosition();
return 0;
}
전역에 있기 때문에 어디서든 접근 가능
구조패턴(데코레이터 패턴)
객체의 기능을 동적으로 확장하는 데 사용하는 디자인 패턴입니다.
#include <iostream>
#include <string>
#include <memory>
using namespace std;
// 추가할 인터페이스 (컴포넌트)
class Coffee {
public:
virtual string getName() const = 0; // 이름 반환
virtual double getPrice() const = 0; // 가격 반환
};
// 구체적인 컴포넌트
class SimpleCoffee : public Coffee {
public:
string getName() const {
return "Simple Coffee"; // 커피 이름
}
double getPrice() const {
return 5.0; // 커피의 가격
}
};
// 데코레이터 추상 클래스
class CoffeeDecorator : public Coffee {
protected:
// 기존의 커피 객체 참조
Coffee* coffee;
public:
//데코레이터는 커피 객체를 받아서 감쌉니다.
CoffeeDecorator(Coffee* c) : coffee(c) {}
//소멸자에서 내부 커피 객체를 삭제
virtual ~CoffeeDecorator()
{
delete coffee;
}
};
// 구체적인 데코레이터 - 우유 추가
class MilkDecorator : public CoffeeDecorator {
public:
MilkDecorator(Coffee* c) : CoffeeDecorator(c) {}
string getName() const {
// 기존 커피의 이름에 " + milk"를 추가
return coffee->getName() + " + milk";
}
double getPrice() const {
// 기존 커피의 가격에 우유 추가 비용 1.5를 더함
return coffee->getPrice() + 1.5;
}
};
// 구체적인 데코레이터 - 설탕 추가
class SugarDecorator : public CoffeeDecorator {
public:
SugarDecorator(Coffee* c) : CoffeeDecorator(c) {}
string getName() const {
// 기존 커피의 이름에 " + Sugar"를 추가
return coffee->getName() + " + Sugar";
}
double getPrice() const {
// 기존 커피의 가격에 설탕 추가 비용 1.5를 더함
return coffee->getPrice() + 0.5;
}
};
// **클라이언트 코드**
int main() {
//단순한 커피 생성
Coffee* coffee = new SimpleCoffee();
//커피에 우유 추가
coffee = new MilkDecorator(coffee);
//커피에 설탕 추가
coffee = new SugarDecorator(coffee);
cout << "coffee :" << coffee->getName()<<endl;
cout << "coffee :" << coffee->getPrice()<<endl;
delete coffee;
return 0;
}
커피에 우유와 설탕을 추가하는 과정을 나타내는 코드입니다.
이 구현은 커피를 상속받는 SimpleCoffee와 커피에 기능을 추가하는 데코레이터 클래스로 나뉩니다. 데코레이터는 기존 커피 객체를 포함하여 기능을 확장합니다.
기능을 추가할 때는 데코레이터를 통해 기존 커피 객체를 감싸는 방식으로, 결과적으로 기존 커피의 기능과 데코레이터의 기능이 조합된 새로운 객체가 생성됩니다.
이러한 방식은 상속과 인터페이스 추가에 의존하지 않고, 객체의 포함 관계를 통해 유연하게 기능을 확장할 수 있도록 해줍니다.
행동 패턴 (옵저버 패턴)
객체의 상태 변경이 발생할 때, 해당 객체와 연결된 모든 옵저버 객체에게 자동으로 통지하여 변경을 감지할 수 있게 해줍니다.
#include <iostream>
#include <string>
#include <memory>
#include <vector>
using namespace std;
//옵저버 인터페이스
class Observer {
public :
//상태 체크 (업데이트)
virtual void update(const string& message) = 0;
virtual ~Observer() = default;
};
//주체 인터페이스 (서브젝트)
class Subject {
public :
virtual void attach(Observer* observer) = 0; //옵저버 등록
virtual void detach(Observer* observer) = 0; //옵저버 해제
virtual void notify(const string& message) = 0; // 옵저버에게 통지
virtual ~Subject() = default;
};
// 구체적인 주체
class WeatherStation : public Subject {
private:
vector<Observer*> observers; //옵저버 목록
public :
void attach(Observer* observer) override {
observers.push_back(observer);
}
void detach(Observer* observer) override {
observers.push_back(observer);
}
void notify(const string& message) override {
for (Observer* observer : observers)
{
observer->update(message);
}
}
void changeWeather(const string& weather)
{
notify("오늘 날씨는 " + weather);
}
};
// 구체적인 옵저버
class PhoneDisplay : public Observer {
public :
void update(const string& message) override {
cout << "메세지가 왔습니다 : " << message << endl;
}
};
// 구체적인 옵저버
class WeatherNews : public Observer {
public:
void update(const string& message) override {
cout << "기상청에서 전해드립니다. " << message << endl;
}
};
// **클라이언트 코드**
int main() {
WeatherStation* weatherStation = new WeatherStation();
PhoneDisplay* phoneDisPlay = new PhoneDisplay();
WeatherNews* weatherNews = new WeatherNews();
//포인터로 만들었으니 주소값 ( & )을 안붙혀도 주소 값 전달 됨
weatherStation->attach(phoneDisPlay);
weatherStation->attach(weatherNews);
weatherStation->changeWeather("맑음");
weatherStation->changeWeather("비");
return 0;
}
이 예시에서는 날씨를 예측하는 기상청을 통해 옵저버 패턴을 설명했습니다. 기상청에서 날씨가 변경되면, 해당 정보를 등록된 뉴스와 핸드폰 애플리케이션에 자동으로 전달합니다.
주체인 WeatherStation은 여러 옵저버를 보관하고 있으며, 날씨가 변경될 때마다 notify 메서드를 통해 모든 옵저버에게 변경된 내용을 알립니다. 이렇게 함으로써, 옵저버들은 주체의 상태 변화에 즉시 반응할 수 있습니다.
이 패턴은 객체 간의 느슨한 결합을 유지하면서도 효과적으로 상태 변경을 관리할 수 있게 해줍니다.
이것 말고도 디자인패턴은 훨씬 많이 있으니 디자인 패턴을 이용해서 불필요한 코드들을 줄일 수 있게 하면 좋을 것 같습니다.