이 스타일 가이드를 준수함으로써
- 보다 쉽게 읽고 익숙하지 않은 코드를 이해할 수 있도록 한다.
- 코드를 보다 쉽게 유지 관리
- 간단한 프로그래머 오류 감소
- 코딩 시 인지 부하 감소
간결함이 주된 목표가 아니라는 점에 유의한다.
코드는 다른 좋은 코드 품질(가독성, 단순성, 명확성 등)이 동일하게 유지되거나 개선될 경우에만 보다 간결하게 만들어야 한다.
- 본 가이드에 없는 가이드라인은 아래를 따른다.
- 모든 규칙을 구체화하기 위해 노력한다.
- 예외사항은 거의 두지 않아야 하고, 있더라도 정당성이 높아야한다.
master
브랜치에서feature
브랜치를 만들어 가이드를 수정하고,master
브랜치로 PR을 보내 리뷰를 통과하면 반영시킨다.
Tip: 일부 코드 또는 모두 선택(Command-A)한 다음 Control-I(또는 편집기 ▸ Structure ▸ Re-Indent)를 선택하여 들여쓰기를 다시 할 수 있다.
예를 들어 ‘// MARK: …’ 또는 ‘// MARK: - …’ 형식을 사용한다.
좋은 예:
// MARK: Layout
override func layoutSubviews() {
// doSomething()
}
// MARK: Actions
override func menuButtonDidTap() {
// doSomething()
}
좋은 예:
var something: Double = 0
var dict = [KeyType: ValueType]()
class MyClass: SuperClass {
// ...
}
나쁜 예:
var something : Double = 0
var dict = [KeyType:ValueType]()
var dict = [KeyType : ValueType]()
class MyClass : SuperClass {
// ...
}
좋은 예:
func doSomething() -> String {
// ...
}
func doSomething(completion: () -> Void) {
// ...
}
나쁜 예:
func doSomething()->String {
// ...
}
func doSomething(completion: ()->Void) {
// ...
}
주석처리 지시어를 제외한 TODO:
, FIXME:
의 형식을 준수하여 작성한다.
단, #warning()
키워드 안에 작성하여 강제로 Warning을 발생시킨다.
TODO와 FIXME를 추적하고 관리하여야 하기 때문이다.
좋은 예:
func emptyFunction() {
#warning("TODO: 추가 로직 작성 예정입니다")
}
#warning("FIXME: 파라미터 명 교체 예정입니다")
func doSomething(complexParameterName: String) {
// ...
}
나쁜 예:
func emptyFunction() {
// TODO: 추가 로직 작성 예정입니다
}
// 파라미터 명 교체 예정입니다
func doSomething(complexParameterName: String) {
// ...
}
좋은 예:
if user.isHappy {
// Do something
} else {
// Do something else
}
guard let urlString = urlString, let url = URL(string: urlString) else {
completion(nil, nil)
return
}
나쁜 예:
if user.isHappy
{
// Do something
}
else {
// Do something else
}
예외: return
, return nil
처럼 간단하게 guard
로 탈출하는 경우 한 줄로 작성한다. 단, { } 안쪽에 공백을 준다.
좋은 예:
guard let self = self else { return false }
나쁜 예:
guard let self = self else {return false}
guard let self = self else {
return false
}
좋은 예:
func reticulateSplines(
spline: [Double],
adjustmentFactor: Double,
translateConstant: Int,
comment: String
) -> Bool {
// reticulate code goes here
}
let actionSheet = UIActionSheet(
title: "정말 계정을 삭제하실 건가요?",
delegate: self,
cancelButtonTitle: "취소",
destructiveButtonTitle: "삭제해주세요"
)
좋은 예:
UIView.animate(
withDuration: 0.25,
animations: {
// doSomething()
},
completion: { finished in
// doSomething()
}
)
좋은 예:
let evenSquares = numbers.filter { $0 % 2 == 0 }.map { $0 * $0 }
나쁜 예:
let evenSquares = numbers.filter {$0 % 2 == 0}.map { $0 * $0 }
이 규칙은 범위 연산자에는 적용되지 않는다(예: 1...3
) 및 postfix 또는 접두사 연산자 (예: guest?
, -1
).
많은 연산자가 있는 문장을 시각적으로 그룹화하기 위해 공백의 폭을 다양하게 하기 보다는 괄호를 선호한다.
좋은 예:
let capacity = 1 + 2
let capacity = currentCapacity ?? 0
let mask = (UIAccessibilityTraitButton | UIAccessibilityTraitSelected)
let capacity = newCapacity
let latitude = region.center.latitude - (region.span.latitudeDelta / 2.0)
나쁜 예:
let capacity = 1+2
let capacity = currentCapacity ?? 0
let mask = (UIAccessibilityTraitButton|UIAccessibilityTraitSelected)
let capacity=newCapacity
let latitude = region.center.latitude - region.span.latitudeDelta/2.0
Tip: 이 스크립트를 실행하여 Xcode에서 일부 설정을 활성화할 수 있다. 예: "Run Script" build phase의 일부가 되게 하면된다.
명명은 기본적으로 Swift API Design Guidelines의 가이드를 따른다. 위 문서의 내용을 여기에 정리할 수 도 있고, 새로운 가이드라인을 추가할 수도 있다.
좋은 예:
protocol SpaceThing {
// ...
}
class SpaceFleet: SpaceThing {
enum Formation {
// ...
}
class Spaceship {
// ...
}
var ships: [Spaceship] = []
static let worldName: String = "Earth"
func addShip(_ ship: Spaceship) {
// ...
}
}
let myFleet = SpaceFleet()
예외: 동일한 이름의 속성이나 메서드가 더 높은 액세스 수준을 가진 경우, private
속성에 밑줄 접두사를 붙일 수 있다.
속성이나 메서드를 backing하는 것이 설명적인 이름을 사용하는 것 보다 더 읽기 쉬울 수 있다. 예를 들어
좋은 예:
- Type erasure
public final class AnyRequester<ModelType>: Requester {
public init<T: Requester>(_ requester: T) where T.ModelType == ModelType {
_executeRequest = requester.executeRequest
}
@discardableResult
public func executeRequest(
_ request: URLRequest,
onSuccess: @escaping (ModelType, Bool) -> Void,
onFailure: @escaping (Error) -> Void) -> URLSessionCancellable
{
return _executeRequest(request, session, parser, onSuccess, onFailure)
}
private let _executeRequest: (
URLRequest,
@escaping (ModelType, Bool) -> Void,
@escaping (NSError) -> Void) -> URLSessionCancellable
}
- 더 구체적인 타입을 사용하여 덜 구체적인 타입을 backing한다.
final class ExperiencesViewController: UIViewController {
// We can't name this view since UIViewController has a view: UIView property.
private lazy var _view = CustomView()
loadView() {
self.view = _view
}
}
이것은 그들이 다른 타입이 아닌 boolean이라는 것을 분명히 한다.
좋은 예:
class URLValidator {
func isValidURL(_ url: URL) -> Bool {
// ...
}
func isProfileURL(_ url: URL, for userID: String) -> Bool {
// ...
}
}
let urlValidator = URLValidator()
let isProfile = urlValidator.isProfileURL(urlToTest, userID: idOfUser)
나쁜 예:
class UrlValidator {
func isValidUrl(_ URL: URL) -> Bool {
// ...
}
func isProfileUrl(_ URL: URL, for userId: String) -> Bool {
// ...
}
}
let URLValidator = UrlValidator()
let isProfile = URLValidator.isProfileUrl(URLToTest, userId: IDOfUser)
"가장 일반적인"의 의미는 상황에 따라 다르지만 대략적으로 "찾는 항목에 대한 검색 범위를 좁히는 데 가장 도움이 되는" 것을 의미해야 한다. 가장 중요한 것은 일관성을 지키는 것이다.
좋은 예:
let titleMarginRight: CGFloat
let titleMarginLeft: CGFloat
let bodyMarginRight: CGFloat
let bodyMarginLeft: CGFloat
나쁜 예:
let rightTitleMargin: CGFloat
let leftTitleMargin: CGFloat
let bodyRightMargin: CGFloat
let bodyLeftMargin: CGFloat
좋은 예:
let titleText: String
let cancelButton: UIButton
나쁜 예:
let title: String
let cancel: UIButton
주어가 명확하다면 생략할 수 있다.
좋은 예:
class ExperiencesViewController {
private func didTapBookButton() {
// ...
}
private func modelDidChange() {
// ...
}
}
나쁜 예:
class ExperiencesViewController {
private func handleBookButtonTap() {
// ...
}
private func modelChanged() {
// ...
}
}
이름간 충돌을 피하기 위한 방식이었는데 Swift에서 더 이상 필요하지 않다. 스위프트의 타입은 해당 타입을 포함하는 모듈에 의해 자동으로 네임스페이스가 지정되며, NS
와 같은 클래스 접두사를 추가해서는 안 된다. 서로 다른 모듈의 두 이름이 충돌하는 경우 유형 이름과 모듈 이름을 접두사로 연결하여 모호한 정보를 해제할 수 있다. 단, 혼동 가능성이 있을 때만 모듈 이름을 지정한다.
좋은 예:
class Account {
// ...
}
나쁜 예:
class AIRAccount {
// ...
}
좋은 예:
func namePickerView(_ namePickerView: NamePickerView, didSelectName name: String)
func namePickerViewShouldReload(_ namePickerView: NamePickerView) -> Bool
나쁜 예:
func didSelectName(namePicker: NamePickerViewController, name: String)
func namePickerShouldReload() -> Bool
좋은 예:
func name(for user: User) -> String?
나쁜 예:
func getName(for user: User) -> String?
타입 이름을 줄이지 않는다. UICollectionViewCell
, UITableViewCell
의 경우는 너무 길기 때문에 Cell
만 활용한다.
어떤 기능의 뷰인지 바로 알 수 있어서 가독성이 좋아진다.
좋은 예:
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var moreButton: UIButton!
@IBOutlet weak var collectionView: UICollectionView!
@IBOutlet weak var bannerCell: UITableViewCell!
나쁜 예:
@IBOutlet weak var title: UILabel!
@IBOutlet weak var moreBtn: UIButton!
@IBOutlet weak var collection: UICollectionView!
@IBOutlet weak var bannerTableViewCell: UITableViewCell!
좋은 예:
let host = Host()
let selector = #selector(viewDidLoad)
view.backgroundColor = .red
let toView = context.view(forKey: .to)
let view = UIView(frame: .zero)
나쁜 예:
let host: Host = Host()
let selector = #selector(ViewController.viewDidLoad)
view.backgroundColor = UIColor.red
let toView = context.view(forKey: UITransitionContextViewKey.to)
let view = UIView(frame: CGRect.zero)
좋은 예:
final class Listing {
init(capacity: Int, allowsPets: Bool) {
self.capacity = capacity
isFamilyFriendly = !allowsPets
}
private let isFamilyFriendly: Bool
private var capacity: Int
private func increaseCapacity(by amount: Int) {
capacity += amount
save()
}
}
나쁜 예:
final class Listing {
init(capacity: Int, allowsPets: Bool) {
self.capacity = capacity
self.isFamilyFriendly = !allowsPets // `self.` not required here
}
private let isFamilyFriendly: Bool
private var capacity: Int
private func increaseCapacity(by amount: Int) {
self.capacity += amount
self.save()
}
}
좋은 예:
class MyClass {
func request(completion: () -> Void) {
API.request() { [weak self] response in
guard let self = self else { return }
// Do work
completion()
}
}
}
나쁜 예:
class MyClass {
func request(completion: () -> Void) {
API.request() { [weak self] response in
guard let strongSelf = self else { return }
// Do work
completion()
}
}
}
경험상 3개 이상의 필드가 있다면, 아마 struct를 사용해야 할 것이다.
좋은 예:
func whatever() -> (x: Int, y: Int) {
return (x: 4, y: 4)
}
let coord = whatever()
coord.x
coord.y
나쁜 예:
func whatever() -> (Int, Int) {
return (4, 4)
}
let thing = whatever()
print(thing.0)
좋은 예:
if userCount > 0 { ... }
switch someValue { ... }
let evens = userCounts.filter { number in number % 2 == 0 }
let squares = userCounts.map { $0 * $0 }
나쁜 예:
if (userCount > 0) { ... }
switch (someValue) { ... }
let evens = userCounts.filter { (number) in number % 2 == 0 }
let squares = userCounts.map() { $0 * $0 }
좋은 예:
let range = NSRange(location: 10, length: 5)
나쁜 예:
let range = NSMakeRange(10, 5)
컴파일 시간에 영향을 주는 주요 원인이므로 지양한다.
좋은 예:
let firstName = "김"
let secondName = "티드"
let wholeName = "\(firstName)\(secondName)"
나쁜 예:
let firstName = "김"
let secondName = "티드"
let wholeName = firstName+secondName
가독성이 향상된다.
좋은 예:
let completionBlock: (() -> Void)?
나쁜 예:
let completionBlock: (() -> ())?
let completionBlock: ((Void) -> (Void))?
어떤 매개변수가 사용되고 어떤 매개변수가 사용되지 않는지 명확히 함으로써 closure를 읽을 때 필요한 인지 오버헤드가 감소한다.
좋은 예:
someAsyncThing() { _, _, argument3 in
print(argument3)
}
나쁜 예:
someAsyncThing() { argument1, argument2, argument3 in
print(argument3)
}
좋은 예:
UIView.animate(withDuration: 1.0) {
self.myView.alpha = 0
}
UIView.animate(withDuration: 1.0, animations: {
self.myView.alpha = 0
}, completion: { finished in
self.myView.removeFromSuperview()
})
나쁜 예:
UIView.animate(withDuration: 1.0, animations: {
self.myView.alpha = 0
})
UIView.animate(withDuration: 1.0, animations: {
self.myView.alpha = 0
}) { f in
self.myView.removeFromSuperview()
}
좋은 예:
var diameter: Double {
return radius * 2
}
나쁜 예:
var diameter: Double {
get {
return radius * 2
}
}
좋은 예:
var messages: [String]?
var names: [Int: String]?
나쁜 예:
var messages: Array<String>?
var names: Dictionary<Int, String>?
? 는 regular optionals 을 의미한다.
! 는 implicitly unwrapped optionals (암묵적으로 포장이 풀리는 옵셔널)을 의미한다. 가이드에서는 ? 와 ! 로 대체하여 표현한다.
! 는 런타임시 크래시의 원인이 될 수 있다.
-
그러나, 라이프타임이 UI 라이프사이클안에 있는 사용자 인터페이스 객체는 ! 를 사용할 수 있다. 왜냐하면 그것들은 no-nil이 보장되기 때문이다.
- XIB 파일이나 스토리보드의 객체에 연결된
@IBOutlet
속성, 외부에서 주입되는 속성 등 그러한 속성을 ? 로 만드는 것은 사용자가 포장을 풀기에는 너무 많은 부담을 줄 수 있다.
- XIB 파일이나 스토리보드의 객체에 연결된
-
또한 ! 는 단위테스트에서 허용된다. 이는 위의 UI 개체 시나리오와 유사한 이유 때문이다.
- 테스트 fixture의 수명은 종종 테스트의 초기화함수가 아니라 테스트의
setUp()
메서드에서 시작하여 각 테스트를 실행하기 전에 재설정할 수 있다.
- 테스트 fixture의 수명은 종종 테스트의 초기화함수가 아니라 테스트의
좋은 예:
textContainer?.textLabel?.setNeedsDisplay()
옵셔널 바인딩을 사용하는 대신 nil을 체크하면 그 실행문의 의도가 무엇인지 즉시 알 수 있다.
좋은 예:
var thing: Thing?
if nil != thing {
doThing()
}
나쁜 예:
var thing: Thing?
if let _ = thing {
doThing()
}
주석은 기본적으로 Swift API Design Guidelines 의 가이드를 따른다. 위 문서의 내용을 여기에 정리할 수 도 있고, 새로운 가이드라인을 추가할 수도 있다.
두드러진 예외는 UIViewController의 view
속성이다.
좋은 예:
class MyClass: NSObject {
init() {
someValue = 0
super.init()
}
var someValue: Int
}
나쁜 예:
class MyClass: NSObject {
init() {
super.init()
someValue = 5
}
var someValue: Int!
}
데이터베이스 연결 열기, 네트워크 요청, 디스크에서 대량의 데이터 읽기 등의 작업을 수행하지 않는다. 객체를 사용할 준비가 되기 전에 이러한 작업을 수행해야 하는 경우 start()
메서드과 같은 것을 만든다.
이는 중첩성을 감소시키고, 속성 선언으로 부터 사이드이펙트를 분리해내고, oldValue
와 같이 암묵적으로 전달되는 매개변수를 명시적으로 사용하게 만든다.
좋은 예:
class TextField {
var text: String? {
didSet { textDidUpdate(from: oldValue) }
}
private func textDidUpdate(from oldValue: String?) {
guard oldValue != text else {
return
}
// Do a bunch of text-related side-effects.
}
}
나쁜 예:
class TextField {
var text: String? {
didSet {
guard oldValue != text else {
return
}
// Do a bunch of text-related side-effects.
}
}
}
이것은 weak-self가 야기하는 복잡성을 막고 중첩성을 감소시킨다.
좋은 예:
class MyClass {
func request(completion: () -> Void) {
API.request() { [weak self] response in
guard let self = self else { return }
self.doSomething(with: self.property, response: response)
completion()
}
}
func doSomething(with nonOptionalParameter: SomeClass, response: SomeResponseClass) {
// Processing and side effects
}
}
나쁜 예:
class MyClass {
func request(completion: () -> Void) {
API.request() { [weak self] response in
if let self = self {
// Processing and side effects
}
completion()
}
}
}
탈출 조건에서 throws
나 return
문 등을 실행하거나 옵셔널 바인딩을 해야할 때 guard
문을 사용하면 중첩도를 줄여서 가독성과 유지보수성을 높일 수 있다.
좋은 예:
func computeFFT(context: Context?, inputData: InputData?) throws -> Frequencies {
guard let context = context else {
throw FFTError.noContext
}
guard let inputData = inputData else {
throw FFTError.noInputData
}
// use context and input to compute the frequencies
return frequencies
}
guard
let number1 = number1,
let number2 = number2,
let number3 = number3
else {
fatalError("impossible")
}
// do something with numbers
나쁜 예:
func computeFFT(context: Context?, inputData: InputData?) throws -> Frequencies {
if let context = context {
if let inputData = inputData {
// use context and input to compute the frequencies
return frequencies
} else {
throw FFTError.noInputData
}
} else {
throw FFTError.noContext
}
}
if let number1 = number1 {
if let number2 = number2 {
if let number3 = number3 {
// do something with numbers
} else {
fatalError("impossible")
}
} else {
fatalError("impossible")
}
} else {
fatalError("impossible")
}
그런 행동이 필요하지 않는 한 open
보다는 public
, fileprivate
보다는 private
쪽을 선호한다.
좋은 예:
class TimeMachine {
private dynamic lazy var fluxCapacitor = FluxCapacitor()
}
나쁜 예:
class TimeMachine {
lazy dynamic private var fluxCapacitor = FluxCapacitor()
}
예외: static
지정자나 @IBAction
, @IBOutlet
, @discardableResult
같은 attributes
하지만, 타입 정의 내에서는 같은 수준의 접근제어를 생략한다.
명시적으로 표시하게 되면 항상 신중하게 결정하게 된다. 정의 내에서 동일한 접근 제어 지정자를 재사용하는 것은 중복이며, 일반적으로 기본값이 합당하다.
좋은 예:
puplic struct Book {}
internal apiKey : String
private func change(options: [Option]) {}
public class ImageDownloader {
var links = ImageLinks()
private var queue = OperationQueue()
}
나쁜 예:
struct Book {}
apiKey : String
func change(options: [Option]) {}
public class ImageDownloader {
public var links = ImageLinks()
private var queue = OperationQueue()
}
좋은 예:
private extension ViewController {
private func setupViews() {}
}
나쁜 예:
private extension ViewController {
func setupViews() {}
}
타입 정의안에서 메서드를 정의하는 것을 선호한다.
이것은 가독성에 도움이 된다. 더 빨리 찾을 수 있다. 전역 함수는 특정 타입이나 인스턴스와 관련이 없을 때 가장 적절하다.
좋은 예:
class Person {
var bornAt: TimeInterval
var age: Int {
// ...
}
func jump() {
// ...
}
}
나쁜 예:
func age(of person, bornAt timeInterval) -> Int {
// ...
}
func jump(person: Person) {
// ...
}
public
또는 internal
인 경우 namespacing 을 위해 static
속성으로 정의한다.
좋은 예:
private let privateValue = "secret"
public class MyClass {
public static let publicValue = "something"
func doSomething() {
print(privateValue)
print(MyClass.publicValue)
}
}
네임스페이스 없이 전역 상수와 함수를 만들지 않는다. 명료함을 위해서라면 네임스페이스를 제한없이 중첩한다.
case
없는 enum
은 인스턴스로 만들 수 없기 때문에 순수한 네임스페이스로 잘 맞는다.
좋은 예:
enum Environment {
enum Earth {
static let gravity = 9.8
}
enum Moon {
static let gravity = 1.6
}
}
값을 명시적으로 할당한다면 그 이유를 설명하는 comment를 추가한다.
사용자 오류를 최소화하고 가독성을 향상시키며 코드를 더 빨리 쓰려면 Swift의 자동 enum
값을 사용한다. 그러나 값이 외부 소스에 매핑되거나(예: 네트워크 요청에서 제공됨) 바이너리 전역에 걸쳐 사라지지 않고 유지되는 경우, 명시적으로 값을 정의하고 이 값이 매핑되는 대상을 문서화한다. 이렇게하면 누군가가 중간에 새로운 값을 추가한다고해도 기존 것을 깨뜨리지 않게 된다.
좋은 예:
enum ErrorType: String {
case error
case warning
}
/// 이것은 logging service에서 사용된다. 명시적인 값은 바이너리 전역에 걸쳐 일관성을 보장한다.
// swiftlint:disable redundant_string_enum_value
enum UserType: String {
case owner = "owner"
case manager = "manager"
case member = "member"
}
// swiftlint:enable redundant_string_enum_value
enum Planet: Int {
case mercury
case venus
case earth
case mars
case jupiter
case saturn
case uranus
case neptune
}
/// 이러한 값은 서버에서 제공하므로, 여기에 명시적으로 매칭되는 값을 설정한다.
enum ErrorCode: Int {
case notEnoughMemory = 0
case invalidResource = 1
case timeOut = 2
}
나쁜 예:
enum ErrorType: String {
case error = "error"
case warning = "warning"
}
enum UserType: String {
case owner
case manager
case member
}
enum Planet: Int {
case mercury = 0
case venus = 1
case earth = 2
case mars = 3
case jupiter = 4
case saturn = 5
case uranus = 6
case neptune = 7
}
enum ErrorCode: Int {
case notEnoughMemory
case invalidResource
case timeOut
}
예외: Codable을 준수하는 타입의 경우 내부의 enum case에서는 명시적인 값을 정의한다
새 컬렉션에 추가하는 대신 map
과 compactMap
을 사용한다. 변경 가능한 컬렉션에서 요소를 제거하는 대신 filter
를 사용한다.
mutable 변수는 복잡성을 증가시키므로 가능한 한 범위를 좁히도록 한다.
좋은 예:
let results = input.map { transform($0) }
let results = input.compactMap { transformThatReturnsAnOptional($0) }
나쁜 예:
var results = [SomeType]()
for element in input {
let result = transform(element)
results.append(result)
}
var results = [SomeType]()
for element in input {
if let result = transformThatReturnsAnOptional(element) {
results.append(result)
}
}
메소드를 재정의해야하는 경우에는 class
키워드를 사용한다.
좋은 예:
class Fruit {
static func eatFruits(_ fruits: [Fruit]) { ... }
}
나쁜 예:
class Fruit {
class func eatFruits(_ fruits: [Fruit]) { ... }
}
클래스를 재정의해야하는 경우에는 final
키워드를 생략한다.
호출할
class
구현체를 선택하는 과정은 런타임 단계에서 수행되며, 이는 dynamic dispatch로 알려져있다. 런타임 오버헤드의 일정부분은 클래스 상속을 사용하는 것과 관련이 있다.final
키워드는 메서드나 함수의 경우 오버라이드 할 수 없게 하고, 클래스는 서브클래싱 할 수 없게 한다. 이 키워드는 런타임에서 메서드나 속성을 직접 호출할 수 있게 해줄 것이며, 약간의 성능 향상을 가져온다. 스위프트 4 프로토콜지향 프로그래밍 3/e 에서 요약 인용
좋은 예:
final class SettingsRepository {
// ...
}
나쁜 예:
class SettingsRepository {
// ...
}
모든 case
를 열거하는 것은 개발자와 검토자가 새로운 case
가 추가될 때 모든 switch
문장의 정확성을 고려하도록 해준다.
좋은 예:
switch anEnum {
case .a:
// Do something
case .b, .c:
// Do something else.
}
나쁜 예:
switch anEnum {
case .a:
// Do something
default:
// Do something else.
}
case
내 처리해야 할 코드가 있는 경우, case
내에 작성된 break
는 컴파일러에게 일만 추가 시키고, 실행되는 의미는 없는 코드가 되어버린다.
좋은 예:
switch anEnum {
case .a:
// Do something
case .b, .c:
// Do something else.
}
나쁜 예:
switch anEnum {
case .a:
// Do something
break
default:
// Do something
break
}
좋은 예:
["1", "2", "3"].compactMap { Int($0) }
var size: CGSize {
CGSize(
width: 100.0,
height: 100.0)
}
func makeInfoAlert(message: String) -> UIAlertController {
UIAlertController(
title: "ℹ️ Info",
message: message,
preferredStyle: .alert)
}
나쁜 예:
["1", "2", "3"].compactMap { return Int($0) }
var size: CGSize {
return CGSize(
width: 100.0,
height: 100.0)
}
func makeInfoAlert(message: String) -> UIAlertController {
return UIAlertController(
title: "ℹ️ Info",
message: message,
preferredStyle: .alert)
}
마틴파울러의 GivenWhenThen 구글링으로 여러 번역본을 찾을 수 있습니다.
좋은 예:
var isMember: Bool {
MemberManager.shared.isMember
}
나쁜 예:
func isMember() -> Bool {
MemberManager.shared.isMember
}
객체의 public interface를 정의할 때 따르는 가이드라인
Law of Demeter. 낯선자에게 말하지 말고, 오직 인접한 이웃하고만 말하라.
디미터 법칙을 따르는 코드는 메시지 전송자가 수신자의 내부 구현에 결합되지 않는다.
디미터 법칙을 위반한 코드를 수정하는 일반적인 방법은 묻지 말고 시켜라
를 따르는 것이다. 객체에게 내부 구조를 묻는 대신 직접 자신의 책임을 수행하도록 시키는 것이다.
이 두 스타일을 따르면 자연스럽게 자율적인 객체로 구성된 유연한 협력을 얻게 된다.
좋은 예:
public class ReservationAgency {
func func reserve(screening: Screening, customer: Customer, audienceCount: Int) -> Reservation {
let fee = screening.calculateFee(audienceCount)
Reservation(customer, screening, fee, audienceCount);
}
}
나쁜 예:
public class ReservationAgency {
func reserve(screening: Screening, customer: Customer, audienceCount: Int) -> Reservation {
let movie: Movie = screening.movie
var fee: Int = 0
if movie.isDiscountable {
let discountAmount = movie.discountAmount
fee = (movie.fee - discountAmount) * audienceCount
} else {
fee = movie.fee * audienceCount
}
return Reservation(customer, screening, fee, audienceCount);
}
}
예외: self 속성의 컬렉션 요소에는 메시지를 전송해도 좋다.
예외: 디미터법칙은 하나의 점을 강제하는 규칙이 아니다
아래 코드는 디미터법칙을 위배하지 않는다. 객체의 내부에 대한 어떤 내용도 묻지 않는다.
[1, 10, 99, 101, 1000].filter{ $0 > 100 }.map{ $0 * 10 }.first
객체의 내부 구조가 노출되지 않는다면 법칙을 준수한 것이다.
속성은 private
으로 만들어서 직접 접근하지 않고, 메서드만 사용한다.
묻지 말고 시켜라 (Tell, Don't Ask) 원칙. 객체의 상태에 관해 묻지 않고 원하는 것을 시킨다.
이렇게 하면 객체에 대해서 알아야할 정보를 최소화할 수 있다. 그리고, 자연스럽게 관련 정보를 가장 잘 알고 있는 객체에게 책임을 할당할 수 있다. 외부에서 객체의 상태를 기반으로 결정을 내리게 되면 캡슐화를 위반하게 된다.
예외:
- 묻는 것 이외에는 다른 방법이 존재하지 않는 경우도 있다. 객체에게 묻지않고 직접 그 일을 하도록 하는 경우 응집도는 높아질 수 있지만, 본질적이지 않는 책임을 떠안게 되어 객체간 결합도가 높아질수 있다.
- 물으려는 객체가 정말로 데이터인 경우도 있다.
- 자료구조라면 당연히 내부를 노출해야하므로 디미터 법칙을 적용할 필요가 없다.
속성에 직접 접근해야한다면 readonly로 만들어서 외부에서 직접 수정할 수 없게 한다. 속성값 수정이 필요하면 메서드를 통해서만 가능하게한다.
좋은 예:
public struct Movie {
private(set) isDiscountable: Bool
private(set) discountAmount: Int
private(set) fee: Int
}
나쁜 예:
public struct Movie2 {
var isDiscountable: Bool
var discountAmount: Int
var fee: Int
}
- '어떻게'는 내부 구현을 설명하며, 설계 시점에 내부 구현에 대해 고민할 수 밖에 없다. 결과와 목적만을 포함하도록 타입과 오퍼레이션의 이름을 부여한다.
- 가장 추상적인 이름을 붙인다. Tip: 매우 다른 두번 째 구현을 상상한다. 그리고, 해당 메서드에 동일한 이름을 붙인다고 상상한다. 가장 추상적인 이름을 메서드에 붙이게 될 것이다.
좋은 예:
public class TicketSeller {
private var ticketOffice: TicketOffice
func sell(to audience: Audience) {
audience.buyTicket(from:ticketOffice.ticket)
}
}
public class PeriodCondition {
func isSatisfied(_ screening: Screening) { ... }
}
나쁜 예:
public class TicketSeller {
private var ticketOffice: TicketOffice
func setTicket(to audience: Audience) {
audience.setTicket(from:ticketOffice.ticket)
}
}
public class PeriodCondition {
func isSatisfiedByPeriod(_ screening: Screening) { ... }
}
어떤 오퍼레이션도 명령이 동시에 쿼리가 되게 하지 않는다.
루틴
은 어떤 절차를 묶어 호출 가능하도록 이름을 부여한 기능모듈이다.루틴
은프로시저
와함수
로 나뉜다.프로시저
는 사이드이펙트를 발생시킬 수 있지만 값을 반환할 수 없다.함수
는 사이드이펙트를 발생시킬 수 없지만 값을 반환할 수 없다. 명령 : 쿼리 =프로시저
:함수
- 분리로 인한 긍정적인 효과
- 쿼리의 순서를 자유롭게 변경할 수 있다.
- 객체지향 패러다임은 객체의 상태변화라는 사이드이펙트를 기반으로 하고 있다. 명령과 쿼리를 분리하면 객체의 사이드이펙트를 제어하기가 수월해진다.
- 가독성이 좋아지고, 디버깅이 쉽고, 실행 결과를 예측 가능하다.
함수형 프로그래밍은 사이드이펙트가 존재하지 않는 수학적인 함수를 기반으로 한다. 그래서, 실행결과를 이해하고 예측하기가 더 쉽다.
struct
는 value semantics를 가지고 있다. 정체성(identity)이 없는 것에는 struct
를 사용한다. [a, b, c]
를 포함하는 배열은 [a, b, c]
를 포함하는 다른 배열과 실제로 동일하며 완전히 교환할 수 있다. 첫 번째 배열을 사용하든 두 번째 배열을 사용하든 상관없다. 왜냐하면 그것들은 정확히 같은 것을 나타내기 때문이다. 그렇기 때문에 배열은 struct
이다.
class
는 reference semantics를 가지고 있다. 정체성(identity)이나 특정한 라이프 사이클이 있는 것에 대해 class
를 이용한다. 두 사람 객체는 서로 다르기 때문에 사람을 class
로 모형화 할 것이다. 두 사람이 이름과 생년월일이 같다고 해서 같은 사람이 되는 것은 아니다. 그러나 1950년 3월 3일의 날짜는 1950년 3월 3일의 다른 날짜 객체와 같기 때문에 그 사람의 생년월일은 struct가 될 것이다. 날짜 자체는 정체성이 없다.
파일을 논리 그룹으로 나누기 위해 높이가 다른 빈 줄보다는 이러한 포맷팅 가이드라인을 선호한다.
각 extension
은 // MARK: -
주석을 달아 잘 정리해야 한다.
관련 메서드가 프로토콜과 함께 그룹화되어 유지되며 관련 메서드를 찾기가 더 쉽고 유지보수가 용이하다.
좋은 예:
class MyViewController: UIViewController {
// class stuff here
}
// MARK: - UITableViewDataSource
extension MyViewController: UITableViewDataSource {
// table view data source methods
}
// MARK: - UIScrollViewDelegate
extension MyViewController: UIScrollViewDelegate {
// scroll view delegate methods
}
나쁜 예:
class MyViewController: UIViewController, UITableViewDataSource, UIScrollViewDelegate {
// all methods
}
내장 모듈을 먼저 놓고, 빈 줄로 세컨드파티를 구분한다. 이후 추가 빈 줄로 서드파티를 구분할 수 있다. header comment 다음에 한 줄을 띄우고 첫 import 문을 시작한다. 이외에는 import 문 사이에 빈 줄을 추가하지 않는다.
standard organization 방법을 통해 파일이 어떤 모듈에 의존하는지를 보다 신속하게 알아낼 수 있다.
좋은 예:
// Copyright © 2022 Wantedlab. All rights reserved.
//
import UIKit
import SecondParty
import SnapKit
import React
import Toaster
예외: @testable
은 일반적인 import
문 뒤에 위치하고 빈 줄로 구분해야 한다.
좋은 예:
// Copyright © 2022 Wantedlab. All rights reserved.
//
import Nimble
import Quick
@testable import wanted
나쁜 예:
// Copyright © 2022 Wantedlab. All rights reserved.
//
import Nimble
@testable import wanted
import Quick
+
는 짧지만 명확하게 의미를 전달한다. 확장되는 코드를 논리적인 기능 block으로 나눌 수 있으며, 가독성과 유지보수성이 좋아진다. 찾는 항목에 대한 검색 범위를 좁히는 데 도움이 된다.
좋은 예:
```swift
AppDelegate+.swift
UIColor+.swift
UIImage+.swift
UIViewController+.swift
```
The Official raywenderlich.com Swift Style Guide
오브젝트 by 조영호