본문 바로가기

iOS/Swift

Swift API Design Guidelines

https://www.swift.org/documentation/api-design-guidelines/

 

Swift.org

Swift is a general-purpose programming language built using a modern approach to safety, performance, and software design patterns.

www.swift.org

 

최고의 컨벤션


Fundamentals

  • 사용 지점에서의 명료함(Clarity)이 가장 중요한 목표이다.
    • 메서드, 프로퍼티는 한번 정의되지만 반복적으로 사용된다.
    • API를 설계하면 이들을 명확하고 간결하게 만든다.
    • 설계를 평가할 때, 선언을 읽는 것 만으로는 불충분하다. 항상 사용성을 검토하여 문맥상 명확하게 이해되는지를 확인해야 한다.
  • Clariry(명료함) > Brevity(간결함)
    • 명확한 것이 간결한 것보다 중요하다.
    • 스위프트 코드의 목적은 가능한한 작은 사이즈의 간결한 코드를 작성하는 것이 아니다.
    • Swift 코드의 간결함(Breivity)은 강력한 type 시스템과 boilerplate 코드를 줄여주는 기능들이 제공하는 부수적인 효과이다.
  • 모든 선언에 Documentation Comment(문서화용 주석)를 작성하라
    • Documentation을 작성함으로 새로운 인사이트를 얻을 수 있다.
    • API의 기능을 간단한 용어로 설명하기 어렵다면 잘못된 설계일 확률이 높다.
    • Swift의 Dialect of Markdown을 활용하라

Dialect of Markdown

  • opt + 클릭으로 함수의 설명을 나타내도록 도와주는 Mark Up으로 변환해주는 언어
    • parameter: 함수 매개변수설명
    • Throws: 에러를 설명
    • Returns: 리턴값을 설명
    • Important: 중요한 설명
    • Note: 노트
    • Version: 버전기재
// MARK: 방법1

/// - parameter name : 이름
/// - parameter age : 나이
func myFunction(name: String, age: Int) {}

// MARK: 방법2
/// - parameters:
///     - name: 이름
///     - age : 나이
func myFunction2(name:String,age:Int){}

// MARK: 방법3
/**
 - parameters:
    - name: 이름
    - age: 나이
 */
func myFunction3(name:String,age:Int){}

 

 

선언된 개체의 Summary에서 부터 시작한다

/// Returns a "view" of `self` containing the same elements in
/// reverse order.
func reversed() -> ReverseCollection

 

  • Foucs on the summary
  • Use a single sentence fragment
    • 가능하면 마침표로 끝나는 단일 문장을 사용하라
    • 필요에 따라 세미콜론(;)을 통해 여러 문장으로 구성할 수 있다.
      • 아래 popFirst 참고
  • Describe what a function or method does and what it returns
    • 함수,메서드가 무슨 기능을 하는지, 어떤 반환 값을 갖는지를 설명하라
    • null과 Void 반환 값에 대한 설명은 생략하라.
/// Inserts `newHead` at the beginning of `self`.
mutating func prepend(_ newHead: Int)

/// Returns a `List` containing `head` followed by the elements
/// of `self`.
func prepending(_ head: Element) -> List

/// Removes and returns the first element of `self` if non-empty;
/// returns `nil` otherwise.
mutating func popFirst() -> Element?

 

  • Describe what a subscript accesses
    • 서브스크립트가 접근하는 것이 무엇인지 설명하라
/// Accesses the `index`th element.
subscript(index: Int) -> Element { get set }
  • Describe What an initializer creates
    • 생성자가 만드는 것이 무엇인지 설명하라
/// Creates an instance containing `n` repetitions of `x`.
init(count n: Int, repeatedElement x: Element)

 

  • For all other declarations, describe what the declared entitiy is
    • 다른 모든 선언들은 그 엔티티가 무엇인지에 대하여 설명하라
/// A collection that supports equally efficient insertion/removal
/// at any position.
struct List {

  /// The element at the beginning of `self`, or `nil` if self is
  /// empty.
  var first: Element?
  ...

 

Blank line을 사이에 추가하고 Additional Description을 추가할 수 있다.

/// Writes the textual representation of each    ← Summary
/// element of `items` to the standard output.
///                                              ← Blank line
/// The textual representation for each item `x` ← Additional discussion
/// is generated by the expression `String(x)`.
///
/// - Parameter separator: text to be printed    ⎫
///   between items.                             ⎟
/// - Parameter terminator: text to be printed   ⎬ Parameters section
///   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

