【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
}
뭔가 더 쉽게 쓸 수 있는 방법이 있는 것 같습니다. . .
조언 등 있으면 매우 기쁩니다.
요약
디자인 패턴이라는 유형을 통해
구체적인 예의 작성이나 과거에 실시하고 있던 구현을 재기록해 보는 등을 실시하는 것으로
점점 이해를 깊게 할 수 있었습니다.
사용 장소를 표제,
코딩 시의 선택지의 하나로서 사용할 수 있도록 해 가고 싶습니다.
Reference
이 문제에 관하여(【Swift】Open-Closed Principle과 Specification 패턴), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://qiita.com/shiz/items/4f1aeaaa68276c7deca1텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)