Swift API Design Guidelines
이번 내용은 Swift API Design Guidelines에 대한 내용입니다.
Convention을 맞추어 코딩을 하는 것도 협업의 일부라고 생각하기 때문에 이번 기회에 정리해보고자 합니다.
티스토리가 리스트로 작성하는 데 생각대로 되지 않아서(그렇다고 HTML로 작성하기엔 귀,,찮),,
리스트의 계층을 ● → ○ → ■ 순서로 봐주시면 좋을 것 같습니다.
기본
● 사용에 대한 명확성(Clarity at the point of use)이 가장 중요한 목표입니다.
메소드와 속성과 같은 엔티티들은 한 번만 선언되지만 반복적으로 사용됩니다. 정확하고 명확하게 API를 디자인하십시오.
● 명확성이 간결성보다 더욱 중요합니다(Clarity is more important than brevity).
스위프트의 코드가 간결할 수는 있지만, 최대한 짧게 작성하는 것이 목표는 아닙니다.
● 모든 선언에 주석을 작성하세요(Write a documentation comment for every declaration).
주석을 통해 얻을 수 있는 인사이트는 당신의 코드 디자인에 매우 중요한 영향을 줄 수 있기 때문에 미루지 마십시오.
○ 스위프트의 마크다운 서식을 사용하십시오.
○ 선언된 엔티티를 설명할 수 있는 요약으로 시작하십시오. API는 선언과 그것의 요약을 통해 완전 이해될 수 있어야 합니다.
/// Returns a "view" of `self` containing the same elements in
/// reverse order.
func reversed() -> ReverseCollection
○ 조금 더 자세히 살펴보겠습니다.
■ 요약에 집중하십시오(Focus on the summary). 이것이 가장 중요한 부분입니다.
훌륭한 주석은 훌륭한 요약에 지나지 않습니다.
■ 가능하다면 마침표로 끝나는 단일 문장 조각을 사용하십시오(Use a single sentence fragment if possible).
완전한 문장을 사용하지 마십시오.
■ 함수나 메소드가 무엇을 하는지와 무엇을 반환하는지 작성하시오(Describe what a function or method does and what it returns).
/// 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 access).
/// 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)
■ 다른 선언문에선(서브스크립트나 이니셜라이저를 제외한 모든 선언문) 선언된 엔티티가 무엇인지 설명하십시오(describe what the declared entity 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?
...
○ 필요에 따라 하나 이상의 단락 및 글머리 기호 항목으로 계속합니다. 빈 줄로 단락을 구분하고 완전한 문장을 사용하십시오.
/// 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
명확한 사용을 추구
● 해당 코드를 읽는 사람들이 모호함을 피하기 위한 모든 단어를 포함합니다(Include all the words needed to avoid ambiguity).
예를 들어 컬렉션 내의 지정된 위치의 요소를 제거하는 메소드를 생각해보겠습니다.
extension List {
public mutating func remove(at position: Index) -> Element
}
employees.remove(at: x)
위 메소드 서명에서 at 단어를 생략한다면, x위치의 요소를 제거한다는 느낌보다 x와 같은 요소를 제거함을 암시할 수도 있습니다.
at을 통해 'x에 있는 값을 제거'한다는 것을 암시적으로 알 수 있습니다.
employees.remove(x) // unclear: are we removing x?
● 불필요한 단어는 생략하십시오(Omit needless words).
이름에 나타나는 모든 단어는 사용할 때 있어서 중요한 정보를 전달해야합니다.
의도나 의미를 명확하게 하기 위해 더 많은 단어들이 필요할 수도 있지만, 독자가 이미 알고 있는 정보에 대해서는 생략해야 합니다.
특히 타입 정보만 반복하는 단어는 생략하십시오.
public mutating func removeElement(_ member: Element) -> Element?
allViews.removeElement(cancelButton)
이 경우에는 Element라는 단어는 중요한 정보가 아니기 때문에 생략하는 것이 더욱 명확합니다.
○ 특별한 정보를 주는 것이 아니기 때문에 생략한다는 의미인 것 같습니다.
public mutating func remove(_ member: Element) -> Element?
allViews.remove(cancelButton) // clearer
때때로 모호성을 피하기 위해 타입 정보를 작성하는 것이 필요할 수도 있지만,
일반적으로 매개 변수의 타입보다는 매개 변수의 역할을 작성하는 것이 좋습니다.
● 타입 제약 조건보다는 역할에 따라 변수, 매개 변수 및 관련 타입으로 이름을 지정하시오(Name variables, parameters, and associated types according to their roles).
var string = "Hello"
protocol ViewController {
associatedtype ViewType : View
}
class ProductionLine {
func restock(from widgetFactory: WidgetFactory)
}
위의 방식으로는 명확성과 표현을 최적으로 표현할 수 없습니다.
그 대신, 엔티티의 역할을 나타내는 이름으로 표현하십시오.
var greeting = "Hello"
protocol ViewController {
associatedtype ContentView : View
}
class ProductionLine {
func restock(from supplier: WidgetFactory)
}
● 약한 타입 정보를 보완하여 매개 변수의 역할을 명확히 합니다(Compensate for weak type information to clarify a parameter’s role).
특히 매개변수 타입이 NSObject, Any, AnyObject나 Int나 String과 같은 기본 타입이라면,
사용 시점에서 타입 정보과와 맥락에 대한 의도를 충분히 전달하지 못할 수도 있습니다.
해당 예시에선 선언은 명확할 수 있지만, 사용 시점에서 모호할 수 있습니다.
func add(_ observer: NSObject, for keyPath: String)
grid.add(self, for: graphics) // vague
명확하게 하기 위해, 약한 타입의 매개변수 앞에 그 역할을 설명하는 명사를 추가합니다.
func addObserver(_ observer: NSObject, forKeyPath path: String)
grid.addObserver(self, forKeyPath: graphics) // clear
Strive for Fluent Usage
● 사용 시 문법적인 영어 구를 형성하도록 하는 메소드나 함수 이름을 선호하십시오(Prefer method and function names that make use sites form grammatical English phrases).
//좋은 예시
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”
//나쁜 예시
x.insert(y, position: z)
x.subViews(color: y)
x.nounCapitalize()
● 팩토리 메소드의 이름은 “make”로 시작합니다.(ex. x.makeIterator())
● 부작용에 따라 메소드나 함수의 이름을 지정해야합니다(Name functions and methods according to their side-effects.).
○ 부작용이 없는 것들은 명사구로 읽어야 합니다.(ex. x.distance(to: y), i.successor())
○ 부작용이 있는 것들은 명령형 동사구로 읽어야 합니다.(ex. print(x), x.sort(), x.append(y))
○ Mutating/nonmutating 메소드 짝의 이름을 일관되게 지어야합니다(Name Mutating/nonmutating method pairs).
mutating 메소드는 nonmutating 변수와 비슷한 의미를 갖지만, 새로운 값을 반환합니다.
■ 만약 연산이 동사에 의해 자연스럽게 설명되는 경우, mutating 메소드에 명령형 동사를 사용하고, nonmutating 부분에 “ed”나 “ing” 접미사를 적용합니다.
Mutating | Nonmutating |
x.sort() | z = x.sorted() |
x.append(y) | z = x.appending(y) |
■ 동사의 과거분사(보통 “ed”)를 사용하여 nonmutating 변수의 이름을 선언합니다.
/// Reverses `self` in-place.
mutating func reverse()
/// Returns a reversed copy of `self`.
func reversed() -> Self
...
x.reverse()
let y = x.reversed()
■ 동사에 목적어가 있어 “ed”가 문법적으로 올바르지 않을 경우, 동사의 현재 분사인 “ing”를 사용하여 nonmutating 변수의 이름을 선언합니다.
/// Strips all the newlines from `self`
mutating func stripNewlines()
/// Returns a copy of `self` with all the newlines stripped.
func strippingNewlines() -> String
...
s.stripNewlines()
let oneLine = t.strippingNewlines()
■ 연산이 명사로 설명되는 경우, nonmutating 메소드에는 명사를 사용하고, mutating 메소드에는 “form” 접두사를 적용하여 mutating 메소드의 이름을 선언합니다.
Nonmutating | Mutating |
x = y.union(z) | y.formUnion(z) |
j = c.successor(i) | c.formSuccesor(&i) |
● Boolean 메소드 및 속성의 사용은 nonmutating일 때 수신자에 대한 주장으로 읽혀야 합니다(Uses of Boolean methods and properties should read as assertions about the receiver). (ex. x.isEmpty, line1.intersects(line2))
● 무엇인지에 대해 설명하는 프로토콜은 명사로 읽어야 합니다(Protocols that describe what something is should read as nouns). (ex. Collection)
● 기능을 설명하는 프로토콜은 접미사 able, ible, ing를 사용하여 읽어야 합니다(Protocols that describe a capability should be named using the suffixes able, ible, or ing). (ex. Equatable, ProgressReporting)
● 다른 타입, 속성, 변수, 상수들은 명사로 읽어야 합니다(types, properties, variables, and constants should read as nouns).
Conventions
General Conventions
● O(1)이 아닌 복잡성은 문서화합니다(Document the complexity of any computed property that is not O(1)).
● 전역 함수(Swift에서는 전역 함수를 free function이라고 합니다.)보다 메소드나 속성을 선호합니다.
전역 함수는 특별한 경우에만 사용됩니다.
1. 명백한 self가 없을 경우
min(x, y, z)
2. 함수가 제한되지 않은 제네릭인 경우
print(x)
3. 함수 구문이 설정된 도메인 표기법의 일부인 경우
sin(x)
● Case conventions을 따르시오(Follow case conventions).
타입과 프로토콜의 이름은 UpperCamelCase를 따르고, 다른 것들은 lowerCamelCase입니다.
● 메소드는 같은 기본 의미를 갖거나 별개의 도메인에서 연산될 때 기본 이름을 공유할 수 있습니다(Methods can share a base name).
예를 들어, 메소드가 기본적으로 동일한 작업을 수행하기 때문에 밑의 예시와 같은 선언이 권장될 수 있습니다.
extension Shape {
/// Returns `true` if `other` is within the area of `self`;
/// otherwise, `false`.
func contains(_ other: Point) -> Bool { ... }
/// Returns `true` if `other` is entirely within the area of `self`;
/// otherwise, `false`.
func contains(_ other: Shape) -> Bool { ... }
/// Returns `true` if `other` is within the area of `self`;
/// otherwise, `false`.
func contains(_ other: LineSegment) -> Bool { ... }
}
하지만 밑의 index 메소드는 의미가 다르기 때문에 이름을 다르게 지어주어야 합니다.
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 { ... }
}
Parameters
func move(from start: Point, to end: Point)
● 문서의 역할을 할 수 있는 배개변수의 이름을 선택하십시오(Choose parameter names to serve documentation).
매개변수의 이름은 사용 시점에 나타나는 것은 아니지만, 식을 설명하는 데에 중요한 역할을 합니다.
문서를 쉽게 읽을 수 있는 이름을 선택하십시오.
예를 들어 다음과 같은 이름은 문서를 자연스럽게 읽도록 해줍니다.
/// 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])
하지만 다음은 문서를 어색하게 만들고 문법적으로 옳지 않게 만듭니다.
/// 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])
● 일반적인 사용을 단순화할 때 기본 매개변수를 사용하십시오(Take advantage of defaulted parameters).
기본 매개변수를 사용하면 가독성을 높이고 훨씬 간단해질 수 있습니다.
let order = lastName.compare(
royalFamilyName, options: [], range: nil, locale: nil)
let order = lastName.compare(royalFamilyName)
요약
사실 내용이 더욱 존재하지만, 우선 이정도의 내용만이라도 숙지하고 적용할 수 있다면 (우선은) 컨벤션과 API Design에 관련해서는 괜찮을 거 같아서 정리를 마무리하려고 합니다.
요약하자면 다음이 가장 중요한 것 같습니다.
- 간결한 것도 중요하지만, 명확한 것이 더욱 중요하다!
- 모호함을 피하기 위한 모든 단어를 추가해도 된다!
- 하지만 불필요한 단어는 생략해라!
- 선언을 한 후 주석을 통해 요약하여 설명하라!
- 주석을 통해 코드 디자인을 다른 사람에게 이해시킬 수 있어야 한다.
'Swift' 카테고리의 다른 글
[Swift] 클래스와 프로토콜에 대한 고찰 및 사용 기준 (0) | 2025.01.23 |
---|---|
[Swift] 스위프트 - Character를 Int로 변환 (wholeNumberValue) (0) | 2024.01.01 |
[Swift] 스위프트 - 배열에 있는 문자와 문자열 간단하게 출력하기 (String, joined, sorted) (0) | 2024.01.01 |
[Swift] 스위프트에서의 ,와 &&의 차이점 (2) | 2023.12.28 |
[Swift] Swift의 메모리 구조 (0) | 2023.08.18 |