apple/DesignPattern & Architecture

[Swift] Prototype Pattern

lgvv 2022. 5. 28. 16:37

Prototype Pattern

 

✅ Prototype Pattern

 

아래의 문서를 구입하여 영어 문서를 번역하고 이해한 것을 바탕으로 글을 작성하고 있습니다.

https://www.raywenderlich.com/books/design-patterns-by-tutorials/v3.0/chapters/14-prototype-pattern

 

Design Patterns by Tutorials, Chapter 14: Prototype Pattern

Methods are merely functions that reside in a class. In this chapter, you’ll take a closer look at methods and see how to add methods onto classes that were created by someone else. You’ll also start in on a new project with this chapter: a drawing app

www.raywenderlich.com

 

 

프로토타입 패턴은 객체가 스스로를 복사할 수 있도록 해주는 생성 패턴입니다. 아래의 두가지 요소를 포함합니다.

 

두가지 요소

 

1. copying은 copy method를 선언하는 프로토콜입니다.

2. prototype 클래스는 copying프로토콜을 상속받습니다.

 

shallow(얕은)와 deep(깊은)이라고 실제로는 두가지 타입이 있습니다. 

( *개인적인 의견 시스템 프로그래밍 시간에 file을 copy방법에 soft와 hard로 하는 것을 생각하면 이해하기 쉽다. )

 

shallow copy는 새로운 객체를 인스턴스를 생성하지만 그것은 properties(속성)은 복사하지 않습니다.

예를 들어서 Swift에서 struct로 구성된 Array는 복사할 때마다 새 Array 인스턴스가 생성되지만 해당 요소는 복제되지 않습니다.

 

deep copy는 새로운 객체 인스턴스를 생성하며 각각의 properties(속성) 또한 복제합니다. 

예를 들어서, 만약 Array를 deep copy 한다면 그것의 각각의 원소들 또한 카피됩니다. Swift는 Array에서 기본적(default)으로 deep copy를 메소드로 지원하지 않습니다. 그래서 우리는 이 챕터에서 Array의 deep copy를 만들어 볼 것입니다.

 

When should you use it?

이 패턴을 사용하여 객체가 스스로를 복사할 수 있도록 합니다.

 