  • 사람이 읽었을 때 모호함이 없는 코드가 좋은 코드이다.
    • 예를들면 List의 remove(at:) 메서드에서 at이 없다면 인덱스를 의미하는 것인지 원소 그 자체를 의미하는 것인지 모호해진다.
  • 필요없는 단어들은 생략하라
    • 읽는 사람이 이미 보유하고 있는 정보들에 대하여 중복된 정보들을 위한 단어들은 생략하라
    • 예를들면 removeElement(_ member: Element) -> Element? 보다 remove(_ member: Element) -> Element 가 더 낫다.
    • 때때로 모호함을 피하기 위해 타입 정보를 반복적으로 알리는 것이 필요할 수는 있지만 일반적으로 타입 정보를 사용하는 것 보다 그 파라미터의 역할에 대해 설명하는 단어만 포함하는 것이 더 좋다.
  • Name variables, parameters, associated types는 그들의 역할에 따라 이름을 지어야한다. 그들의 타입에 얽매인 타입 이름은 좋지 않다.
// Not Good

var string = "Hello"
protocol ViewController {
    associatedtype ViewType : View 
}
class ProductLine {
    func restock(from widgetFactory : WidgetFactory)
}

// Better

var greeting = "Hello"
protocol ViewController {
    associatedtype ContentView : View
}
class ProductLine {
    func restock(from supplier: WidgetFactory)
}

 

  • 만약 associated type에서 프로토콜 이름 자체가 그 역할이 되는 경우는 프로토콜 이름에 Protocol 을 추가하여 네이밍에 대한 충돌을 방지하는 것이 좋다.
protocol Sequence {
  associatedtype Iterator : IteratorProtocol
}
protocol IteratorProtocol { ... }

 

  • 파라미터의 역할을 명확하기 위해서 타입 정보를 보완할 수도 있다.
    • 파라미터 타입이 기본 타입(NSObject, Any, AnyObject, 기본 자료형 .. )인 경우 타입 정보와 파라미터의 의미를 명확히 전달하지 못할 위험이 있다.
      • 아래 1번 코드는 선언 자체는 명확하지만 사용 시점에서 모호함을 갖게 된다. 따라서 2번 코드가 더 좋다.
// 1

func add(_ observer: NSObject, for keyPath: String)
grid.add(self, for: graphics) //vague

// 2

func addObserver(_ observer: NSObject, forKeyPath path : String)
grid.addObserver(self, forKeyPath: graphics) // clear

 

Strive for Fluent Usage

  • 메서드,함수 이름은 사용 시점에서 영문법 적으로 자연스러운 것이 선호된다. 
x.insert(y, at: z)         // “x, insert y at z”
x.subViews(havingColor: y) // “x's subviews having color y”
x.capitalizingNouns()      // “x, capitalizing nouns”

 

