【Swift】Open-Closed Principle과 Specification 패턴

경위



어쩐지 아는 디자인 패턴에 대해 배울 기회가 있고,
살펴보면

그때 더 이렇게 하면 좋았어
이 곤란한 곳은 이런 식으로 쓸 수 있습니다.

라고 생각하는 것이 많이 있었으므로, 정리해 가려고 생각했습니다.

조심하고 싶은 것



「디자인 패턴 주」라는 말도 있을 정도
디자인 패턴을 고집하는 것에 조심해,
디자인 패턴은 어디까지나 알기 쉬운 코드를 쓰기 위한 좋은 참고예로서 배우고 싶습니다.

Open-Closed Principle



클래스는 확장에 대해 열려 있고 수정에 대해 닫아야 합니다.

기존 코드를 수정하지 않고 확장을 위해 코드를 추가하면 변경으로 인한 버그가 발생하기 어렵다는 정책입니다.

예



검색 조건을 지정하는 장면을 생각하고 싶습니다.
enum Color {
    case green, red, blue
}

enum Size {
    case small, middle, large
}

enum Area {
    case domestic
    case foreign
}

struct Fruit {
    let name: String
    let color: Color
    let size: Size
    let price: Int
}

struct FruitFilter {

    func filterByColor(_ fruits: [Fruit], _ color: Color) -> [Fruit] {
        var result = [Fruit]()
        for p in fruits {
            if p.color == color {
                result.append(p)
            }
        }
        return result
    }

    func filterBySize(_ fruits: [Fruit], _ size: Size) -> [Fruit] {
        var result = [Fruit]()
        for p in fruits {
            if p.size == size {
                result.append(p)
            }
        }
        return result
    }

    func filterBySizeAndColor(_ fruits: [Fruit],
                              _ size: Size, _ color: Color) -> [Fruit] {
        var result = [Fruit]()
        for p in fruits {
            if (p.size == size) && (p.color == color) {
                result.append(p)
            }
        }
        return result
    }
}
let apple = Fruit(name: "Apple", color: .red, size: .small, area: .domestic)
let melon = Fruit(name: "Melon", color: .green, size: .large, area: .foreign)
let grape = Fruit(name: "Grape", color: .blue, size: .small, area: .domestic)

let fruits = [apple, melon, grape]

let filter = FruitFilter()

for f in filter.filterBySize(fruits, .small) {
    print(" - \(f.name) is small ")
    // Apple is small
    // Grape is small
}

for f in filter.filterBySizeAndColor(fruits, .small, .red) {
    print(" - \(f.name) is small and red ")
    // Apple is small and red
}

또한 생산지에서 검색 조건을 추가하려는 경우 FruitFilter 클래스에 새 메서드를 추가합니다.
func filterByArea(_ fruits:[Fruit], _ area: Area) -> [Fruit] {
    var result = [Fruit]()
    for p in fruits {
        if (p.area == area) {
           result.append(p)
        }
    }
    return result
}

그럼, 생산지와 색이 검색 조건인 경우는?
그러면 또 메소드가 늘어나, 클래스는 점점 커져 버립니다.

작을 때는 좋지만, 커지면 맛있어지고,
어쩌면 예기치 않은 부분에 영향을 줄 수도 있습니다.

그래서 Open-Closed Principle을 적용하고 싶습니다.

Specification(사양) 패턴



DDD 책 에서 소개된 패턴으로

· 입력 체크 등의 검증 (검증)
· 집합 (배열, 목록 등)에서 특정 요소 추출 (선택)
・조건을 만족한 새로운 오브젝트를 작성한다(생성)

등에 사용됩니다.
protocol Specification {

    associatedtype T
    func isSatisfied(_ item: T) -> Bool
}

protocol Filter {
    associatedtype T
    func filter<Spec: Specification>(_ items: [T], _ spec: Spec)
        -> [T] where Spec.T == T    
}

struct ColorSpecification: Specification {

    typealias T = Fruit

    let color: Color

    init(color: Color) {
        self.color = color
    }

    func isSatisfied(_ item: T) -> Bool {
        return item.color == color
    }
}

struct SizeSpecification: Specification {

    typealias T = Fruit

    let size: Size

    init(size: Size) {
        self.size = size
    }

    func isSatisfied(_ item: T) -> Bool {
        return item.size == size
    }
}

struct FruitFilter: Filter {
    typealias T = Fruit

    func filter<Spec>(_ items: [Fruit], _ spec: Spec) -> [Fruit]
        where Spec : Specification, FruitFilter.T == Spec.T {

        var result = [Fruit]()
        for i in items {
            if spec.isSatisfied(i) {
                result.append(i)
            }
        }
        return result
    }
}
let apple = Fruit(name: "Apple", color: .red, size: .small, area: .domestic)

