-
디자인 패턴 정리 - 01. 생성 패턴프로그래밍 지식/디자인 패턴 2024. 6. 3. 22:50
생성 패턴
- Simple Factory 패턴
: 객체를 생성하는 팩토리를 따로 두는 것
, 객체 생성 로직을 클라이언트 코드에서 분리
- 장점 : 복잡한 오브젝트의 생성 과정을 클라이언트가 직접 다룰 필요 없음
- 어떤 상황에서는 Object를 생성하는 과정이 복잡할 수 있다.
- 오브젝트 생성 과정은 모두 팩토리 안에 숨겨놓고 클라이언트는 "테슬라 모델3 만들어주세요" "테슬라 모델Y 만들어주세요"를 요청하면 팩토리가 필요한 오브젝트를 생성하여 return
#include <iostream> #include <string> #include <memory> class Car { public: virtual ~Car() = default; virtual void brake() = 0; }; class Model3 : public Car { public: void brake() override { std::cout << "Modle3 brake" << std::endl; } }; class ModelY : public Car { public: void brake() override { std::cout << "ModelY brake" << std::endl; } }; // Factory 함수에서 객체를 생성 class TeslaFactory { public: std::unique_ptr<Car> createCar(const std::string& type) const { if (type == "Model3") { return std::make_unique<Model3>(); } else if(type == "ModelY") { return std::make_unique<ModelY>(); } else { return nullptr; } } }; // Client 함수 int main() { TeslaFactory teslaFactory; auto car1 = teslaFactory.createCar("Model3"); if (car1) { car1->brake(); } auto car2 = teslaFactory.createCar("ModelY"); if (car2) { car2->brake(); } }
- Factory Method Pattern
- 객체 생성 메서드를 하위 클래스에 위임하여 객체 생성을 캡슐화하는 패턴
- 객체 생성의 확장을 용이하게 함
- 단일 객체 생성
#include <iostream> #include <string> #include <memory> class Car { public: virtual ~Car() = default; virtual void brake() = 0; }; class Model3 : public Car { public: void brake() override { std::cout << "Modle3 brake" << std::endl; } }; class ModelY : public Car { public: void brake() override { std::cout << "ModelY brake" << std::endl; } }; class TeslaFactory { public: virtual ~TeslaFactory() {} // 객체 생성을 위한 메서드 (단일) virtual std::unique_ptr<Car> createCar() const = 0; void brake() const { auto car = createCar(); car->brake(); } }; class Model3Factory : public TeslaFactory { public: std::unique_ptr<Car> createCar() const override { return std::make_unique<Model3>(); } //costom 가능 }; class ModelYFactory : public TeslaFactory { public: std::unique_ptr<Car> createCar() const override { return std::make_unique<ModelY>(); } //costom 가능 }; // Client 함수 int main() { Model3Factory model3Factory; auto car1 = model3Factory.createCar(); if (car1) { car1->brake(); } ModelYFactory modelYFactory; auto car2 = modelYFactory.createCar(); if (car2) { car2->brake(); } }
- 심플 팩토리 패턴 vs 팩토리 메서드 패턴
- 객체 생성 책임
- 심플 팩토리 패턴 : 클라이언트가 팩토리 클래스를 사용하여 객체 생성
- 팩토리 메서드 패턴 : 객체 생성 메서드가 추상 클래스나 인터페이스에 정의되고 하위 클래스에서 구현. 클라이언트가 하위 클래스를 사용하여 객체 생성
- 확장성
- 심플 팩토리 패턴 : 새로운 타입의 객체를 생성하려면 팩토리 클래스 코드 수정 필요
- 팩토리 메서드 패턴 : 새로운 타입의 객체를 생성하려면 새로운 하위 클래스를 추가. 기존 클래스 수정 필요 X
- 사용 시기
- 심플 팩토리 패턴 : 객체 생성 로직이 자주 변경되지 않거나 비교적 단순할 때
- 팩토리 메서드 패턴 : 객체 생성 로직이 자주 변경되거나 확장될 가능성이 있을 때
- 객체 생성 책임
- Abstract Factory Pattern
- 관련된 객체의 집합을 생성하는 인터페이스를 제공
- 다수 객체 생성
#include <iostream> #include <string> #include <memory> class Brake { public: virtual ~Brake() = default; virtual void use() = 0; }; class Window { public: virtual ~Window() = default; virtual void use() = 0; }; class Model3Brake : public Brake { public: void use() override { std::cout << "Modle3 brake" << std::endl; } }; class ModelYBrake : public Brake { public: void use() override { std::cout << "ModelY brake" << std::endl; } }; class Model3Window : public Window { public: void use() override { std::cout << "Modle3 window" << std::endl; } }; class ModelYWindow : public Window { public: void use() override { std::cout << "ModelY window" << std::endl; } }; class TeslaFactory { public: virtual ~TeslaFactory() {} virtual std::unique_ptr<Brake> createBrkae() const = 0; virtual std::unique_ptr<Window> createWindow() const = 0; }; class Model3Factory : public TeslaFactory { public: // 복수의 관련 객체들 집합 생성 virtual std::unique_ptr<Brake> createBrkae() const override { return std::make_unique<Model3Brake>(); } virtual std::unique_ptr<Window> createWindow() const override { return std::make_unique<Model3Window>(); } //costom 가능 }; class ModelYFactory : public TeslaFactory { public: virtual std::unique_ptr<Brake> createBrkae() const override { return std::make_unique<ModelYBrake>(); } virtual std::unique_ptr<Window> createWindow() const override { return std::make_unique<ModelYWindow>(); } //costom 가능 }; // Client 함수 int main() { Model3Factory model3Factory; auto brake1 = model3Factory.createBrkae(); auto window1 = model3Factory.createWindow(); brake1->use(); window1->use(); ModelYFactory modelYFactory; auto brake2 = modelYFactory.createBrkae(); auto window2 = modelYFactory.createWindow(); brake2->use(); window2->use(); }
팩토리 메서드 패턴 vs 추상 팩토리 패턴
- 목적 :
- 팩토리 메서드 패턴 : 단일 객체의 생성
- 추상 팩토리 패턴 : 관련된 객체들의 집합을 생성
- 구조 :
- 팩토리 메서드 패턴 : 객체 생성을 위한 하나의 메서드가 있으며, 구체적인 서브 클래스가 이 메서드를 구현하여 객체 생성
- 추상 팩토리 패턴 : 여러 종류의 객체를 생성하는 여러 팩토리 메서드를 가진 인터페이스가 있으며, 구체적인 팩토리 클래스가 이 인터페이스 관련된 객체들을 생성
- 확장성 :
- 팩토리 메서드 패턴 : 새로운 객체 유형을 추가할 때 새로운 서브클래스 추가하여 팩토리 메서드 오버라이드
- 추상 팩토리 패턴 : 새로운 제품군을 추가할 때 새로운 팩토리 클래스를 추가하여 관련된 모든 제품을 생성하는 메서드 구현
ex. 제품군 객체들을 모두 만들어야할 때
- 팩토리 메서드 일 때 Client : 모델3 브레이크 객체 만들어주세요, 모델3 윈도우 객체 만들어주세요, ...
- 추상 팩토리 일 때 Client : 모델3와 관련된 객체들 만들어주세요 (팩토리 메서드를 한번 더 추상화)
팩토리 관련 패턴들의 차이점을 이해하기 어려웠다. (모두 추상화를 사용하고 코드 상 비슷해보였기 때문이다.)
구현의 차이점을 명확히 아는 것 보다는 각각의 패턴을 사용하는 컨셉이 중요한 것 같다. 내가 이해한 바로는 하기와 같다.
- 심플 팩토리 패턴 : 객체 생성하는 과정은 안 보여줄게. 클라이언트는 그냥 뭐 사용하고싶은지만 말해
- 팩토리 메서드 팩턴 : 객체 생성을 상위 클래스와 하위 클래스로 나눠서 나중에 다른 종류의 하위 클래스가 생길 확장성도 고려할래
- 추상 팩토리 패턴 : 클라이언트가 뭐 사용하고 싶다고 말하면 관련된 제품군들 객체를 싹 다 만들어줄래
Builder Pattern
#include <iostream> #include <string> class Car { public: void setWindow(const std::string &window) { this->window = window; } void setWheels(const std::string &wheels) { this->wheels = wheels; } void setBrake(const std::string &brake) { this->brake = brake; } void setSeats(const int seats) { this->seats = seats; } void show() const { std::cout << "Car with window: " << window << ", wheels: " << wheels << ", seats: " << seats << ", brake: " << brake << std::endl; } private: std::string window; std::string wheels; std::string brake; int seats; }; class CarBuilder { public: virtual ~CarBuilder() = default; Car* getCar() { return car; } void createNewCar() { car = new Car(); } virtual void buildWindow() = 0; virtual void buildWheels() = 0; virtual void buildSeats() = 0; virtual void buildBrake() = 0; protected: Car *car; }; class Car1Builder : public CarBuilder { public: void buildWindow() override { car->setWindow("2024 Window"); } void buildWheels() override { car->setWheels("18 inch Alloy Wheels"); } void buildSeats() override { car->setSeats(2); } void buildBrake() override { car->setBrake("2011 Brake"); } }; class Car2Builder : public CarBuilder { public: void buildWindow() override { car->setWindow("2021 Window"); } void buildWheels() override { car->setWheels("14 inch Alloy Wheels"); } void buildSeats() override { car->setSeats(2); } void buildBrake() override { car->setBrake("2020 Brake"); } }; class CarDirector { public: void setCarBuilder(CarBuilder *builder) { this->builder = builder; } Car* getCar() { return builder->getCar(); } // 객체 생성 단계 명시 // 여러 인자를 나열하는 방식에서 벗어나 메서도 체이닝이 가능하다. void constructCar() { builder->createNewCar(); builder->buildWindow(); builder->buildWheels(); builder->buildSeats(); builder->buildBrake(); } private: CarBuilder *builder; }; int main() { CarDirector director; Car1Builder sportsCarBuilder; director.setCarBuilder(&sportsCarBuilder); director.constructCar(); Car *Car1 = director.getCar(); Car1->show(); Car2Builder familyCarBuilder; director.setCarBuilder(&familyCarBuilder); director.constructCar(); Car *Car2 = director.getCar(); Car2->show(); delete Car1; delete Car2; return 0; }
장점
- 객체 생성의 단계적 구성:
- 객체의 생성 과정을 단계별로 나눌 수 있어, 각 단계에서 필요한 설정을 수행할 수 있습니다.
- 클라이언트 코드가 어떤 단계에서든 객체의 특정 부분을 생성할 수 있습니다.
- 유연한 객체 생성:
- 다양한 구성의 객체를 동일한 생성 과정으로 만들 수 있어 코드의 재사용성을 높입니다.
- 동일한 빌더 인터페이스를 사용하여 여러 가지 변형된 객체를 생성할 수 있습니다.
- 복잡한 객체 생성의 단순화:
- 복잡한 객체의 생성 과정을 단순화하고, 생성 로직을 한 곳에 집중시켜 관리할 수 있습니다.
- 빌더 패턴을 사용하면 객체 생성 코드가 객체의 내부 표현과 분리되므로, 생성 로직을 더 잘 관리하고 유지보수할 수 있습니다.
- 불변 객체 생성에 유용:
- 빌더 패턴을 사용하면 불변 객체를 쉽게 생성할 수 있으며, 이는 멀티스레딩 환경에서 안전성을 제공합니다.
- 가독성 향상:
- 객체 생성 과정이 명확하게 드러나므로, 코드의 가독성과 이해도가 높아집니다.
- 메서드 체이닝을 통해 직관적이고 읽기 쉬운 코드 작성을 도와줍니다.
단점
- 복잡성 증가:
- 간단한 객체 생성에 대해서도 불필요하게 복잡한 구조를 도입할 수 있습니다.
- 빌더 클래스와 디렉터 클래스 등 여러 클래스를 추가로 작성해야 하므로, 코드가 길어지고 복잡해질 수 있습니다.
- 메모리 오버헤드:
- 빌더 객체와 최종 객체를 별도로 유지해야 하므로, 메모리 사용량이 증가할 수 있습니다.
- 객체 생성 시간이 증가할 수 있음:
- 단계별로 객체를 생성하고 설정하기 때문에, 객체 생성에 시간이 더 걸릴 수 있습니다.
- 일관성 유지가 어려울 수 있음:
- 모든 필수 필드를 설정하지 않고 객체를 생성하게 되면, 일관성 문제가 발생할 수 있습니다. 이를 방지하기 위해서는 적절한 검증 로직을 추가해야 합니다.
- 복잡한 객체를 위한 추가 코드 필요:
- 복잡한 객체를 생성할 때 필요한 설정 메서드를 많이 작성해야 하므로, 빌더 클래스의 구현이 복잡해질 수 있습니다.
빌더 패턴은 객체 생성의 복잡성을 줄이고, 생성 과정의 유연성을 높이는 데 매우 유용합니다. 그러나 간단한 객체에는 불필요하게 복잡성을 증가시킬 수 있으므로, 사용 시에는 필요성과 상황을 잘 고려해야 합니다.
참고 : chatGPT
- Simple Factory 패턴