  • 첫 번째 또는 두 번째 인자가 호출 의미에서 중심이 아닐 때 Fluency 가 조금 떨어져도 괜찮다.
// It is acceptable for fluency to degrade after the first argument or two when those arguments are not central to the call’s meaning:

AudioUnit.instantiate(
  with: description,
  options: [.inProcess], completionHandler: stopProgressBar)
)

 

  • Factory 메서드의 경우 make 라는 단어로 시작하여 만들어라 (ex. x.makeIterator())

 

  • 생성자, 팩토리 메서드 호출은 첫 번째 파라미터를 포함하지 않는 구문으로 구성해야 한다.
    • 아래 1번 코드는 첫 번째 인자와 문법의 연속성을 완성시키고 있다. 옳지 않다. 
    // 1
    // Not Good
    
    let foreground = Color(havingRGBValuesRed : 32, green : 64, andBlue: 128)
    let newPart = factory.makeWidget(havingGearCount: 42, andSpindleCount : 14)
    
    // 2
    // Good
    
    let foreground = Color(red:32, green:64, blue:128)
    let newPart = factory.makeWidget(gears:42, spindles: 14)
    • 이 가이드 라인은, 메서드 호출이 타입 보존 변환(value preserving type conversions) 을 수행하지 않는다면 첫 번째 인자는 label을 가져야 함을 의미하기도 한다.
      • 타입이 보존되지 않는 생성자, 팩토리 메서드의 경우는 label을 추가
  • Side-effects를 고려하여 함수와 메서드의 이름을 정하라
    • side-effect가 없는 경우 : 명사구로 읽어야 한다.
      • x.distance(to: y), i.successor()
    • side-effect가 있는 경우 : 동사구로 읽어야 한다.
      • print(x) , x.sort(), x.append(y)
    • Mutating / nonmutating 메서드 쌍의 이름이 일치해야 한다.
      • mutating 메서드는 의미적으로 동일한 nonmutating 메서드를 갖는 경우가 종종 있다. ( nonmutating은 새로운 값을 반환하는 형태 )
      • 메서드 이름이 동사로 표현될 경우 ed 또는 ing 를 mutating에 추가하여 nonmutating 네이밍을 한다. 
        • ex. x.sort() , z = x.sorted()
        • ex2. x.append(y), z = x.appending(y)
      • 메서드 이름이 명사로 표현될 경우 form 을 mutating에 추가한다.
        • ex. x = y.union(z) , y.formUnion(z)
        • ex2. j = c.successor(i) , c.formSuccessor(&i)
  • Boolean 메서드 및 프로퍼티는 nonmutating으로 사용되는 경우 리시버에 대한 단언문(asserition)으로 읽혀야 한다.
    • ex. x.isEmpty line1.intersects(line2)
  • 무엇인지를 설명하는 프로토콜은 명사로 읽혀야 한다.
    • ex. Collection
  • 어떤 기능(능력)을 설명하는 프로토콜은 able , ible , ing 과 같은 suffixes로 나타낸다.
    • ex. Equatable, ProgressReporting
  • 타입, 프로퍼티, 변수, 상수는 명사로 네이밍한다.

Use Terminology Well

Term Of Art : 특정 분야에서 정확하고 전문성을 나타내는 단어 또는 구

  • 불명확한 용어는 피해야 한다.
    • 해당 용어를 나타내는 일반적인 단어를 선택하라
    • Term of art는 불가피할 경우만 사용하라
  • Term of art를 사용해야 한다면, 기존 의미를 고수해야 한다.
    • 일반적인 단어보다 기술적인 용어를 사용하는 유일한 이유는 일반적인 단어들을 사용했을 때 불분명하거나 모호한 경우이다.
    • 따라서 이를 사용할 경우는 반드시 기존에 받아들여지는 의미를 그대로 가져와 사용해야 한다.
    • 전문가들을 당황시키지마라
      • 만약 전문 용어에 새로운 의미를 부여한다면 이미 그 단어에 익숙한 전문가들에게 혼란을 야기한다.
    • 비전문가를 혼돈시키지마라
      • 그 용어에 익숙치 않은 사람들은 웹 서치를 하게 될텐데 기존 의미와 다르다면 혼돈을 가져오게 된다.
  • 약어는 피해야 한다.
    • 약어를 사용하는 것은 일종의 Term of art와 같다. 제대로 풀어쓰지 않으면 코드 이해에 방해 된다.
    • 약어를 사용해야 한다면 웹에서 쉽게 찾을 수 있어야 한다. (일반적으로 통용되어야 함)
  • 선례는 받아들여야 한다.
    • 기존 문화에 순응하는 대가로 모든 초보자를 위한 용어로 최적화하면 안된다.
    • 예를들면 Array와 List중 네이밍을 위해서 Array를 사용하는 것이 좋다.
    • List라는 이름이 초보자들이 더욱 쉽게 파악할 수 있겠지만 Array는 거의 모든 프로그래머들이 배우는 용어이다.
    • 많은 개발자들이 친숙한 용어를 사용하는 것이 좋다.
    • 수학적 기호 sine을 veverticalPositionOnUnitCircleAtOriginOfEndOfRadiusWithAnglertical(x) 라고 사용하지 않고 sin(x)라고 사용하는 것도 같은 이치이다.

Conventions

