제목: On Comments

코드를 작성할때 가장 큰 적은 복잡성이다. 추상화 수준(level of abstraction)을 유지하고, 코드베이스에서 작업하는 개발자들은 그 개념으로 동작하게 하는 것은 큰 소프트웨어 프로젝트에서 필수적이다.

주석는 복잡성을 유지보수하는 도구이기도하지만, 때로는 돕는게 아니라 미묘하게 코드베이스를 상하게 만들기도 한다.

주석의 내 견해는 두가지로부터 온다. 1) 주석은 컴파일하지 않는다. 2) 주석은 연하게 문법 하이라이트된다. 이것은 컴파일 되지 않고 시아에서 흐릿하게 보이기 때문에 코드에 변경사항이 생길때 무시되기 쉽다. 코드는 변경했는데 주석은 바뀌지 않는다면, 코드의 내용을 정확하게 반영하지 않은 주석과 함께해야한다.

이상적인 세상에서는 코드 리뷰때 이것을 발견할 수도 있지만, 실제로는 절때 발견하지 못할때도 있다. 코드 리뷰 툴이 문법 하이라이트 기능을 가지고 있더라도 주석은 그 문맥에서 백그라운드로 사라지게된다. 또한, 코드 리뷰는 변경된 부분의 주변만 보여주기 때문에, 오래된 주석은 그 자체가 변경될 쯤에만 발견될 수 있을 것이다. 만약 메소드의 전제조건을 변경하여 한 라인을 바꿨다면, 리뷰어는 주석을 보지 못한채로 바꿔라고 말하지 못할 것이다.

일반적인 주석을 피하는 몇가지 방법이 있다. 그 중 몇개는 a blog post by an old manager of mine에서 다루고, 나머지는 Andrew에서 나온 것인데, 루비에서 나온 것이므로 다이나믹 타입 언어에대한 이야기여서, 적용시킬 수 없다.

  1. 이름을 잘 정하자! 주석을 피하는 첫번째 방법은 다음과 같다. 한 단어 이름, 추상적인 이름, 모호한 이름을 피하자. 여러분이 할 수 있는 만큼 더 정확하게 이름을 정할수록 주석을 달 필요가 줄어든다.
  2. 메소드에 전제조건이 있으면 유효하지 않은 값이 들어올때 앱이 크래쉬내는 assertion를 넣자(항상 그러지는 말고 디버깅때만!). 만약 양수지만 0이아닌 정수만 받는다면 코드에 precondition(int>0)으로 작성할 수 있다.
  3. 런타인 assertion보다 나은 것이 컴파일타임의 것이다. 만약 메소드가 비어있지 않는 배열만 받는다면 precondition(!array.isEmpty)를 쓸 수 있다. 그러나 절때 빈 배열이 되지 않는 타입을 사용하는 것도 하나의 방법이다. 여러분의 API의 사용자들은 이제 절때 파라미터에 빈 배열을 보낼 수 없을 것이다.같은 성질에서, 두 이름을 case를 가진 열거형으로 표현된 것보다 더 잘 표현된 불(bool) 파리미터를 가져본 적이 있는가? 다른 열거형처럼 여러분의 옵셔널이 더 잘 표현되는가?  여러분의 의도를 네이밍에 드러내라.
  4. 임시 코드, 프로토타입 코드에 표시를 하자. 나는 종종 이 함수가 이상적으로 짜지지 않았으면 hack_를 표시한다. 스위프트에서 메소드의 언더바는 내 코드베이스를 좋지않게 만들어 괴롭히므로 보기에 거슬린다.그리고 이것을 상기시켜 고치게 만든다. 우리는 최근에 shouldReallyBeInTheCoordinator_를 접두에 붙인 함수를 만들었는데, 코드리뷰를 받아야야할때 코드가 올바른 클래스에 있지 않았기 때문이다. 올바르지 않은 코드가 거슬리게 생긴다면, 코드베이스 요구와 여러분의 감정이 드러맞게 된다. 또다른 좋은 접두에는 perf_temp_ 같은 것들이 있다.
  5. Mark Sands에 의하면, ID를 버그 추적기에서 메소드 이름으로 인코드 할 수 있고, 이것은 스택 트레이스(stack traces)에서 나타날 것이다. UIKit은 몇 케이스에서 레이더 넘버(Radar Number)를 참조한다. 이것은 현실이다.
    -[UIViewController _hackFor11408026_beginAppearanceTransition:animated:]
  6. 함수의 이름이 "왜" 이것인지 설명하는 것을 두려워하지마라. updateFrameOnNextTickBecauseAutoLayoutHasntCompletedYet(frame: CGRect)처럼 함수를 만들 수 있다. 컴파일러는 메소드 길이를 신경쓰지 않으며 코드는 작성하는 수보다 읽히는 횟수가 더 많다. 주석은 단지 단어들이며 메소드 이름이다. 그 코드베이스의 미래 유지보수 담당자는 여러분의 장황한 설명에 감사할 것이다.
  7. TODO(date: Date, message: String) 같은 보조함수(helper function)을 만들어라. 여기서 TODO는 어떤 날짜를 정하지 않으면 에러를 출력한다. (혹은 디버깅에서 크래쉬를 내는게 더 나을 수도 있다) Jordan Rose의 또다른 예제이다.
  8. 테스트에 어떤 알고리즘 요구사항을 인코드하라. 만약 위의 모든것이 실패하고, 특정 문제를 풀기에 precondition, 타입, 메소드이름에 의존할 수 없다면, 테스트를 작성하라. 이것은 특히 엣지 케이스에 좋다. 누군가 코드를 다시 작성했는데, 테스트에 실패한다면 새로운 코드안에 다뤄줘야할 케스이가 있다는 사실을 알게될 것이다.
