총평

팀리뷰에서 Swift 관례 및 용례를 잘못 사용한다는 지적이 많아서, 해당 사항을 보완하기 위해서 몇가지 문서를 먼처 챙겨 읽기로 하였다. Java의 관례를 준수하며 Swfit로 코드를 작성하면 이런 사태가 벌어질 것을 알았으나, 당장 혼자서 해당 문제를 해결할 수 없어서 코드 리뷰의 힘을 빌려서 코드 품질을 높이는데 중점을 두고 있다. 이렇듯, 코드와 자신을 물아일체로 취급하지 않고 객관적으로 유지하면서 리뷰를 통해서 얻은 많은 교훈을 발판으로 더 성장하기 위해선 좋은 가이드라인이 필요한데, 애플에서 제시한 API Design Guidelines은 좋은 가이드라인이다. 기회가 된다면 꼭 읽어보자.

API Design Guidelines

Fundamentals

  • Clarity at the point of use is your most important goal.
    • API는 사용 시점(at the pooint of use)에 명확(clarity)해야 합니다.
    • API는 문맥(a use case)에서 명확하게 사용되고 있는지 확인해야 합니다.
    • API를 잘못 사용하고 있다면 API를 잘못 만든 겁니다.
  • Clarity is more important than brevity.
    • 명확성(clarity)은 간결성보다 중요합니다.
    • 코드를 극도로 짧게 만드는게 목표가 아닙니다. 코드는 명확하게 작성해야 합니다.
  • Write a documentation comment for every declaration.
    • 문서화용 주석을 작성하세요.
      • 문서의 구성은 요약 - 빈줄 - 추가사항 - 매개변수 - 용어설명 등과 같이 Apple에서 제시하는 가이드라인 및 애플의 Swift 소스 코드 등에서 확인할 수 있습니다.
    • 문서 작성시 얻은 통찰력은 더 좋은 API 설계로 이어집니다. 좋은 문서는 좋은 API를 설계할 수 있는 근본이 됩니다.
    • 시작은 짧은 요약, 가능하다면 좀 더 자세한 사항을 기록해보자. 문서 작성에 관한 자세한 사항은 이것을 참고하세요.
/// Returns a "view" of `self` containing the same elements in
/// reverse order.
func reversed() -> ReverseCollection

/// Writes the textual representation of each    ← 요약
/// element of `items` to the standard output.
///                                              ← 빈줄
/// The textual representation for each item `x` ← 추가 사항
/// is generated by the expression `String(x)`.
///
/// - Parameter separator: text to be printed    ⎫
///   between items.                             ⎟
/// - Parameter terminator: text to be printed   ⎬ 매개변수
///   at the end.                                ⎟
///                                              ⎭
/// - Note: To print without a trailing          ⎫
///   newline, pass `terminator: ""`             ⎟
///                                              ⎬ Symbol commands
/// - SeeAlso: `CustomDebugStringConvertible`,   ⎟
///   `CustomStringConvertible`, `debugPrint`.   ⎭
public func print(
  _ items: Any..., separator: String = " ", terminator: String = "\n")

Naming

Promote Clear Usage

  • 모호성을 피하기 위해선 필요한 모든 단어를 사용하세요.

    extension List {
      public mutating func remove(at position: Index) -> Element
    }
    employees.remove(at: x)
    employees.remove(x) // x가 무엇인가?
    
  • 하지만 불필요한 단어는 생략하세요.

    public mutating func remove(member: Element) -> Element?
    allViews.remove(cancelButton) // good
    
    public mutating func removeElement(member: Element) -> Element?
    allViews.removeElement(cancelButton) // bad
    
  • 역할을 중심으로 생각하세요.

    // bad
    var string = "Hello"
    protocol ViewController { 
    	associatedtype ViewType : View
    }
    class ProductionLine {
      func restock(from widgetFactory: WidgetFactory)
    }
    
    // good
    var greeting = "Hello"
    protocol ViewController {
      associatedtype ContentView : View
    }
    class ProductionLine {
      func restock(from supplier: WidgetFactory)
    }
    
  • 매개변수의 역할을 명확히 하세요.

    // bad
    func add(observer: NSObject, for keyPath: String)
    grid.add(self, for: graphics) // bad
    
    // good
    func addObserver(_ observer: NSObject, forKeyPath path: String)
    grid.addObserver(self, forKeyPath: graphics) // good
    