General Conventions

  • Computed Property는 O(1)의 시간복잡도로 접근할 수 없는 경우 문서를 작성하라
    • 사람들은 프로퍼티 접근에 대해서 중요한 계산들을 수반하지 않는다고 생각하는 경향이 있다. 이러한 가정에 반하는 경우 알려야 한다.
  • 전역함수(free function) 보다 메서드와 프로퍼티를 사용하라
    • 전역함수는 다음과 같은 상황에서만 사용해야 한다.
      1. 명확한 self가 없을 경우
        • ex. min(x,y,z)
      2. 함수가 제약되지 않은 제네릭인 경우
        • ex. print(x)
      3. 햠수의 용어가 도메인 표기의 일부로 설정된 경우
        • ex. sin(x)
  • 이름 짓기 규칙
    • 타입, 프로토콜 : UpperCamelCase
    • 그 외 : lowerCamelCase
    • 영미권에서 모두 대문자로 나타내는 두문자어의 경우 관례에 따라 모두 대문자 또는 모두 소문자로 나타내야 한다.
var utf8Bytes: [UTF8.CodeUnit]
var isRepresentableAsASCII = true
var userSMTPServer: SecureSMTPServer

 

  • 의미는 같고 내부 연산이 다른 메서드들은 base name을 공유할 수 있다.
extension Shape {
  /// Returns `true` iff `other` is within the area of `self`.
  func contains(_ other: Point) -> Bool { ... }

  /// Returns `true` iff `other` is entirely within the area of `self`.
  func contains(_ other: Shape) -> Bool { ... }

  /// Returns `true` iff `other` is within the area of `self`.
  func contains(_ other: LineSegment) -> Bool { ... }
}
extension Collection where Element : Equatable {
  /// Returns `true` iff `self` contains an element equal to
  /// `sought`.
  func contains(_ sought: Element) -> Bool { ... }
}

 

  • 메서드의 의미 자체가 다른 경우는 다른 네이밍을 해야 한다.
// Not Good

extension Database {
  /// Rebuilds the database's search index
  func index() { ... }

  /// Returns the `n`th row in the given table.
  func index(_ n: Int, inTable: TableID) -> TableRow { ... }
}

 

  • 반환 타입의 오버로딩은 타입 추론의 모호함을 만들기 때문에 피해야 한다.
extension Box {
  /// Returns the `Int` stored in `self`, if any, and
  /// `nil` otherwise.
  func value() -> Int? { ... }

  /// Returns the `String` stored in `self`, if any, and
  /// `nil` otherwise.
  func value() -> String? { ... }
}

Parameters

func move(from start Point, to end: Point)

  • 문서로 제공할 파라미터 이름을 결정하라
    • 사용 시점에 파라미터 이름이 직접 노출되지는 않지만 파라미터에 대한 설명을 해주는 중요한 역할을 수행한다.

좋은 예 : read naturally

/// 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])

 

나쁜 예 : awkward, ungrammatical

/// Return an `Array` containing the elements of `self`
/// that satisfy `includedInResult`.
func filter(_ includedInResult: (Element) -> Bool) -> [Generator.Element]

/// Replace the range of elements indicated by `r` with
/// the contents of `with`.
mutating func replaceRange(_ r: Range, with: [E])

 

  • 디폴트 매개변수는 일반적인 사용을 simplify할 때 이용하라.

하나만 흔히 사용하는 값을 가진 매개변수는 디폴트 값을 고려해야 한다.

// Not Good
let order = lastName.compare(
  royalFamilyName, options: [], range: nil, locale: nil)
  )

// Good; 디폴트 값을 메서드 선언 시 지정해라
let order = lastName.compare(royalFamilyName)

 

이러한 디폴트 값들은 API를 이해하고 사용하려는 사람들의 인저적 부담을 덜어준다.

// Not Good
extension String {
  /// ...description 1...
  public func compare(_ other: String) -> Ordering
  /// ...description 2...
  public func compare(_ other: String, options: CompareOptions) -> Ordering
  /// ...description 3...
  public func compare(
     _ other: String, options: CompareOptions, range: Range) -> Ordering
  /// ...description 4...
  public func compare(
     _ other: String, options: StringCompareOptions,
     range: Range, locale: Locale) -> Ordering
}

// Good
extension String {
  /// ...description...
  public func compare(
     _ other: String, options: CompareOptions = [],
     range: Range? = nil, locale: Locale? = nil
  ) -> Ordering
}

 

  • 디폴트 값을 갖는 파라미터는 끝으로 위치시켜라
    • 디폴트 값을 갖지 않는 파라미터가 의미적으로 더 중요하다.
    • 또한 안정적인 사용의 초기 패턴을 제공한다.

