당신이 오늘의 집 서비스의 시스템 담당자라고 가정해 봅시다. 시스템 내부엔 위 그림과 같은 가구 클래스가 존재합니다.
다양한 가구 종류가 추가(의자, 소파 등)될 수도 있고, 다양한 컨셉(모던, 빅토리안 등)이 추가될 수 있습니다. 이 때 기존 코드를 수정하지 않고 새로운 항목을 추가하려면 어떻게 해야할까요? 문제를 재정의한다면 다음과 같습니다.
어떻게하면 코어 코드의 수정 없이 클래스를 쉽게 확장시킬 수 있을까?
클라이언트가 인스턴스를 생성할 때 구현 클래스를 명시하지 않고 추상 클래스를 명시하여 생성하도록 해서 문제를 해결합니다. 이를통해 OCP, DIP를 달성합니다.
product
는 위 예시에서의 가구에 해당합니다. ProductA
는 Chair
, ProductB
는 Sofa
.. 이런 식입니다.
factory
는 가구를 생성하는 클래스입니다. 다양한 방식으로 가구를 생성할 수 있도록 해서 product
가 컨셉별로 구분될 수 있도록 합니다. Factory1
은 modern
컨셉의 가구 생성, Factory2
는 victorian
컨셉의 가구 생성.. 이런 식입니다.
클린코드의 저자는 시스템 생성과 시스템 사용을 분리하여 객체가 생성되는 시점을 애플리케이션을 신경쓰지 않는 것이 좋다고 합니다. (생성과 관련한 코드는 모두 main이나 main이 호출하는 모듈로 옮기고, 나머지 시스템은 모든 객체가 생성되었고 모든 의존성이 연결되었다고 가정하는 방식을 권장합니다.)
하지만 때로는 객체가 생성되는 시점을 애플리케이션이 결정할 필요가 생길 수 있는데, 이럴때 추상 팩토리 패턴을 사용하면 된다고 이해했습니다.
또한, 구현한 코드를 보면 DTO를 마치 클래스인 것 처럼 인터페이스를 통해 다루는데, 실제 상황에선 그럴 필요가 없습니다. 클라이언트가 구현 클래스가 아닌 추상 클래스를 의존하도록 해서 OCP와 DIP를 달성하는 것이 핵심이지, 너무 딱딱하게 클래스 다이어그램을 항상 100%따를 필요는 없다고 이해했습니다.
├── abstract_factory
├── furniture
│ ├── furnitureFactory.go
│ ├── artDecoFurnitureFactory.go
│ ├── victorianFurnitureFactory.go
│ ├── modernFurnitureFactory.go
│ ├── chair.go
│ ├── desk.go
│ └── sofa.go
├── go.mod
└── main.go
cd abstract_factory && go mod init abstract_factory
package furniture
import "fmt"
type FurnitureFactory interface {
CreateChair() IChair
CreateSofa() ISofa
CreateDesk() IDesk
}
func GetFurnitureFactory(concept string) (FurnitureFactory, error) {
if concept == "art_deco" {
return &ArtDecoFurnitureFactory{}, nil
} else if concept == "victorian" {
return &VictorianFurnitureFactory{}, nil
} else if concept == "modern" {
return &ModernFurnitureFactory{}, nil
}
return nil, fmt.Errorf("unknown furniture concept passed")
}
package furniture
type ArtDecoFurnitureFactory struct {
}
func (f *ArtDecoFurnitureFactory) CreateChair() IChair {
return &ArtDecoChair{
Chair: Chair{
style: "Art Deco",
legCnt: 0,
weight: 10,
},
}
}
func (f *ArtDecoFurnitureFactory) CreateSofa() ISofa {
return &ArtDecoSofa{
Sofa: Sofa{
style: "Art Deco",
cost: 100,
weight: 50,
},
}
}
func (f *ArtDecoFurnitureFactory) CreateDesk() IDesk {
return &ArtDecoDesk{
Desk: Desk{
style: "Art Deco",
legCnt: 2,
cost: 100,
weight: 100,
},
}
}
package furniture
type VictorianFurnitureFactory struct {
}
func (f *VictorianFurnitureFactory) CreateChair() IChair {
return &ModernChair{
Chair: Chair{
style: "Victorian",
legCnt: 0,
weight: 10,
},
}
}
func (f *VictorianFurnitureFactory) CreateSofa() ISofa {
return &ModernSofa{
Sofa: Sofa{
style: "Victorian",
cost: 100,
weight: 50,
},
}
}
func (f *VictorianFurnitureFactory) CreateDesk() IDesk {
return &ModernDesk{
Desk: Desk{
style: "Victorian",
legCnt: 2,
cost: 100,
weight: 100,
},
}
}
package furniture
type ModernFurnitureFactory struct {
}
func (f *ModernFurnitureFactory) CreateChair() IChair {
return &ModernChair{
Chair: Chair{
style: "modern",
legCnt: 0,
weight: 10,
},
}
}
func (f *ModernFurnitureFactory) CreateSofa() ISofa {
return &ModernSofa{
Sofa: Sofa{
style: "modern",
cost: 100,
weight: 50,
},
}
}
func (f *ModernFurnitureFactory) CreateDesk() IDesk {
return &ModernDesk{
Desk: Desk{
style: "modern",
legCnt: 2,
cost: 100,
weight: 100,
},
}
}
예시에선 chair가 메서드가 없는 DTO 형태인데, 코드는 메서드가 존재하는 클래스에 대해서도 확장 가능하도록 구현됨
package furniture
type IChair interface {
setChairStyle(style string) //억지메소드..
}
type Chair struct {
style string
legCnt int
weight int
}
func (c *Chair) setChairStyle(style string) {
c.style = style
}
type ArtDecoChair struct {
Chair
}
type VictorianChair struct {
Chair
}
type ModernChair struct {
Chair
}
package furniture
type ISofa interface {
setSofaStyle(style string) //억지메소드..
}
type Sofa struct {
style string
cost int
weight int
}
func (s *Sofa) setSofaStyle(style string) {
s.style = style
}
type ArtDecoSofa struct {
Sofa
}
type VictoriaSofa struct {
Sofa
}
type ModernSofa struct {
Sofa
}
package furniture
type IDesk interface {
setDeskStyle(style string) //억지메소드..
}
type Desk struct {
style string
legCnt int
cost int
weight int
}
func (d *Desk) setDeskStyle(style string) {
d.style = style
}
type ArtDecoDesk struct {
Desk
}
type VictorianDesk struct {
Desk
}
type ModernDesk struct {
Desk
}
package main
import (
"abstract_factory/furniture"
"fmt"
)
func main() {
artDecoFactory, _ := furniture.GetFurnitureFactory("art_deco")
victorianFactory, _ := furniture.GetFurnitureFactory("victorian")
modernFactory, _ := furniture.GetFurnitureFactory("modern")
artDecoChair := artDecoFactory.CreateChair()
artDecoSofa := artDecoFactory.CreateSofa()
artDecoDesk := artDecoFactory.CreateDesk()
victorianChair := victorianFactory.CreateChair()
victorianSofa := victorianFactory.CreateSofa()
victorianDesk := victorianFactory.CreateDesk()
modernChair := modernFactory.CreateChair()
modernSofa := modernFactory.CreateSofa()
modernDesk := modernFactory.CreateDesk()
fmt.Println(artDecoChair)
fmt.Println(artDecoSofa)
fmt.Println(artDecoDesk)
fmt.Println(victorianChair)
fmt.Println(victorianSofa)
fmt.Println(victorianDesk)
fmt.Println(modernChair)
fmt.Println(modernSofa)
fmt.Println(modernDesk)
}
&{{Art Deco 0 10}}
&{{Art Deco 100 50}}
&{{Art Deco 2 100 100}}
&{{Victorian 0 10}}
&{{Victorian 100 50}}
&{{Victorian 2 100 100}}
&{{modern 0 10}}
&{{modern 100 50}}
&{{modern 2 100 100}}