본문 바로가기
🍎 iOS/문서읽기

[iOS] 변수명, 어떻게 지을까? - API Design Guideline(3)

by @Eddy 2023. 3. 28.
728x90

Conventions

General Conventions ( 일반 규칙 )

1. 시간복잡도가 O(1)이 아닌 모든 연산프로퍼티(any computed property)의 복잡도를 문서화한다. 사람들은 종종 mental model로서 Property를 저장했기에, Property 접근에 연산이 중요하지 않다고 생각하는 경우가 있다. 이는 잘못된 생각이다.

Document the complexity of any computed property that is not O(1). People often assume that property access involves no significant computation, because they have stored properties as a mental model. Be sure to alert them when that assumption may be violated.

 

2. Free functions보다 method, property를 지향해라. Free function은 특별한 경우에만 사용된다.

Prefer methods and properties to free functions. Free functions are used only in special cases:

Free Function이 사용되는 경우

When there’s no obvious self:

min(x, y, z)


When the function is an unconstrained generic:

print(x)


When function syntax is part of the established domain notation:

sin(x)

 

3. 대소문자 convention을 따라라. Type과 Protocol만 UpperCamelCase를 사용하고, 그 외엔 lowerCamelCase를 사용한다.

Follow case conventions. Names of types and protocols are UpperCamelCase. Everything else is lowerCamelCase.

일반적으로 미국 영어에서 모두 대문자로 표시되는 약어와 이니셜은 대소문자 규칙에 따라 대문자 또는 소문자로 통일되어야 한다.
Acronyms and initialisms that commonly appear as all upper case in American English should be uniformly up- or down-cased according to case conventions:

var utf8Bytes: [UTF8.CodeUnit]
var isRepresentableAsASCII = true
var userSMTPServer: SecureSMTPServer


그 외 단어는 일반 단어로 취급되어야 한다.

Other acronyms should be treated as ordinary words:

var radarDetector: RadarScanner
var enjoysScubaDiving = true

 

4. Method는 동일한 기본 의미를 공유하거나 서로 다른 도메인에서 작동할 때, 기본 이름을 공유할 수 있다.

Methods can share a base name when they share the same basic meaning or when they operate in distinct domains.

예를 들어, 기본적으로 같은 동작을 할 때는 다음과 같은 방식을 권장한다.
For example, the following is encouraged, since the methods do essentially the same things:

/// - Note: 좋은 예

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 { ... }
}


And since geometric types and collections are separate domains, this is also fine in the same program:

/// - Note: 좋은 예

extension Collection where Element : Equatable {
  /// Returns `true` if `self` contains an element equal to
  /// `sought`; otherwise, `false`.
  func contains(_ sought: Element) -> Bool { ... }
}


하지만, 이러한 method처럼 다른 의미를 가질 때에는 다른 이름을 사용해야 한다.
However, these index methods have different semantics, and should have been named differently:

/// - Note: 나쁜 예

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 { ... }
}


마지막으로, 타입추론은 모호성을 야기할 수 있으므로, "Overloading on return type(반환 타입에 대한 Overloading)"은 지양해라.
Lastly, avoid “overloading on return type” because it causes ambiguities in the presence of type inference.

/// - Note: 나쁜 예

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)

1. 문서화를 제공할 Parameter 이름을 선택해라. 비록 Parameter 이름이 function이나 method의 사용 위치에 나타나지 않더라도 중요한 설명역할을 한다.

Choose parameter names to serve documentation. Even though parameter names do not appear at a function or method’s point of use, they play an important explanatory role.

읽기 쉽게 문서화하려면 이 이름들을 선택해라. 예를들어 predicate, subRange, newElements같은 이름이 문서를 자연스럽게 읽게 해준다.
Choose these names to make documentation easy to read. For example, these names make documentation read naturally:

/// - Note: 좋은 예

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


아래같은 경우는 문서를 어색하게 만들고, 문법적이지 않다
These, however, make the documentation awkward and ungrammatical:

/// - Note: 나쁜 예

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

 

2. 일반적인 사용을 쉽게 할 때, 기본 parameter를 제공해라. 일반적으로 사용되는 단일 값이 있는 모든 parameter는 기본값 후보이다.

Take advantage of defaulted parameters when it simplifies common uses. Any parameter with a single commonly-used value is a candidate for a default.

기본 argument들은 굳이 작성하지 않아도 되는 정보를 숨겨서 가독성을 향상시킨다.
Default arguments improve readability by hiding irrelevant information. For example:

/// - Note: 나쁜 예

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


더 단순하게 표현하면,
can become the much simpler:

/// - Note: 좋은 예

let order = lastName.compare(royalFamilyName)


일반적으로 기본 arguments들은 비슷한 method를 모아둔 것(Method Families)보다 선호되어지는데, 그 이유는 API를 이해하려는 사람들에게 인지적 부담을 줄여주기 때문이다.
Default arguments are generally preferable to the use of method families, because they impose a lower cognitive burden on anyone trying to understand the API.

/// - Note: 좋은 예

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


The above may not be simple, but it is much simpler than:

