[swift2.1] Swift 프로토콜 지향 프로그래밍 2

Swift 2015. 12. 9. 01:00



이 글은 아래의 자료를 참고하여 작성하였습니다.



지난 시간에 이어, 프로토콜 지향 프로그래밍에 또다른 예제를 살펴보겠습니다. 프로토콜과 구조체가 어떻게 클래스를 대체할 수 있는지 좀더 자세히 알아보기 위해, 도형 그리기를 목적으로 설계된 Renderer 를 소개합니다.



프로토콜 Renderer

  • Renderer 프로토콜은 그려지는 대상과 그리는 방법을 알고 있습니다. (어디에 어떻게 그릴까? 도화지와 물감)
  • Renderer는 특정 점으로 이동하고, 라인을 그리고, 호를 그리는 함수 인터페이스를 정의합니다.
protocol Renderer {
    func moveTo(p:CGPoint)
    func lineTo(p:CGPoint)
    func arcAt(center:CGPoint, radius:CGFloat, startAngle:CGFloat, endAngle:CGFloat)
}



프로토콜 Drawble

  • Drawable 프로토콜은 그릴 객체(대상)을 표현합니다. (무엇을 그릴까? 그림체의 데이터를 제공)
  • draw 함수는 renderer 프로토콜이 제공하는 인터페이스를 사용하여 도형의 정보를 제공합니다.
protocol Drawable {
    func draw(renderer:Renderer)
}
  • 이제 Drawable을 구현하는 도형을 정의합니다.



다각형

  • Polygon은 정점들의 배열 corners를 갖습니다.
  • draw 함수에서 가장 마지막 정점부터 이웃하는 정점으로 선을 그려 다각형을 그립니다.
struct Polygon: Drawable {

    func draw(renderer: Renderer) {
        renderer.moveTo(corners.last!)
        for p in corners {
            renderer.lineTo(p)
        }
    }
    var corners:[CGPoint] = []
}



  • Circle은 중점과 반지름을 정의합니다.
  • draw 함수에서 중점을 기준으로 0~360도 호를 그려 원을 그립니다.
let twoPi = CGFloat(M_PI * 2)

struct Circle: Drawable {

    func draw(renderer: Renderer) {
        renderer.arcAt(center, radius: radius, startAngle: 0.0, endAngle: twoPi)
    }
    var center:CGPoint
    var radius:CGFloat
}



다이어그램(Drawable 조합을 그릴수 있는 객체)

  • Diagram은 그릴수 있는 도형 리스트인 elements 를 갖습니다.
  • 도형리스트를 순회하며 도형들에게 draw 함수를 위임합니다.
struct Diagram:Drawable {
    func draw(renderer: Renderer) {
        for f in elements {
            f.draw(renderer)
        }
    }
    var elements:[Drawable] = []
}

지금까지 정의한 도형들을 실제로 그려보아야 겠지요? 첫번째로 컨솔에 단순히 좌표를 찍는 TestRenderer 를 정의해보겠습니다.



컨솔에 그리기

  • TestRenderer는 Renderer의 함수 인터페이스를 구현하며 단순히 파라미터들을 print 문으로 출력합니다.
struct TestRenderer:Renderer {
    func moveTo(p:CGPoint) { print("moveTo(\(p.x), \(p.y))") }

    func lineTo(p:CGPoint) { print("lineTo(\(p.x), \(p.y))") }

    func arcAt(center:CGPoint, radius:CGFloat, startAngle:CGFloat, endAngle:CGFloat) {
        print("arcAt:(\(center), radius: \(radius), startAngle: \(startAngle), endAngle:\(endAngle))")
    }
}

이름은 Renderer인데 컨솔에 좌표만을 출력하고 있으니 영 심심하죠? 이제 실제로 UI로 그리기 위한 Renderer를 구현해보겠습니다.



코어 그래픽으로 그리기

  • 재미있는 점은 CGContext에 그리는 CGContextRenderer를 구현하지 않고, 이미 존재하는 CGContext가 Renderer 프로토콜을 구현하도록 확장했다는 것입니다.
  • 이렇게 swift의 extension을 활용하면 Retroactive modeling 이 가능합니다.

    Retroactive modeling은 원래 코드를 수정하지 않고 타입을 확장할 수 있는 기능입니다.

extension CGContext:Renderer {

    func moveTo(p: CGPoint) {
        CGContextMoveToPoint(self, p.x, p.y)
    }
    func lineTo(p: CGPoint) {
        CGContextAddLineToPoint(self, p.x, p.y)
    }
    func arcAt(center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat) {
        let arc = CGPathCreateMutable()
        CGPathAddArc(arc, nil, center.x, center.y, radius, startAngle, endAngle, true)
        CGContextAddPath(self, arc)
        CGContextClosePath(self)
    }
}
  • extension의 또 한가지 재미있는 특징은 프로토콜의 디폴트 구현을 제공할 수 있다는 것입니다.