예{를들어 Foundation은 NSCopying 프로토콜을 정의합니다. 그러나 이 프로토콜은 Objective-C용으로 설계되었기에 불행히도 Swift에서는 잘 작동하지 않습니다. NSCopying을 Swift에서도 여전히 사용할 수 있지만, 더 많은 boilerplate 코드를 작성하게 될 것입니다.

 

* boilerplate란?

 - 컴퓨터 프로그래밍에서 보일러플레이트 or 보일러플레이트 코드라고 부르는 것은 최소한의 변경으로 여러곳에서 재사용 되며 반복적으로 비슷한 형태를 띄는 코드를 의미합니다. (

글 최하단에 참고할 글을 넣어두었습니다.)

 

대신에 우리는 나만의 Copying 프로토콜을 이번 장에서 구현하여 사용해 볼 것입니다. 우리는 이런 방법으로 prototype 패턴에 대해서 자세히 배우고 결과 또한 더 Swifty할 것입니다!

 

* Swifty란? Swift 스럽다.

 

Playground example

 

Monster클래스에서 상속받는 Copying 프로토콜을 생성합니다. 아래의 코드를 작성하세요.

public protocol Copying: class { 
  // 1
  init(_ prototype: Self)
}

extension Copying {
  // 2
  public func copy() -> Self {
    return type(of: self).init(self)
  }
}

1. 먼저 이니셜라이저 init(_ prototype: Self)를 정의합니다. 기존 인스턴스를 사용하여 새 클래스 인스턴스를 만드는 것을 목적으로 하기 때문에 우리는 이를 copy 이니셜라이저라고 합니다.

2. 일반적으로는 직접적으로 copy 이니셜라이저를 호출하지 않습니다. 대신에 복사하기를 원하는 Copying 클래스 인스턴스에서 심플하게 copy() 메소드를 통해 호출합니다. 프로토콜 자체 내에서 copy 이니셜라이저를 선언했으므로 copy() 매우 간단합니다. type(of: self)를 호출함으로써 현재 유형을 확인하고, 그것은 copy 이니셜라이저를 호출하여, self 인스턴스를 전달합니다. 따라서 Copying() 준수하는 유형의 하위 클래스를 작성하더라도 copy()는 올바르게 작동합니다.

 

다음으로는 아래 코드를 추가합니다.

// 1
public class Monster: Copying {

  public var health: Int
  public var level: Int

  public init(health: Int, level: Int) {
    self.health = health
    self.level = level
  }

  // 2
  public required convenience init(_ monster: Monster) {
    self.init(health: monster.health, level: monster.level)
  }
}

위의 코드가 하는 일은 다음과 같습니다.

 

1. 이것은 간단한 Monster type정의합니다. 그리고 그것은 Copying을 준수하고 health와 level properties를 준수합니다.

2. Copying을 만족시키기 위해서는 init(_ prototype:)을 필수적으로 선언해야 합니다. 하지만 convenience을 명시함으로써 이것을 허용하고, 우리가 정확하게 수행하기 원하는 다른 designated initializer를 호출합니다.

 

 

아래에 다음의 코드를 추가합니다.

// 1
public class EyeballMonster: Monster {

  public var redness = 0

  // 2
  public init(health: Int, level: Int, redness: Int) {
    self.redness = redness
    super.init(health: health, level: level)
  }

  // 3
  public required convenience init(_ prototype: Monster) {
    let eyeballMonster = prototype as! EyeballMonster // 강제 캐스팅해서 런타임시에 오류 아래에 적혀있음
    self.init(health: eyeballMonster.health,
              level: eyeballMonster.level,
              redness: eyeballMonster.redness)
  }
}

c1. 실제 앱에서는 Monster에 추가 properties와 기능(함수) 등을 추가하는 서브 클래스들이 있을 수 있습니다. 여기서 우리는 EyeballMonster를 정의합니다. 

2. 새 properties를 추가했으므로 초기화 시 속성들도 선언해야 합니다. 이렇게 하려면 새 이니셜라이저 init(health:level:redness:)를 생성합니다.

3. 새로운 이니셜라이저를 생성했기 때문에 required initializers도 제공해야 합니다. 제너럴한 타입(Monster)과 함께 EyeballMonster를 캐스팅해야 합니다. 

 

이제 클래스를 사용할 준비가 다 되었어요! 아래의 코드를 따라해보죠!

let monster = Monster(health: 700, level: 37)
let monster2 = monster.copy()
print("Watch out! That monster's level is \(monster2.level)!")

여기에서 moster를 생성했고, monster2를 복제했습니다. 그리고 monster2.level을 콘솔에 출력해보면 아래의 코드가 나타납니다.

Watch out! That monster's level is 37!

복사가 된 것을 확인할 수 있어요!

 

아래의 코드를 입력해보아요!

let eyeball = EyeballMonster(
  health: 3002,
  level: 60,
  redness: 999)
let eyeball2 = eyeball.copy()
print("Eww! Its eyeball redness is \(eyeball2.redness)!")

위의 코드로 캐스팅한 것도 복사본으로 만들 수 있음을 증명할 수 있습니다.

 

Eww! Its eyeball redness is 999!

Monster로부터 EyeballMonster를 생성을 시도하면 어떤일이 일어날까요?

let eyeballMonster3 = EyeballMonster(monster)

위의 코드는 컴파일에는 문제가 없지만, 런타임시에 exception(예외)가 발생합니다. 이것은 우리가 앞에서 프로토타입이라고 불렀던 강제 캐스팅 때문입니다. (as! EyeballMonster)

 

Montser의 어떠한 서브클래스에서도 init(_monster:)에 대한 호출을 하지 않는 것이 이상적입니다. 대신 항상 copy()를 통해 호출해야 합니다.

 

하위 클래스 메소드를 "unavailable"로 표시하여 다른 개발자에게 표시할 수도 있습니다. 

 

아래 init코드로 변경해보죠!

@available(*, unavailable, message: "Call copy() instead")

eyeballMonster3를 만들면 아래의 오류가 나타납니다.

error: 'init' is unavailable: Call copy() instead

 

 

What should you be careful about?

 

플레이 그라운드 예시에서 볼 수 있듯이 기본적으로 슈퍼클래스 인스턴스를 서브클래스 copy 이니셜라이저에 전달할 수 있습니다. 서브클래스가 슈퍼클래스 인스턴스에서 완전히 초기활 될 수 있다면 이것은 문제가 되지 않을 수도 있습니다. 하지만 하위 클래스가 새로운 properties를 추가하는 경우에는 상위 클래스에서 초기화하지 못할 수 있습니다.

 

이 문제에 대응하기 위해서 서브클래스 copy 이니셜라이저를 "unavailable"로 표시할 수 있습니다. 이에 대한 응답으로 컴파일러는 이 메소드에 대한 직접 호출을 컴파일 하는 것을 거부합니다.

 

메소드를 copy()와 같이 간접적으로 호출하는 것은 여전히 가능합니다. 하지만 이 방법은 대부분의 use case에 대해서 충분히 양호(good enough)해야 합니다.

 

이렇게 해도 use case(사용 사례)에 대해 문제가 방지되지 않는다면, 문제를 처리할 다른 방법을 고려해야 합니다. 위에서 했던 방법처럼 콘솔에 에러 메시지를 출력하고 컴파일 에러를 내거나 아니면 애초에 default값을 제공하여 처리할 수도 있습니다.

 

 

 

Tutorial project

아래의 예제에서는 자신을 복사해서 패드에 그림을 그렸던 것을 재사용하고 있다.

 

https://github.com/lgvv/DesignPattern/tree/main/prototype-pattern

 

GitHub - lgvv/DesignPattern: ✨ 디자인 패턴을 공부합니다!

✨ 디자인 패턴을 공부합니다! Contribute to lgvv/DesignPattern development by creating an account on GitHub.

github.com

 

Key points

프로토타입 패턴의 핵심에 대해서 정리해봅시다.

 

1. 프로토타입 패턴은 스스로를 객체로 복제가 가능하게 합니다. copying 프로토콜과 프로토콜을 상속받는 두 부분이 필요합니다.

2. copying 프로토콜은 copy 메소드를 선언합니다. 그리고 프로토타입은 프로토콜을 상속받습니다.(따른다)

3. Foundation은 NSCopying 프로토콜을 제공합니다. 하지만 swift에서 아주 작 작동하지 않습니다. 더 쉽게 하려고 우리는 Copying 프로토콜을 만들 수 있고, 다른 프레임워크에 대해 의존도를 제거할 수 있습니다.

4. 프로토콜 생성의 핵심 Copying은 init(_ prototype:)를 사용해서 생성합니다.

 

이번장에서는 MirrorPad의 주요 기능을 구현했습니다. 여기까지도 아주 좋은 앱이지만 애니메이션이 진행되는 동안 계속 그림을 그릴 수도 있습니다. 이는 State Pattern을 통해 알아보도록 합시다!

 

 

(추가) 같이 스터디하시는 분의 자료가 이 패턴을 이해하는데 큰 도움이 되어서 첨부합니다.

 

(참고)

https://charlezz.medium.com/%EB%B3%B4%EC%9D%BC%EB%9F%AC%ED%94%8C%EB%A0%88%EC%9D%B4%ED%8A%B8-%EC%BD%94%EB%93%9C%EB%9E%80-boilerplate-code-83009a8d3297