Strive for Fluent Usage

  • 문장 구조 형태를 사용하세요.

    // bad
    x.insert(y, position: z)
    x.subViews(color: y)
    x.nounCapitalize()
    
    // good
    x.insert(y, at: z)          “x에서 z에다 y를 삽입”
    x.subViews(havingColor: y)  “색상 y을 갖는 x의 subviews”
    x.capitalizingNouns()       “x에 명사를 대문자화”
    
  • make로 팩토리 메소드 이을 시작하세요.

    x.makeIterator()
    
  • 이니셜라이저와 팩토리 메소드 호출은 첫 번째 인자를 포함하지 않는 문구로 구성해야 합니다. e.g.

    // good
    let foreground = Color(red: 32, green: 64, blue: 128)
    let newPart = factory.makeWidget(gears: 42, spindles: 14)
    let ref = Link(target: destination)
    
    // bad
    let foreground = Color(havingRGBValuesRed: 32, green: 64, andBlue: 128)
    let newPart = factory.makeWidget(havingGearCount: 42, andSpindleCount: 14)
    let ref = Link(to: destination)
    
  • 사이드 이펙트에 따라 함수와 메소드 이름을 지정합니다.

    • 사이드 이펙트가 없는 것은 명사, 사이드 이펙트가 있는 것은 동사로 작성합니다.
    // 사이드 이펙트 없음
    x.distance(to: y)
    i.successor()
    
    // 사이드 이펙트 있음
    print(x)
    x.sort()
    x.append(y)
    
    • mutating v.s. nonmutating
    // mutating
    x.sort()
    x.append(y)
    
    // nonmutating
    z = x.sorted() // 목적어가 없다면 ~ed
    z = x.appending(y) // 목적어가 있다면 ~ing
    
    // mutating
    y.formUnion(z)
    c.formSuccessor(&i)
    
    //nonmutating
    x = y.union(z)
    j = c.successor(i)
    
    • Boolean 메소드와 속성 사용은 nonmutating일 때, 수신자(receiver)에 관한 주장으로 해석해야 합니다
    x.isEmpty
    line1.intersects(line2).
    
    • 무언가를 설명하는 프로토콜은 명사로 읽어야 합니다.
    Collection
    
    • 능력을 설명하는 프로토콜은 ableible 또는 ing 접미사를 사용하여 이름을 지정해야 합니다.
    Equatable
    ProgressReporting
    
    • 다른 타입, 속성, 변수 그리고 상수의 이름은 명사로 읽어야 합니다.

Use Terminology Well

  • Term of Art - 특정 분야 또는 직군 내에서 정확하고 특수한 의미를 지닌 단어 또는 구문을 뜻하는 것으로 전문 용어(Term of Art)를 잘 활용하세요.
    • 애매한 용어는 피하세요.
    • 해당 용어를 명확하게 사용해서 기존에 익숙한 전문가나 처음 접하는 초보자에게 해당 용어가 쉽게 받아들여질 수 있게 사용하세요.
    • 약자/약어는 주의해서 사용하세요(그냥 사용하지 말자)
    • 관례를 받아들이세요.

Conventions

General Conventions

  • 프로퍼티(속성)을 참고할 때 중요한 계산을 하지 않는다고 가정하는데, 이런 가정은 프로퍼티에 대한 일반적인 모델에 기인합니다. 만약 이러한 가정이 어긋날 수 있을 때 경고해야 합니다.

  • 일반적인 함수 보다는 메소드와 프로퍼티를 사용하세요. 함수는 특수한 상황에서만 사용하세요.

    min(x,y,z) // 순수한 함수
    print(x) // 타입이 generic 일 때
    sin(x) // 함수 구문이 대표성이 있을 때
    
  • 타입과 프로토콜의 이름은 UpperCamelCase입니다. 나머지는 모두 lowerCamelCase입니다. 하지만 잘 알려진 두문자의 경우 일관되게 작성해야 합니다.

    var utf8Bytes: [UTF8.CodeUnit]
    var isRepresentableAsASCII = true
    var userSMTPServer: SecureSMTPServer
    
  • 메소드는 동일한 의미를 공유하거나, 특정 영역에서 작동할 때 오버로딩을 허용하세요. 하지만 반환값에 대한 오버로드는 피하세요.

    extension Shape {
      func contains(_ other: Point) -> Bool { ... }
      func contains(_ other: Shape) -> Bool { ... }
      func contains(_ other: LineSegment) -> Bool { ... }
    }
    
    // 반환값에 대한 오버로드는 피하세욤
    extension Box {
      func value() -> Int? { ... }
      func value() -> String? { ... }
    }
    

