【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
}

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

요약



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

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

좋은 웹페이지 즐겨찾기