Swift API Design Guidelines - Conventions

고라니·2023년 10월 16일
0

TIL

목록 보기
42/67

Conventions

General Conventions(일반적인 규칙)

O(1)이 아닌 연산 프로퍼티는 복잡정을 작성한다.

개발자들은 보통 프로퍼티에 접근하는 것에 엄청난 연산이 필요하다고 생각하지 않는다.

특별한 경우가 아니라면 전역함수보다는 모듈에 속하는 메서드 사용을 권장

전역함수는 특별한 경우에만 사용된다. 아래의 코드 예시를 보자

// 특정 객체에 대한 동작이 아닐 때
min(x, y, z)

// 함수가 제네릭이며 특정 타입에 제약을 받지 않을 때
print(x)

// 함수의 문법이 이미 잘 알려진 도메인 표기법의 일부일 때
sin(x)

타입과 프로토콜 이름은 UpperCamelCase, 나머지는 lowerCamleCase

ok

메서드가 동일한 기본 의미를 가지거나 서로 다른 도메인에서 동작할 때 같은 기본 이름 사용 가능

이는 메서드의 네이밍에 있어서 일관성과 의미를 유지하게 도와준다. 같은 이름을 사용하는 예시와, 잘못된 예시를 알아보자
1. 메서드가 본질적으로 같은 일을 하고 있기 때문에 같은 이름 사용 권장

extension Shape {
      /// Returns `true` iff `other` is within the area of `self`.
      /// `other`가 `self`의 영역 내에 있는 경우 `true`를 반환합니다.
      func contains(_ other: Point) -> Bool { ... }

      /// Returns `true` iff `other` is entirely within the area of `self`.
      /// `other`가 완전히 `self`의 영역 내에 있는 경우 `true`를 반환합니다.
      func contains(_ other: Shape) -> Bool { ... }

      /// Returns `true` iff `other` is within the area of `self`.
      /// `other`가 `self`의 영역 내에 있는 경우 `true`를 반환합니다.
      func contains(_ other: LineSegment) -> Bool { ... }
  }
  1. 기하학적 타입과 컬렉션은 서로 다른 도메인이기 때문에 동일한 메서드 이름 사용해도 괜찮다. (즉, 서로 다른 도메이인이기 때문에 같은 메서드 이름 사용 가능)
extension Collection where Element : Equatable {
      /// Returns `true` iff `self` contains an element equal to
      /// `sought`.
      /// `self`에 `sought`와 동일한 요소가 포함된 경우 `true`를 반환합니다.
      func contains(_ sought: Element) -> Bool { ... }
  }
  1. 메서드들이 다른 의미를 가지고 있다면 다른 이름을 지어야 한다.
extension Database {
      /// Rebuilds the database's search index
      /// 데이터베이스의 검색 인덱스 리빌드
      func index() { ... }

      /// Returns the `n`th row in the given table.
      /// 주어진 테이블의 `n`번째 행을 반환합니다.
      func index(_ n: Int, inTable: TableID) -> TableRow { ... }
  }
  1. 반환타입만 재정의 하는 방식은 타입 추론 시 모호해질 수 있으므로 피한다.
extension Box {
      /// Returns the `Int` stored in `self`, if any, and
      /// `nil` otherwise.
      /// `self`에 저장된 `Int`가, 만약에 있으면, 반환하고
      /// 그렇지 않으면 `nil`을 반환합니다.
      func value() -> Int? { ... }

      /// Returns the `String` stored in `self`, if any, and
      /// `nil` otherwise.
      /// `self`에 저장된 `String`이, 만약에 있으면, 반환하고
      /// 그렇지 않으면 `nil`을 반환합니다.
      func value() -> String? { ... }
  }

위의 코드의 경우 Int를 반환하고 싶었는데 모르고 아래의 String반환 메서드를 사용할 가능성이 있을 듯

Parameters(매개변수)

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

함수나 메서드에서 매개변수의 이름은 해당 함수의 설명(문서화)에 중요한 역할을 한다.

매개변수의 이름은 함수의 역할과 기능을 명확하게 표현하는 데 중요한 역할을 한다.

  • 올바른 코드1
/// 'predicate'를 만족시키는 'self'의 요소를 포함하는 'Array'를 반환
func filter(_ predicate: (Element) -> Bool) -> [Generator.Element]

이 함수의 매개변수 이름은 해당 함수의 역할을 잘 설명하고 있음

  • 부적절한 코드1
/// 'includedInResult'를 만족시키는 'self'의 요소를 포함하는 'Array'를 반환
func filter(_ includedInResult: (Element) -> Bool) -> [Generator.Element]

includedInResult라는 매개변수 이름은 함수의 조건을 나타내지 않고 결과에 포함되는 요소를 나타내므로, 설명이 어색함

  • 올바른 코드2
/// 주어진 요소의 'subRange'를 'newElements'로 변환
  mutating func replaceRange(_ subRange: Range, with newElements: [E])

매개 변수 이름들이 함수의 기능을 잘 설명하고 있음

  • 부적절한 코드2