Argument Labels

  • 매개변수가 구분되는 것이 의미가 없는 경우 모든 label을 생략하라
    • ex. min(number1,number2)
  • 생성자에서 손실 없이 타입 변환이 이루어지는 경우는 첫번째 argumetLabel을 생략한다.
    • ex. Int64(someUInt32)

첫번째 인자는 항상 변환되는 데이터가 되어야 한다.

 

extension String{
  // Convert `x` into its textual representation in the given radix
  init(_ x: BigInt, radix: Int = 10)
}

text = "The value is: "
text += String(veryLargeNumber)
text += " and in hexadecimal, it's"
text += String(veryLargeNumber, radix: 16)

 

더 좁은 타입으로의 변환인 경우 narrowing을 설명해주는 것이 권장된다. truncating, saturating

extension UInt32 {
  /// Creates an instatnce having the specified `value`.
  init(_ value: Int16) ← Widening, so no label
  /// Creates an instance having the lowest 32 bits of `source`.
  init(truncating source: UInt64)
  /// Creates an instance having the nearest representable
  /// approximation of `valueToApproximate`.
  init(saturating valueToApproximate: UInt64)
}

 

  • 첫 인자가 전치사구의 일부를 형성한다면, argument label을 붙여라. argument label은. 보통 전치사로 시작해야 한다.

단 첫 두개의 인자가 하나의 추상화의 일부라면 다음과 같이 수정해야 한다.

 

a.move(toX: b, y: c)
a.fade(fromRed: b, green: c, blue: d)

// 수정 후
a.moveTo(x: b, y: c)
a.fadeFrom(red: b, green: c, blue: d)

 

 

  • 첫 인자가 문법적인 구문의 일부를 형성한다면 label을 생략하고, 기본 이름에 선행 단어를 추가하라.
    • ex. x.addSubview(y)
    • 즉 첫 번째 인자가 문법적인 구문 일부를 형성하지 않으면 레이블을 가져야함을 의미한다. 
  • 문법적인 것에 앞서 의미적인 것이 더 중요하다는 것을 상기하라
    • view.dismiss(false) : 문법적으로는 괜찮지만 의미론적으로 명확하지 않다.
  • 그 외 다른 모든 인자는 레이블을 지정하라

Special Instructions

  • 튜플 멤버와 클로저 매개변수에 label을 지정하라
    • 클로저 파라미터에 사용되는 이름들은 파라미터 이름을 짓는 방식과 동일하게 선택되어야 한다. 함수 내부에서 클로저 호출 시 일관되게 읽을 수 있다.
/// Ensure that we hold uniquely-referenced storage for at least
/// `requestedCapacity` elements
///
/// If more storage is needed, `allocate` is called with
/// bytes to allocate.
///
/// - Returns:
///   - reallocated: `true` iff a new block of memory
///     was allocated.
///   - capacityChanged: `true` iff `capacity` was updated.
mutating func ensureUniqueStorage(
	minimumCapacity requestedCapacity: Int,
	allocate: (_ byteCount: Int) -> UnsafePointer<Void>
) -> (reallocatted: Bool, capacityChanged: Bool)

 

  • 오버로드 셋에서 제약되지 않은 다형성에 주의를 더 기울여야 한다. ( ex. Any, AnyObject, 제약되지 않은 제네릭) 이는 모호성을 피하기 위함이다.
struct Array {
  /// Inserts `newElement` at `self.endIndex`.
  public mutating func append(_ newElement: Element)

  /// Inserts the contents of `newElements`, in order, at
  /// `self.endIndex`.
  public mutating func append(_ newElements: S)
    where S.Generator.Element == Element
}

 

위 코드를 보면 의미적으로 동일하지만 첫 인자의 타입이 명백히 다르다. Element가 Any인 경우, 하나의 Element는 Element의 시퀀스로서 같은 타입을 가질 수 있다.

var values: [Any] = [1, "a"]
// 모호하다
values.append([2, 3, 4]) // [1, "a", [2, 3, 4]] or [1, "a", 2, 3, 4]?

 

모호성을 제거하기 위해 아래와 같이 두 번째 오버로드에 label을 추가한다.

struct Array {
  /// Inserts `newElement` at `self.endIndex`.
  public mutating func append(_ newElement: Element)

  /// Inserts the contents of `newElements`, in order, at
  /// `self.endIndex`.
  public mutating func append(contentsOf newElements: S)
    where S.Generator.Element == Element
}