let melon = Fruit(name: "Melon", color: .green, size: .large, area: .foreign)

let grape = Fruit(name: "Grape", color: .blue, size: .small, area: .domestic)

let fruits = [apple, melon, grape]
let filter = FruitFilter()

let color = ColorSpecification(color: .blue)

for f in filter.filter(fruits, color) {
    print(" - \(f.name) is blue ")
    // Grape is blue
}

let size = SizeSpecification(size: .small)
for f in filter.filter(fruits, size) {
   print(" - \(f.name) is small ")
   // Apple is small
   // Grape is small
}

let colorAndSize = ColorAndSizeSpecfication(color, size)    
for f in filter.filter(fruits, colorAndSize) {
    print(" - \(f.name) is blue and small ")
    // Grape is blue and small
}

복합 조건의 경우는 다음과 같이 합니다.
protocol AndSpecification: Specification {
    associatedtype SpecA: Specification
    associatedtype SpecB: Specification

    var specA: SpecA { get }
    var specB: SpecB { get }
    init(_ specA: SpecA, _ specB: SpecB)
}

extension AndSpecification where SpecA.T == T, SpecB.T == T {
    func isSatisfied(_ item: T) -> Bool {
        return specA.isSatisfied(item) && specB.isSatisfied(item)
    }
}

struct ColorAndSizeSpecfication: AndSpecification {

    typealias T = Fruit
    typealias SpecA = ColorSpecification
    typealias SpecB = SizeSpecification

    private let _specA: SpecA
    private let _specB: SpecB

    var specA: ColorSpecification { return _specA }
    var specB: SizeSpecification { return _specB }

    init(_ specA: SpecA, _ specB: SpecB) {
        _specA = specA
        _specB = specB
    }
}
let colorAndSize = ColorAndSizeSpecfication(color, size)

for f in filter.filter(fruits, colorAndSize) {
    print(" - \(f.name) is blue and small ")
    // Grape is blue and small
}

코드 양은 그다지 변하지 않을 수 있지만,
향후 조건 추가가 발생하더라도
기존 코드를 변경하지 않고 확장이 가능하며,
오픈 클로즈드 원칙에 따른 형태가 되었습니다.

고민하는 곳



AND 조건을 가변으로 하고 싶은 경우, 다음과 같은 구현이 떠오릅니다.
// Associated Typeを使って定義されたプロトコル(抽象型)は
// そのまま型として使用できないためType Eraser(型消去)をする必要がある
struct AnySpecification<T>: Specification {

    private let _isSatisfied: (T) -> Bool
    init<Spec: Specification>(_ spec: Spec) where Spec.T == T {
        self._isSatisfied = spec.isSatisfied
    }

    func isSatisfied(_ item: T) -> Bool {
        return _isSatisfied(item)
    }    
}

protocol VariadicAndSpecification: Specification {
    associatedtype Spec: Specification

    var specs: [Spec] { get }
    init(_ specs: Spec...)
}

extension VariadicAndSpecification where Spec.T == T {

    func isSatisfied(_ item: T) -> Bool {
        var isSatisfied = true
        for spec in specs {
            if !spec.isSatisfied(item) {
                isSatisfied = false
                break
            }
        }
        return isSatisfied
    }
}

struct ColorAndSizeAndAreaSpecification: VariadicAndSpecification {

    typealias T = Fruit
    typealias Spec = AnySpecification<T>

    private let _specs: [Spec]
    var specs: [Spec] { return _specs }

    init(_ specs: Spec...) {
        _specs = specs
    }
}

struct AreaSpecification: Specification {

    typealias T = Fruit

    let area: area
    init(area: Area) {
        self.area = area
    }

    func isSatisfied(_ item: T) -> Bool {
        return item.producingArea == area
    }
}
let area = AreaSpecification(area: .domestic)

let colorAndSizeAndArea = ColorAndSizeAndAreaSpecification(
    AnySpecification(color),
    AnySpecification(size),
    AnySpecification(area)
)

for f in filter.filter(fruits, colorAndSizeAndArea) {
    print(" - \(f.name) is blue and small and domestic")
    // Grape is blue and small and domestic
}

뭔가 더 쉽게 쓸 수 있는 방법이 있는 것 같습니다. . .
조언 등 있으면 매우 기쁩니다.

요약



디자인 패턴이라는 유형을 통해
구체적인 예의 작성이나 과거에 실시하고 있던 구현을 재기록해 보는 등을 실시하는 것으로
점점 이해를 깊게 할 수 있었습니다.

사용 장소를 표제,
코딩 시의 선택지의 하나로서 사용할 수 있도록 해 가고 싶습니다.

좋은 웹페이지 즐겨찾기