Renderer 프로토콜에 중점과 반지름만으로 원을 그리는 circleAt 함수를 추가해보겠습니다.

extension Renderer {
    func circleAt(center: CGPoint, radius: CGFloat) {
        arcAt(center, radius: radius, startAngle: 0, endAngle: twoPi)
    }
}

프로토콜에 저장 속성을 정의할수 없다는 제약이 있지만, 글로벌 상수와 다른 멤버함수를 조합하여 디폴트 구현을 적용할수 있다는 것은 큰 장점이라 할수 있겠죠? 공통의 디폴트 구현을 제공하고, 특정 함수들만 프로토콜 구현체에서 제공하는 방식으로 코드량을 줄일 수 있기 때문입니다.



Playground에서 어떻게 그려지는지 볼까요?

  • 원과 삼각형을 정의합니다.
var circle = Circle(center: CGPoint(x: 187.5, y: 333.5), radius: 93.75)

var triangle = Polygon(corners: [
        CGPoint(x: 187.5,  y: 427.25),
        CGPoint(x: 268.69, y: 286.625),
        CGPoint(x: 106.31, y: 286.625)
    ])
  • RendererView를 정의하고, circle과 triangle을 Diagram으로 그룹핑합니다.
  • drawRect 함수에서 얻은 CGContext는 Renderer 프로토콜을 구현하기 때문에, draw 함수에 전달할 수 있습니다.
class RendererView:UIView {
    var diagram = Diagram(elements: [circle, triangle])

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = UIColor.blackColor()
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.backgroundColor = UIColor.blackColor()
    }
    override func drawRect(rect: CGRect) {
        let ctx = UIGraphicsGetCurrentContext()!

        UIColor.magentaColor().setStroke()
        CGContextSetLineWidth(ctx, 2)

        diagram.draw(ctx)

        CGContextDrawPath(ctx, CGPathDrawingMode.FillStroke)
    }
}




  • 자! 이제 결과를 볼까요?
import UIKit
import XCPlayground

...

var rendererView = RendererView(frame: CGRectMake(0, 0, 320, 568))

// deprecated
// XCPShowView("rendererView", view: rendererView)

XCPlaygroundPage.currentPage.liveView = rendererView




프로토콜 지향 프로그래밍에 대한 감이 조금 오셨나요? 그렇지 않았더라도 사고의 전환을 환기시키는 예제라는 생각이 드네요!


ProtocolOriented2.playground.zip





[swift1.2] Swift 프로토콜 지향 프로그래밍

Swift 2015. 8. 6. 00:57



WWDC 2015 세션중 "Protocol-Oriented Programming in Swift" 라는 흥미로운 세션이 있습니다. 애플에서 Standard Library 그룹의 리더를 맡고 있는 Dave Abrahams 라는 분이 발표한 세션입니다. 세션에서 제공하는 슬라이드와 InfoQ에서 이 세션을 정리한 아티클을 바탕으로 내용을 요약했습니다.


클래스는 멋집니다!


다음과 같은 멋진 특징들을 제공하지요.

  • 캡슐화 (Encapsulation)
  • 접근제어 (Access Cotrol)
  • 추상화 (Abstraction)
  • 네임스페이스 (Namespace)
  • 풍부한 문법 (Epressive Syntax)
  • 확장성 (Extensibility)

그런데...


구조체로도 다 할수 있습니다.

클래스는 훌륭합니다. 타입은 훌륭합니다!

사실 위 특징들은 모든 타입들의 특징이며 클래스는 그것을 구현하는 하나의 방법에 불과합니다. 그렇다면 클래스 사용으로 인한 단점은 무엇이 있을까요?


1. 암묵적인 참조(Reference) 공유

  • 두 객체가 동시에 어떤 객체를 참조하는 경우, 서로 그 사실을 모른채 그 객체를 변경할 수 있습니다.
  • 참조 공유를 방지하기 위해 참조하는 객체를 복사할 수 있지만, 그렇게 하면 효율성이 나빠집니다.
  • 참조를 공유하는 객체에 Race Conditions 이 발생할 수도 있습니다.
  • Lock을 사용하여 레이스 컨디션을 방지할수 있지만 효율성은 더 나빠집니다.
  • Lock을 잘못 사용하면 Deadlock 상태에 빠질 수 있습니다.
  • 잦은 Lock 사용은 코드를 복잡하게 합니다.
  • 복잡성은 더 많은 버그를 유발합니다.

Values Don't Share. (That's a good thing)


