https://www.swift.org/documentation/api-design-guidelines/
최고의 컨벤션
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번 코드가 더 좋다.
- 파라미터 타입이 기본 타입(NSObject, Any, AnyObject, 기본 자료형 .. )인 경우 타입 정보와 파라미터의 의미를 명확히 전달하지 못할 위험이 있다.
// 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)
- side-effect가 없는 경우 : 명사구로 읽어야 한다.
- 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) 보다 메서드와 프로퍼티를 사용하라
- 전역함수는 다음과 같은 상황에서만 사용해야 한다.
- 명확한 self가 없을 경우
- ex. min(x,y,z)
- 함수가 제약되지 않은 제네릭인 경우
- ex. print(x)
- 햠수의 용어가 도메인 표기의 일부로 설정된 경우
- ex. sin(x)
- 명확한 self가 없을 경우
- 전역함수는 다음과 같은 상황에서만 사용해야 한다.
- 이름 짓기 규칙
- 타입, 프로토콜 : 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
}
'iOS > Swift' 카테고리의 다른 글
Swift 공식문서 4. Collection Types (0) | 2024.09.08 |
---|---|
Swift 공식 문서 3. Strings and Characters (2) (1) | 2024.08.31 |
Swift 공식 문서 3. Strings and Characters (1) (0) | 2024.08.29 |
Swift 공식 문서 2. Basic Operator (0) | 2024.08.28 |
Swift 공식 문서 1. The Basics (0) | 2024.08.28 |