기억하기: 받아드리기 힘든 코드를 작성하거나 주석이 없는 코드는 변명할 수 없다! 주석을 스킵한 코드는 반드시 명확해야한다. 결국에는 나를 위한 주석이다. 다음 프로그래머에게 내 의도를 표현할 수 있는 또다른 방법을 찾을 수 있다면 주석을 달지 않을 것이다.

더 읽을거리


이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
제목: That One Optional Property

때론 새로운 것을 위해 여러분의 뷰컨트롤러를 수정해야할 필요가 있다. 한가지는 다른것을 야기하는데, 뷰컨트롤러에 옵셔널 변수를 추가하는 자신을 발견할 것이다. 이것은 몇몇 케이스에서 설정될 것이고 몇몇은 아닐 것이다.

내 생각엔 더 자주 일어나고 이것은 결점이있는 방법이다. 여기에는 몇가지 이유가 있다. 첫째로 어떨때만 쓰이는 옵셔널 프로퍼티를가진 클래스는 정체성이 약한 느낌을 가진다. 바꿔 말해보면, 옵셔널 프로퍼티를 추가하면 그 타입의 근본적인 의미가 흐려진다. 첫번째로 이 옵셔널 프로퍼티는 어떤 시멘틱 의미를 고려하지 않는다. 이 타입이 nil이라면 이 오브젝트의 상태가 무엇이라 할 수 있을까? 여러분의 코드를 가볍게 읽고있는 사람들은 어떤 경우에 프로퍼티가 nil이될지, 혹은 그 오브젝트에서 가지는 분기가 무엇인지 말할 수 없을 것이다. 세번째로 코드는 가변으로 계속 될것이다. 한 옵셔널 프로퍼티는 누가지 선택적 프로퍼티로 될것이고, 이것은 세개로 되고, 당신이 알기 전에 미끄러운 경사의 아래에 가있을 것이다. 이 값이 반드시 존재하는지 이 값이 nil인지 표현하고 싶으면 간단한 옵셔널로는 그렇게 할 수 없다.

내가본 모든 코드베이스에서는, 여러분의 뷰컨트롤러에 왜 옵셔널 프로퍼티가 필요한지에대한 두가지 주된 이유를 발견했다. 나는 이 두가지 다를 탐험할 것이고 각 문제를 해결하기위한 더 나은 패턴을 제안할 것이다.

문제의 옵셔널 프로퍼티를 가지는것에대한 첫번째 이유는 이 뷰컨트롤러가 쓰일때마다 오브젝트나 몇 데이터를 반드시 가질 필요가 없을때이다.

최근에 만난 이것에대한 예시는 뷰컨트롤러가 어떤 경우 푸시 노티피케이션으로부터 표시되는 상황이다. 이때 노티피케이션에서 나온 특정 메시지를 보여줘야한다. 이 문제를 해결하기위한 가장 간단한 방법은 옵셔널 문자열 프로퍼티를 추가하는 것이었다.
class LocationViewController: UIViewController {
     //...
     var notificationMessage: String?
     //...
}
뷰컨트롤러에 다른 코드는 어떤 뷰에 할당할지, 어떻게 배치할지등을 결정하기위해 메시지가 있다고 전환했다. 프로퍼티의 선택성은 그냥 문자열의 존재보다 더 표현한다. It had implications in the rest of the view controller and the rest of the view layer.