2. 상속 (비지니스에 관한 모든 것을 가지고 있습니다.)

  • 슈퍼 클래스를 하나 밖에 가질수 없기 때문에, 초기에 잘 선택해야합니다.
  • 슈퍼 클래스를 나중에 변경하는 것은 매우 어렵습니다.
  • 슈퍼클래스가 갖는 모든 저장속성은 서브클래스에도 (강제적으로) 상속됩니다.
  • 초기화 처리가 복잡해집니다.
  • 수퍼클래스의 불변성을 깨뜨리지 않을수 없게됩니다.
  • 언제 무엇을, 어떻게 재정의해야 하는지 (수퍼 클래스에 대해서)알아야 합니다.

More and more, we promote delegation.


3. 타입관계를 잃어버립니다.(Lost Type Relationships.)

  • 추상 수퍼클래스와 서브클래스의 구현코드가 함께 존재하게 됩니다.
  • 서브클래스 메소드에 접근하기 위해 수퍼클래스를  서브클래스로 다운캐스팅하면서 타입관계를 잃게됩니다.

  • 이진탐색 클래스 모델 Ordered 정의

class Ordered {
    func precedes(other: Ordered) -> Bool { fatalError("메소드를 구현해주세요.") }
}

class Number:Ordered {
    var value:Double = 0
    override func precedes(other: Ordered) -> Bool {
        return value < (other as! Number).value // 
    }
}  

func binarySearch(sortedKeys: [Ordered], forkey k: Ordered) -> Int {
    var lo = 0
    var hi = sortedKeys.count
    while hi > lo {
        let mid = lo + (hi - lo) / 2
        if sortedKeys[mid].precedes(k) { lo = mid + 1 }
        else { hi = mid }
    }
    return lo
}

as! ASubclass 는 타입 관계를 잃어버렸다는 신호
대부분 추상화를 위해 클래스를 사용하기 때문이다.


4. 좋은 추상화 메커니즘이란?

  • Value type을 지원합니다.(클래스 외에도)
  • 정적 타입관계를 지원합니다. (동적 디스패치 외에도)
  • 큰 덩어리로 뭉치지 않아야합니다.
  • Retroactive 모델링을 지원합니다.
  • 모델에 인스턴스 데이터를 강요하지 않아야 합니다.
  • 모델에 초기화의 부담이 없어야 합니다.
  • 무엇을 구현해야 하는지 명확해야 합니다.

Swift는 프로토콜 지향 프로그래밍 언어!


5. 프로토콜 지향 프로그래밍

  • Swift에서 새롭게 추상화를 생각할 때 첫 번째 포인트는 프로토콜!

이진탐색 프로토콜 모델 Ordered 변경해보겠습니다.
protocol Ordered {
    func precedes(other:Self) -> Bool
}    

struct Number : Ordered {
    var value:Double = 0
    func precedes(other:Number) -> Bool {
        return self.value < other.value 
    }
}

이진탐색 함수를 정의합니다.
func binarySearch<T:Ordered>(sortedKeys:[T], forKey k: T) -> Int {
    var lo = 0
    var hi = sortedKeys.count
    while hi > lo {
        let mid = lo + (hi-lo)/2 
        if sortedKeys[mid].precedes(k) { lo = mid + 1 }
        else { hi = mid }
    }
    return lo
}

Int 배열에 이진탐색 함수를 적용해 보겠습니다.
  • 먼저 Int가 프로토콜 Ordered를 구현해야합니다.
// Int형 Ordered 프로토콜 구현하기 

extension Int:Ordered {
    func precedes(other: Int) -> Bool {
        return self < other
    }
}
  • Int 배열에 이진탐색을 적용합니다.
// 1. 키 정렬 
var keys = [10, 30, 1, 4, 5, 6, 7, 8, 55, 20]
var sortedKeys = keys.sorted {  $0 < $1 }
// sortedKeys: [1, 4, 5, 6, 7, 8, 10, 20, 30, 55]

// 2. 찾을 키
var searchKey = 30
// 키 30의 index는 8.

// 3. 이진탐색 
var searchIndex = binarySearch(sortedKeys, forkey: searchKey)
// searchIndex: 8
Number 배열에도 이진탐색을 적용해보겠습니다.
// 1. 정렬된 Number 배열 생성  
var numbers = sortedKeys.map { return Number(value: Double($0)) }
// numbers: [1.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0, 20.0, 30.0, 55.0]

// 2. 찾을 키
var keyToSearch = 5.0

// 3. 이진탐색
var index = binarySearch(numbers, forkey: Number(value: keyToSearch))
// index: 2

프로토콜을 사용하면, 커스텀 Number 구조체 뿐만 아니라 기존 타입에도 이진탐색을 쉽게 적용할 수 있음을 알 수 있습니다.