/// 'r'로 표시된 요소의 범위를 'with'의 내용으로 변환
  mutating func replaceRange(_ r: Range, with: [E])

subRange 대신 r이라는 짧은 이름, newElements 대신 with라는 이름을 사용하여 어색하고 불분명하게 보임

기본 매개변수 활용

기본 매개변수 값은 사용사례를 간단하게 만들고, 가독성을 향상시킨다.

let order = lastName.compare(
    rotalFamilyName, option: []m range: nil, locale: nil)let order = lastName.compare(royalFamilyname)

기본 매개변수 값을 사용하는 것이 같은 기능을 하는 여러 메서드를 만드는 것보다 권장한다. 사용자에게 더 낮은 인지 부하를 주기 때문

즉 기본 매개변수를 사용한 단일 메서드는 프로그래머에게 훨씬 우수한 경험을 제공한다.

기본값이 있는 매개변수는 매개변수 목록의 끝쪽에 위치시키는 것을 선호

기본 값이 없는 매개면수는 일반적으로 메서드의 의미가 더 필수. 또한 안정적인 초기 패턴을 제공

API가 프로덕션에서 실행될 경우, #fileID같은 리터럴들을 적극 사용

프로덕션은 실제 사용자가 사용하는 환경으로 개발환경, 테스트환경과 구분된다.
Swift는 #fileID, #filePath, #file 같은 리터럴을 제공한다.
이러한 리터럴을 프로덕션에서 사용하면 공간 절약, 개인 정보 보호의 장점이 있다.

Argument Labels(전달인자 레이블)

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

함수에서 첫 번째 인자와 두 번째 인자가 구별되는 경우 이를 명확하게 표시하기 위해 from과 to 같은 전달인자 레이블을 사용

전달인자 레이블이 전달인자를 유용하게 구분하지 못하는 경우 모든 전달인자 레이블이 무시.

ex) min(number1, number2), zip(sequance1, sequence2)

값을 보존하고 타입만 변경하는 이니셜라이저는 첫 번째 전달인자의 라벨은 생략한다.

ex) Int64(someUInt32)

타입 변환의 대상은 반드시 첫 번째 전달인자여야 한다.

extension String {
	// 'x'를 주어진 radix(진법)의 텍스트 형태로 변환한다.
    init(_ x: BigInt, redix: Int = 10)
}
text = "The value is: "
text += String(veryLargeNumber)
text += " and in hexadecimal, it's"
text += String(veryLargeNumber, radix: 16)

하지만 범위가 좁아지는 타입 변환에서는, 변환의 특성을 설명하는 라벨 사용이 권장된다.

범위가 좁아지는 타입 변환?
: 큰 데이터 타입에서 작은 데이터 타입으로의 변환을 의미
ex) Int64 -> Int8, Double -> Int

extension UInt32 {
  /// 지정한 `value` 값을 가진 인스턴스를 생성합니다.
  init(_ value: Int16)
  /// `source`의 최하위 32비트 값을 가진 인스턴스를 생성합니다.
  init(truncating source: UInt64)
  /// `valueToApproximate` 값에 가장 근접한 값을 가진 인스턴스를 생성합니다.
  init(saturating valueToApproximate: UInt64)
}

값을 보존하면서 타입을 바꾸는 것을 단형성(monomorphism)이라고 한다. 타입변환 대상의 값이 다르면 결과도 다르다, Int8 -> Int64로 타입을 바꾸는 경우 값이 보존되지만 반대의 경우 값이 보존되지 않는다.

뭔가 이부분은 잘 이해가 되지 않았는데 추가로 이해한 내용을 설명 해보자면 범위가 좁아지는 타입변환에서는 원본 타입의 모든 값이 변환되지 않을 가능성이 있고, 이런경우 변환 결과가 어떻게 될지 명확히 표현하는것이 좋기 때문에 레이블을 작성하는것을 권장하고 있는것이다.

함수의 첫 번째 전달 인자가 전치사구의 일부로 표현될 때, 해당 인자에 레이블을 붙이는 것이 좋다.

이때 라벨은 전치사에서 시작하는 것이 원칫이다.

만약 첫 번째와 두 번째 인자가 하나의 통합된 개념의 일부로 볼 수 있다면, 위에서의 원칙이 적용되지 않는다.

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)

하지만 첫 번째 인자가 문법적인 문구의 일부로 구성 될 때 그 인자와 라벨을 생략

ex) add라는 동사와, Subview라는 명사가 문법적으로 문구형성이 되기 때문에 "add(subview: a)" 보다는 "addSubview(a)" 적절

중요한 점은, 해당 문구가 올바른 의미를 전달해야 한다는 것. 잘못된 라벨이나 생략된 라벨로 인해 문장이 문법적으로 올바르더라도 잘못된 의미를 전달할 수 있다.

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

❌
view.dismiss(false)   // 해제하지 말아라? Bool을 해제해라?
words.split(12)       // 숫자 12를 나눠라?

위에서 설명한 경우를 제외하고는 전달 인자 레이블을 작성해야 한다.


참고: Swift API Guidelines, yagom-academy

profile
🍎 무럭무럭

0개의 댓글