/// - Note: 나쁜 예 (Method Family)

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
}

 

Method Family의 모든 Method는 개별적으로 문서화되어야 하고, 사용자가 이해할 수 있어야 한다. 사용자가 Method를 사용하려면 모든 Method를 이해해야하고, 그렇지 않으면 사용자는 종종 예상치 못한 관계에서 사소한 차이를 알아내기 위해 고생하게 만들 수 있다. (ex) foo(bar: nil), foo()) 그래서 기본값이 있는 단일 method를 사용하는 것이 우수한 경험을 제공한다.
Every member of a method family needs to be separately documented and understood by users. To decide among them, a user needs to understand all of them, and occasional surprising relationships—for example, foo(bar: nil) and foo() aren’t always synonyms—make this a tedious process of ferreting out minor differences in mostly identical documentation. Using a single method with defaults provides a vastly superior programmer experience.

 

3. 기본값이 없는 Parameter를 Parameter 앞 순서에 두는 게 좋다. 기본값이 없다는 것은 일반적으로 method 의미에서 더 필요하다는 의미이며, method가 호출될 때 안정적인 사용 패턴을 제공하기 때문이다.

Prefer to locate parameters with defaults toward the end of the parameter list. Parameters without defaults are usually more essential to the semantics of a method, and provide a stable initial pattern of use where methods are invoked.

 

4. API가 프로덕션 환경에서 실행된다면, 다른 대안보다 #fileID가 좋다. #fileID는 공간을 절약하고 개발자의 개인정보를 보호한다. 전체 경로가 개발 워크플로(development workflows)를 단순화하거나 file I/O에 사용된다면, Test helper나 script같은 최종 사용자가 실행하지 않는 API에는 #filePath를 사용해라. Swift 5.2 이하 버전과의 소스 호환성을 유지하기 위해 #file을 사용해라.

If your API will run in production, prefer #fileID over alternatives. #fileID saves space and protects developers’ privacy. Use #filePath in APIs that are never run by end users (such as test helpers and scripts) if the full path will simplify development workflows or be used for file I/O. Use #file to preserve source compatibility with Swift 5.2 or earlier.

 

 

Argument Labels

func move(from start: Point, to end: Point)
x.move(from: x, to: y)

1. Argument들을 유의미하게 구분지을 수 없으면, 모든 Label을 생략해라.

ex) min(number1, number2), zip(sequence1, sequence2).

Omit all labels when arguments can’t be usefully distinguished, e.g. min(number1, number2), zip(sequence1, sequence2).

 

2. Value preserving type conversion(값 보존 타입 변환)을 하는 Initializer에서는 첫번째 argument label을 생략해라.

ex) Int64(someUInt32)

In initializers that perform value preserving type conversions, omit the first argument label, e.g. Int64(someUInt32)

첫번째 argument는 항상 변환할 값을 전달한다.
The first argument should always be the source of the conversion.

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

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


하지만, "좁히는" 타입 변환은 좁히는 것을 설명하는 label이 권장된다.
In “narrowing” type conversions, though, a label that describes the narrowing is recommended.

extension UInt32 {
  /// Creates an instance 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)
}


Value preserving type conversion은 source값의 차이가 결과 값의 차이를 초래하는 단형성이다. 
예를 들어, Int8 -> Int64로의 변환은 값을 보존하지만, Int64이 Int8보다 표현할 수 있는 값이 더 많아 Int64 -> Int8로의 변환은 보존이 불가능하다 

참고: 기존 값을 검색할수 있는 기능은 변환이 값을 보존하는 것인지와는 관련이 없다.
A value preserving type conversion is a monomorphism, i.e. every difference in the value of the source results in a difference in the value of the result. For example, conversion from Int8 to Int64 is value preserving because every distinct Int8 value is converted to a distinct Int64 value. Conversion in the other direction, however, cannot be value preserving: Int64 has more possible values than can be represented in an Int8.

Note: the ability to retrieve the original value has no bearing on whether a conversion is value preserving.

3. 첫번째 argument가 전치사 구의 일부를 형성할 땐, argument label을 지정해라. 일반적으로 Argument label은 전치사에서 시작해야 한다.

When the first argument forms part of a prepositional phrase, give it an argument label. The argument label should normally begin at the preposition, e.g. x.removeBoxes(havingLength: 12).

처음에 두 argument가 단일 추상화의 일부를 나타나면 예외가 발생한다.
An exception arises when the first two arguments represent parts of a single abstraction.

/// - Note: 나쁜 예

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


이런 경우에는 추상화를 명확하게 하기 위해 전치사(-to) 뒤에 argument label을 입력해라.
In such cases, begin the argument label after the preposition, to keep the abstraction clear.

/// - Note: 좋은 예

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

 

4. 첫번째 argument가 문법적 구문의 일부를 형성한다면, 기본이름 앞에 단어를 추가해서 그 argument의 label을 생략해라

Otherwise, if the first argument forms part of a grammatical phrase, omit its label, appending any preceding words to the base name, e.g. x.addSubview(y)