여기서 더 중요한 점은 문자열이 더이상 거기에 있을지 아니면 없을지 표현하는게 아니게 된다. 이제 뷰컨트롤러 안에있는 표현 스타일이나 모드를 표현한다. 이것이 푸시 노티피케이션의 문맥에서 만들어진 것인가 혹은 일반 브라우징을 통해 만들어진 것인가? 답은 가까스로 관련된 프로퍼티에 있다.

이 문제를 해결하기위해, 이 모드를 모두 명시적으로 만들어야한다. 이 뷰컨트롤러에서 이것은 일급시민이면, 뷰컨트롤러의 그 영향은 더 분명해질것이다.
class LocationViewController: UIViewController {
     //...
     enum Mode {
          case fromNotification(message: String)
          case normal
     }

     var mode: Mode
     //...
}
Sandi Metz가 말한것처럼, 한 특수화는 절때 없다(there's never one specialization). 옵셔널 프로퍼티를 쓰면 그 코드는 이 프로퍼티의 nil 상태에대한 의미를 가지거나 고유의 의미를 가진다고 할 수 없다. 열거형을 사용하면 코드에서 구체화되고 형식화된다.

새로운 열겨형이 Optional 타입 정의의 열거형 형태와 아주 비슷하다는 점을 인지하자.
enum Optional<Wrapped> {
     case some(Wrapped)
     case none
}
그러나 여기에는 몇가지 분명히 다른 유용한점이 존재한다.

첫째로 시멘틱이다. somenone은 추상적이다. normalfromNotification은 그것과 관련된 의미를 가진다. 여러분의 코드를 읽는 사람들은 여러분에게 감사할 것이다.

두전째로 확장성이다. 뷰 컨트롤러에 다른 모드가 추가되면 그것을 완전히 설명하는 더나은 의미를 가진다. 만약 새로운 모드가 두 모드와 절때 겹치지 않으면, 필요한 연관된 데이터가 어떻든 새로운 열거형 케이스를 추가할 수 있다. 새로운 모드가 현재 모드들과 겹친다면, 새로운 열거형이되어 새로운 프로퍼티를 함께할 수 있다. 오브젝트의 상태는 읽기에 더욱 설명가능한 것이 된다. 어떤 선택이든 새 모드를 위해 또다른 옵셔널을 추가하는 것보다는 낫다.

세번째도 확장성이다. LaunchViewController.Mode가 일급 타입이기 때문에 우리는 함수와 계산된 프로퍼티를 추가할 수 있다. 예를들어 노티피케이션 메시지를 잡아두는 레이블의 높이는 아마 그 메시지의 존재에 달렸을 것이다. 따라서 코드를 열거형으로 옮길 수 있다.
extension Mode {
     var notificationLabelHeight: CGFloat {
          switch self {
          case .normal: return 0
          case .fromNotification(let message): return message.size().height
          }
     }
}
더 풍요로운 타입으로 데이터를 옮기는 것은 적은 코드를 투자하여 이 모든 앞단에서의 이득을 제공한다.

옵셔널 프로퍼티를 사용할지도 모르는 두번째 이유는 첫번째 이유의 부분집합이다. 어떤 경우, 옵셔널 프로퍼티는 뷰컨트롤러의 다른 모드를 표현하지 않는데, 코드에서 임시적인 특징을 표현한다. 아직 값을 가지고 있지 않기 때문에 값으로 프로퍼티를 초기화할 수 없다. 이것의 일반적인 예시는 네트워크로부터 뭔가 패치를 하거나, 시스템으로부터 뭔가 긴 검색시간이 걸려 비동기가 필요해서 기다려야 할때이다.
class UserViewController: UIViewController {
     //...

     // will be loaded asynchronously
     var user: User?
     //...
}
이런 종류의 문제는 어떨때 데이터가 배열형식으로 들어오면 준비될 수 있다. 빈 배열로 존재하지 않는 상태를 표현할 수 있기 때문에, 옵셔널을 사용하지 않더라도 이 문제는 여전히 숨어있다. 테이블 뷰 컨트롤러에 빈 상태를 추가하는 힘든 상황의 횟수가 바로 이런 문제의 정도를 나타낸다.

이 문제는 첫번째 문제의 부분집합이기 때문에, 열거형으로 같은 방법으로 해결할 수 있다.
enum LoadingState<Wrapped> {
     case initialcase loading
     case loaded(Wrapped)
     case error(Error)
}
이 솔루션이 동작하는동안, 비동기 상태를 관리하기위한 더 나은 추상화가 있다. 아직 존재하진 않지만 미래의 어느 시점엔 있을 한 값은 Promise에의해 잘 표현된다. 프로미스는 여러분의 유스케이스에따라 여러 이점을 가진다.

먼저, 프로미스는 가변을 허락하지만, 오직 한번만 그리고 오직 결정되지 않을때까지만이다. 프로미스가 한번 변경되면, 다시는 변경할 수 없다. (여러번에걸처 변경해야한다면, Signal이나 Observable이 여러분이 찾던 것일 것이다.) 이 말은 let과같은 시멘틱을 가지지만 여전히 비동기 데이터를 관리한다.

다음으로, 프로미스는 값이 들어왔을때, 없어질 블럭을 추가하는 기능을 가진다. 만약 프로퍼티가 간단한 옵셔널 프로퍼티를 남긴다면, 추가된 블럭들은 didSet 프로퍼티 옵저버에서 코드와 동일하다. 그러나 프로미스 블럭은 하나 이상 추가할 수 있고, 클래스 어디에서든 추가될 수 있기 때문에 어욱 강력하다. 게다가 프로미스가 이미 값으로 채워져 있을때 추가한다면 그들은 즉시 실행될것이다.

마짐가으로 필요에따라 프로미스는 에러도 처리한다. 특정 종류의 비동기 데이터에대해, 이것이 중요하며, 이것으로부터 자유로울 것이다.

Promise 라이브러리를 사용하고 있다면 빈 생성자로 미결정된 Promise를 생성할 수 있고, fulfill 메소드로 언제든지 채울 수 있다.

옵셔널 프로퍼티를 사용할 때는, 가끔 예전에 보이지 않던 것들이 드러낸다. 뷰컨트롤러에 옵셔널 파라미터를 추가하는 자신을 발견할 때 스스로에게 물어보자. 이 옵셔널이 정말로 의미하는바는 무엇인가? 이 데이터를 표현할 더 좋은 방법은 없는가?



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.


WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
제목: You Probably Don't Want enumerated

스위프트 표준 라이브러리에서 한가지 종종 오용되는 부분은 시퀀스(Sequence)의 enumerated()함수이다. 이 함수는 새로운 시퀀스를 만들어주는 데, 이 시퀀스는 원래 시퀀스의 각 요소와 그 요소에 해당되는 번호를 가진다.

enumerated()는 잘못 사용되고 있다. 이 함수는 각 요소에 번호를 제공하기 때문에 그 번호 문제에대한 쉬운 해결법이 될 수 있다. 그러나 이런 번호 문제는 더 나은 방법으로 해결될 수 있다. 그런 경우가 무엇인지 보자. 우리가 어떤 실수를 하고 있는지 보고, 이것을 추상적인 수준에서 한번 해결해보자.

enumerated()를 사용할때 주된 이슈는 이 함수가 요소와 그에 해당하는 각 인덱스(index)를 반환한다고 생각해버리는 것이다. 이 함수는 모든 시퀀스에서 사용가능하지만, 시퀀스는 인덱스를 가진다는 것을 보장하는 녀석이 아니기 때문에, 우리는 이것이 인덱스가 아니라는 것을 기억해야한다. 코드에서 index라 부르지않고 offset이라 부르고 있는데, 이 네이밍 컨벤션은 글 마지막에 소개해 놓았다. 오프셋(offset)은 항상 0에서 시작하여 각 요소마다 증가하는 정수형태를 말한다. Array의 경우 인덱스와 일치하겠지만, 기본적으로 다른 타입에서는 그렇지 않다. 아래 예제를 한번 보자.
let array = ["a", "b", "c", "d", "e"]
let arraySlice = array[2..<5]
arraySlice[2] // => "c"
arraySlice.enumerated().first // => (0, "c")
arraySlice[0] // fatalError
arraySlice라는 변수는 당연하게도 ArraySlice이다. 그러나 startIndex는 특별하게도 0이아닌 2이다. 이때 enumerated()first를 호출하면 오프셋이 0인 튜플을 반환해주고, 그 첫번째 요소인 "c"를 반환한다.

다른 방법으로 예시를 보자.
zip(array.indices, array)
실제로는 이렇게 한다.
zip((0..<array.count), array)
그리고 Array가 아닌 다른 것과 작업을 하면 언제든지 틀린 동작 결과를 만들 것이다.

enumerated()를 사용하면서 (인덱스가 아닌) 오프셋을 사용것으로 생긴 이슈 말고도 다른 이슈들이 있다. enumerated() 사용에대해 여러번 생각해볼 수 있는데, enumerated()를 사용할때 얻을 수 있는 더 나은 이점이 있다. 조금 더 살펴보자.

내가 본 enumerated()의 가장 일반적인 사용은, 다른 배열로부터 일치하는 요소를 잡기위해 열거된(enumerated) 배열로부터 오프셋으로 사용하는 것이다.
for (offset, model) in models.enumerated() {\
     let viewController = viewControllers[offset]
     viewController.model = model
}
이 코드는, 배열이된 modelsviewControllers에 의존하는데, 이 배열은 0에서 시작하고 정수에의해 색인(index)된다. 그리고 이 배열의 길이가 같다는 것에도 의존하고 있다. 만약 models 배열이 viewControllers 배열보다 짧다면, 별다른 나쁜일이 일어나지 않겠지만, viewControllersmodels보다 짧다면 크레쉬가 일어날 것이다. 또한 큰 역할을 하고 있지도 않은 추가적인 offset 변수까지 가지고 있어야한다. 더 스위프트한 방법으로 다시 짜보면 아래처럼 될 수 있다.
for (model, viewController) in zip(models, viewControllers) {
     viewController.model = model
}
이 코드는 읽는이를 집중시키며, 모든 Sequence 타입에서 동작한다. 또한 배열의 길이가 일치하지 않는 것도 알아서 처리해준다. 더 나은 방법일 것이다.

다른 예제를 보자. 이 코드는 첫번째 imageView와 그 컨테이너 사이에 오토레이아웃 제약(constraint)를 추가하고 쌍의 이미지뷰 사이의 오토레이아웃 제약을 만든다.
for (offset, imageView) in imageViews.enumerated() {
     if offset == 0 {
          imageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
     } else {
          let imageToAnchor = imageView[offset - 1]
          imageView.leadingAnchor.constraint(equalTo: imageToAnchor.trailingAnchor).isActive = true
     }
}
이 코드 예제도 비슷한 문제가 있다. 우리는 쌍의 요소가 필요하지만, 고수준에서 작업할때 enumerated()를 사용하는 것은 지긋지긋한 인덱스를 다뤄가며 필요한 번호를 뽑아내야 한다는 의미이다. 이 부분도 마찬가지로 zip이 도와줄 것이다.

먼저 첫번째 요소에서 컨테이너 제약을 다루는 코드를 작성하자.
imageViews.first?.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
다음으로 이 쌍의 요소를 다루자.
for (left, right) in zip(imageViews, imageViews.dropFirst()) {
     left.trailingAnchor.constraint(equalTo: right.leadingAnchor).isActive = true
}
이제 됐다. 인덱스는 보이지 않고, 어떠한 (다중-전달) 시퀀스로 동작하므로 더 집중할 수 있게 되었다.

(한 익스텐션에 쌍으로 만드는(pairing) 코드를 넣을 수도 있고, 필요에 따라서는 .eachPair()을 호출할 수도 있다.)

enumerated()의 몇몇 유효한 사용이 있을 수 있다. 여러분이 얻어내고 있는 것이 인덱스가 아니라 그냥 정수이기 때문에 각 요소에 해당하는 (인덱스가 아닌) 번호로 작업해야 할 때가 바로 옳바른 사용 시점이다. 예를들어 여러 뷰들의 각 수직 좌표 y를 높이와 시퀀스의 오프셋의 곱으로 만들어야 한다면 enumerated()가 적절할 것이다. 아래에 구체적인 예시가 있다.
for (offset, view) in views.enumerated() {
     view.frame.origin.y = offset * view.frame.height
}
여기의 offset은 번호의 속성으로 사용되고 있기때문에 enumerated()가 잘 동작한다.

이제 간단하게 요약해보자면, enumerated()를 인덱스로 사용하고 있다면 그 문제를 해열하는데 더 좋은 방법이 있을 것이며, enumerated()를 번호로 사용한다면 좋아요를 표시한다.



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,