Parameters(매개변수)

  • 파라미터 이름은, 함수 또는 메소드의 사용 시점에 나타나지 않지만 중요한 설명 역할을 합니다.

    /// Return an `Array` containing the elements of `self` 
    /// that satisfy `predicate`. 
    func filter(_ predicate: (Element) -> Bool) -> [Generator.Element] 
    
    /// Replace the given `subRange` of elements with `newElements`. 
    mutating func replaceRange(_ subRange: Range, with newElements: [E])
    
  • 사용성을 단순화하기 위해선 기본 매개변수를 활용하세요.

    let order = lastName.compare(royalFamilyName, options: [], range: nil, locale: nil)
    
    // 메소드를 더 쉽게 사용할 수 있음
    let order = lastName.compare(royalFamilyName)
    
  • Parameter 목록의 뒷 부분에, 기본값이 있는 매개변수를 사용하세요. 기본값이 없는 매개변수는 일반적으로 메소드의 의미에 더 중요하기 때문입니다.

Argument Labels(인자 레이블)

  • 인자가 유용하게 구별될 수 없을 때 모든 레이블은 생략합니다.

    min(number1, number2)
    
  • 타입을 변환하는 과정에서 값을 보존하는 생성자의 경우 첫번째 인자 레이블을 생략하세요.

    • 첫 번째 인수는 항상 변환해야 할 값이어야 합니다.
    • 타입이 변화하는 과정에서 기본 값을 축소할 경우 인자 레이블을 사용해서 설명하세요.
    Int64(someUInt32)
    
    extension String {
      init(_ x: BigInt, radix: Int = 10)   ← 처음 밑줄을 주의
    }
    
    extension UInt32 {
      init(_ value: Int16)            ← Widening이고 label이 아님
      init(truncating source: UInt64)
      init(saturating valueToApproximate: UInt64)
    }
    
  • 첫번째 인수가 전치사구의 일부인 경우, 인자 레이블을 지정하세요.

    a.moveTo(x: b, y: c)
    a.fadeFrom(red: b, green: c, blue: d)
    
  • 첫번째 인수가 문법적인 구문 일부 형태일 때, 인자 레이블을 생략하고 기본 이름에 선행 단어를 추가하세요.

    x.addSubview(y)
    
  • 이외 모든 인자는 인자 레이블을 지정하세요.

Special Instructions

  • Tuple 멤버에게 레이블을 지정하고, API에 표시되는 클로져 매개변수 이름을 지정합니다.

    • 클로저에서 사용될 때 기술적으로 인자 레이블이지만, 레이블을 선택해야 하고 매개 변수 이름이었던 것처럼 문서에서 사용해야 합니다. 함수 본체에서 클로저 호출은 첫 번째 인자를 포함하지 않는 기본 이름에서 구문을 시작하는 함수를 일관되게 읽을 수 있습니다:
    mutating func ensureUniqueStorage(
    	minimumCapacity requestedCapacity: Int, 
    	allocate: (byteCount: Int) -> UnsafePointer<Void>) -> 
    (reallocated: Bool, capacityChanged: Bool)
    
    allocate(byteCount: newCount * elementSize)
    
  • 모호성을 피하도록 제약되지 않은 다형성에 주의 하세요.

    struct Array {
    
      public mutating func append(newElement: Element)
    
      public mutating func append<
    		S : SequenceType where S.Generator.Element == Element
    	>(contentsOf newElements: S)
    
    }