이는 곧, 첫번째 Argument가 문법적 구문을 형성하지 않으면, label이 있어야 한다는 걸 의미한다.
This guideline implies that if the first argument doesn’t form part of a grammatical phrase, it should have a label.

/// - Note: 좋은 예

view.dismiss(animated: false)
let text = words.split(maxSplits: 12)
let studentsByName = students.sorted(isOrderedBefore: Student.namePrecedes)


문구가 정확한 의미를 전달하는지가 중요하다. 아래는 문법적으로 이상없지만, 잘못된 의미를 전달할 수 있으니 주의해라.
Note that it’s important that the phrase convey the correct meaning. The following would be grammatical but would express the wrong thing.

/// - Note: 나쁜 예

view.dismiss(false)   Don't dismiss? Dismiss a Bool?
words.split(12)       Split the number 12?


기본 값을 가진 인수들은 생략될 수 있고, 이 경우에는 문법적 구문을 형성하지 않으니 항상 label이 있어야한다.
Note also that arguments with default values can be omitted, and in that case do not form part of a grammatical phrase, so they should always have labels.

 

5. 다른 모든 Arguments에 Label을 지정해라.

Label all other arguments.

 

 

Special Instructions

1. Tuple members와 Closure parameter가 API에서 표시되는 곳에서 이름을 지정해라.

Label tuple members and name closure parameters where they appear in your API.

이런 이름들이 쉽게 이해할 수 있게 만들고, 문서 주석으로 참고할 수 있으며, Tuple members에 표현식 접근을 제공한다.
These names have explanatory power, can be referenced from documentation comments, and provide expressive access to tuple members.

/// Ensure that we hold uniquely-referenced storage for at least
/// `requestedCapacity` elements.
///
/// If more storage is needed, `allocate` is called with
/// `byteCount` equal to the number of maximally-aligned
/// bytes to allocate.
///
/// - Returns:
///   - reallocated: `true` if a new block of memory
///     was allocated; otherwise, `false`.
///   - capacityChanged: `true` if `capacity` was updated;
///     otherwise, `false`.
mutating func ensureUniqueStorage(
  minimumCapacity requestedCapacity: Int,
  allocate: (_ byteCount: Int) -> UnsafePointer<Void>
) -> (reallocated: Bool, capacityChanged: Bool)


Closure parameters에서 사용되는 이름들은 최상위 함수에 대한 Parameter이름처럼 선택해야 한다. 호출 지점(the call site)에서 나타나는 Closure argument에 대한 Label들은 지원되지 않는다.
Names used for closure parameters should be chosen like parameter names for top-level functions. Labels for closure arguments that appear at the call site are not supported.

 

2. overload sets에서의 모호성을 피하기 위해 제한없는 다형성(Any, AnyObject, Generic)을 각별히 주의해라.

Take extra care with unconstrained polymorphism (e.g. AnyAnyObject, and unconstrained generic parameters) to avoid ambiguities in overload sets.

For example, consider this overload set:

/// - Note: 나쁜 예

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
}


이러한 Methods들은 의미적으로 통하고, Argument 타입들이 세세하게 구분되는 걸로 보인다. 하지만, Element가 Any타입일 때, 단일요소(a single element)는 요소들의 시퀀스(a sequence of elements)로서 동일한 타입을 가질 수 있는 문제가 있다.
These methods form a semantic family, and the argument types appear at first to be sharply distinct. However, when Element is Any, a single element can have the same type as a sequence of elements.

/// - Note: 나쁜 예

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


모호함을 없애기 위해 다른 overload의 이름을 더 명확하게 해라.
To eliminate the ambiguity, name the second overload more explicitly.

/// - Note: 좋은 예

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
}


새 이름이 문서 주석과 얼마나 더 잘 어울리는지 봐라. 이 경우엔 작성해둔 문서 주석이 실제 API작성자의 의도에 맞게 이름을 작성할 수 있게 해줬다.
Notice how the new name better matches the documentation comment. In this case, the act of writing the documentation comment actually brought the issue to the API author’s attention.

 

느낀 점

문서를 번역하는 건 기억에 남기기 좋은 방식이지만, 시간이 생각보다 많이 소요되어 좋은 공부방식이라고 보기 어렵다.

단지, 기초에 좀 더 충실할 수 있는 기회이고 증명이었다고 생각한다.

프로젝트를 할 때마다 변수명에 대한 고민이 많아 API Design Guideline을 읽은건데, 실제로 유의미하게 느낀 바가 있다.

(왜 영어 잘하는 사람을 선호하는지도 알 것 같다.)

적어도 이 문서에 한해서는 크게 어긋나지 않게 변수명을 짓고 있었던 것 같아 다행이고,

결국 이 문서는 나의 근거가 될거라 생각한다.

 

API Design Guideline 원문, 번역본

 

Swift.org

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

www.swift.org

 

API Design Guidelines

API Design Guidelines Table of Contents 기본 개념 가장 중요한 목표는 사용 시점에서의 명료성입니다. 메소드, 속성과 같은 개체들(Entities)은 한 번만 선언되지만, 반복적으로 사용됩니다. API는 이러한 개

minsone.github.io

 

반응형

댓글