스위프트로 다시보는 객체지향 프로그래밍: 피해야할 코딩 습관

배경

컴퓨터와 회화(painting)를 전공했고 꽤 성공한 누군가가 프로그래머는 과학자보다 화가와 공통점이 더 많다고 했다. 정말로, 프로그래밍을 잘하려는 노력을 하다보면 과학보다는 경험적으로 실력을 쌓아야하는 운동이나 미술과 비슷하다고 느껴진다.

좋은 그림을 알아보는 능력과 그림을 잘 그리는 것이 별개이듯 코드를 보고 좋다 나쁘다를 판단할 줄 아는 것과 좋은 코드를 짜는 실력은 안타깝게도 붙어오지 않는다. 코드를 많이 보게 되고 많이 짜보고 남이 짠 코드를 유지보수 해보면서 경험적으로 판별하는 후각이 조금은 탑재된 것 같다. 하지만 내가 키보드를 두들기기 시작하면 답답함이 찾아온다. 이렇게 짜면 안된다는.. 본능적인? 경험적인? 느낌은 마구 들지만, 그렇다면 이렇게 말고 어떻게 다르게, 좋게 짜야하는지에 대한 명쾌한 답은 없는 그런 상태이다. 학창 시절에 시험을 치다보면 자신있게 답을 고르는 경우도 있지만 확실히 아닌 답을 제거한 후에 남아있는 것 중에서 고르는 경우도 있다. 확실히 아닌 것을 배제하고 나면 남아 있는 선택지에 더 많은 고민을 투자할 수 있고, 답을 선택할 확률이 높아진다. 프로그래밍으로 다시 돌아와보면, 잘못된 패턴이나 유지보수에 좋지 않은 코드의 모습을 알고 이를 피하면 좀 더 나은 코드를 쓰게 되지 않을까 하는 생각을 하게 됐다.

작년, 회사에서 함수형 스위프트 프로그래밍 스터디를 했었다. 스터디를 시작하던 날, 공부 방향을 잡기 위해 가이드를 받고자 사내에서 함수형 스위프트 강의를 하셨던 멘토님을 초빙해 조언을 부탁했다. 그 자리에서 멘토님은 지금까지 약 1년간 내 프로그래밍 공부의 방향이 된 말을 하셨다. “함수형 프로그래밍이 얼마나 어떻게 좋은지 비교하려면 잘 짠 함수형 코드와 잘 짠 객제 지향형 코드를 비교해야하는데 아직도 내가 객체 지향으로 잘 짠다고 할 수 없어서 비교가 어렵다.” 아.. ‘OOP식’으로 매일 코딩을 한다고해서, 할 줄 아는 것이 OOP 밖에 없다고 내가 OOP를 하고 있는거구나 라고 여겼었던 내 무의식적인 생각이 뿌리부터 흔들렸다. 그래서 다시 기본부터 잘해야겠다고 생각했고 이 글은 지난 몇 개월 간 습득한 지식의 요약이다.

스위프트와 SOLID

SOLID는 로버트 마틴이라는 사람이 명명한 객체 지향 프로그램 및 설계의 기본 원칙 다섯 개이다. 스위프트는 멀티패러다임 언어이지만 Foundation, UIKit 등 코코아 터치 프레임워크들은 기본적으로 OOP에 근간을 두고 있다. SOLID 원칙들과 iOS를 연관지어서 살펴보니 내가 쓰고 있는 많은 클래스들이 왜 그런식으로 만들어졌는지 알 수 있었고, 또 내가 어떻게 이 원칙들을 녹여낼 수 있는지 좀 더 와닿게 이해할 수 있었다. 본문에서는 각 원칙에 따라 만들어진(혹은 위반하고 있는) iOS 프레임워크의 예시와 함께 이 원칙을 잘 지키기 위해서 어떻게 코딩을 해야하는지, 특히 이 원칙을 어긴 것은 어떤 형태인지 중점적으로 정리했다. 앞서 말했듯, 처음부터 코드를 잘 짤 순 없으니 최소한 잘못된 코드가 어떤건지는 알고 피해가자는 컨셉이다. 그러면 어려운 이야기를 전부 이해하지 못했더라도 어떻게든 그 방향으로는 나아갈 수 있다고 생각하기 때문이다.

다만 이 글에서는 마지막 두 원칙을 제외한 SOL(쏠?)까지만을 다룬다. 그 이유는 여러가지가 있는데, 아직 뒷 부분은 설명을 할 수 있을 만큼 이해를 다 하지 못했고 또 ID는 이 글의 컨셉에 맞춰 해당 원칙이 위반된 상황을 판단하려고 하는 것이 훨씬 어렵고 미묘하기 때문이다. 하지만 SOL만으로도 내 코드의 개선 사항을 수두룩 빡빡하게 찾는데는 전혀 부족함이 없었고 끝이 안보인다!

Single Responsibility Principle

단일책임 원칙은 모든 클래스는 하나의 책임만 가져야 한다는 원칙이다. 로버트 마틴은 책임을 변경하려는 이유로 정의했다. SRP의 가장 대표적인 위반 사례는 Massive View Controller 현상이다. 이런 MVC의 문제를 해결하기 위해 여러 다른 패턴들이 생겨나고 시도되고 있는데, 그것들의 공통점은 너무 많은 역할을 하고 있는 뷰컨트롤러를 쪼개서 단일 책임만을 가지는 여러 클래스를 만들려고 하는 것이다.

SRP를 지키기 위해 당장 시도해볼 수 있는 것은 클래스를 작게 만드는 것이다. 클래스가 작다는 것이 SRP의 충분 조건은 아니지만 필요 조건이다. 이 목표를 달성하기 위해서 멘토님이 제시한 10-200룰을 기준으로 코딩해보고 있다. 10-200룰이란 함수는 10줄 이내, 클래스는 200줄 이내로 만드는 것이다. 하지만 이를 엄격하게 지키기는 쉽지 않고, 아직은 이상향으로 여기는 수준이다. 현실적으로는 200줄이 넘어가면 혼자 머리 속에서 경보를 발령하고 더이상 안 늘어나게 노력하거나 리팩토링을 시도한다. 10-200룰은 실제로 시도해보면 정말 많은 고민을 하게하고 나를 불편하게 만드는 규칙이다. 하지만 그런 불편함을 극복해나가면서 배움이 생기는 것 같다.

우선 ‘함수는 10줄’ 룰을 지키려면 함수는 하나의 작업만 해야한다라는 대원칙을 지킬 수 밖에 없고 또 추상화 레벨에 대해 고민을 하게 된다. 추상화 레벨이란 얼만큼의 정보를 해당 함수에서 노출할 것인가 하는 것이다. 신라면 봉지 뒷면에 써있는 조리법을 살펴보자. 물 550ml를 끓인다, 면과 스프, 후레이크를 넣는다, 4분 30초 더 끓인다로 되어있다. 세 단계의 추상화 레벨이 비슷하다. 물을 끓이는 단계는 더 쪼개면 냄비를 꺼낸다, 정수기를 튼다, 물을 담는다, 가스렌지에 올린다, 불을 지핀다 등등이 있을 것이다. 하지만 이걸 라면 끓이는 법에 포함시키진 않는다. 왜냐면 그것들은 물 550ml를 끓인다라는 단계(함수)로 묶어서 표현했기 때문이다. 이런 식으로 작업(함수)을 어느 수준까지 쪼개서 설명(추상화)할 것인지에 대해 고민을 하게 만들어 준다.

‘클래스는 200줄’ 룰을 지키려면 프로퍼티나 함수, 메서드를 그냥 손이 가는 곳 아무데나 만들어서 쓸 수 없다. 꼭 얘가 이 곳에 있어야 하는 이유를 찾아야 한다. 해당 클래스에 있을 이유가 없으면 SRP 위반이다. 가장 쉽게 판단할 수 있는 방법 하나는, 함수나 메서드 내부에서 self의 프로퍼티나 메서드를 얼마나 쓰고 있는지 보는 것이다. 만약 하나도 쓰고 있지 않다면 그 클래스 안에 있을 이유가 전혀 없는 것이다. 또는 self의 호출 빈도가 적을수록 클래스와 연관성이 떨어지는 것이니 나중에 클래스를 리팩토링 하거나 다이어트 시켜야 할 상황이 오면 우선적으로 내쫓을 후보가 되는 것이다.

SRP를 모든 패턴의 시작이라고 한다. SRP를 제대로 지키지 못한 채로 코딩을 하면 그 어떤 패턴을 도입해보려고 해도 잘 안될 가능성이 높다고 한다. 클래스는 하나의 역할만 해야한다는 것, 객체지향을 배우면서 가장 먼저 배우는 원칙임에도 정말 지키기 쉽지 않은 것 같다.

Open-Closed Principle

개방폐쇄 원칙은 확장에는 열려 있으나 변경에는 닫혀있어야 한다는 원칙이다. 열려 있다는 것은 기능 추가나 변경을 할 수 있어야 한다는 것이고 닫혀있다는 것은 기능 추가를 할 때 그 모듈을 쓰고 있는 코드들을 줄줄이 수정하지 않아야 한다는 것이다. OCP 위반의 대표적인 예는 어떤 타입에 대한 반복적인 분기문이다. 즉, 하나의 enum에 대해 여러 군데에서 반복적으로 if/switch문을 쓰고 있다면 고민을 해봐야한다. 왜냐하면 이런 경우, 기능 추가는 case를 한줄 추가하는 것만큼이나 쉽지만 그렇게 하는 순간 해당 enum을 스위칭하고 있는 모-오든 코드를 찾아서 수정해줘야 한다. exhaustive하게 짰다면 그나마 컴파일러의 도움을 받을 수도 있겠지만 default문을 추가했다면 그것도 여의치 않다.

OCP는 if/switch를 최대한 안 쓰는 방법을 통해 연습할 수 있다. 모든 분기문을 없애는 것은 아니고(불가능하고) enum 같이 타입을 분기하는 지점에 대해서. 처음에 이 얘기를 들었을때는 말도 안된다고 생각했다. if문은 프로그래밍의 가장 기본 아닌가? 어떻게 if/switch 없이 분기를 할 수 있지? 처음엔 너무 어색하고 답답했지만 시도해보면서 어떤식으로 내 코드에 도움이 되는지 조금씩 느끼고 있다. 앞서 언급한 10-200룰과도 상호보완적이다. 분기문을 없애는 것 만으로도 함수 및 클래스의 길이를 많이 줄일 수 있다.

그러면 if/switch문을 대체할 수 있는 방법 두 가지를 소개한다. 첫번째로 protocol(혹은 class)을 만들고 상속받아 쓰는 방법이다. 이 방법이 직접적으로 OCP를 지키는 구조다. 더 자세한 내용은 이 블로그에 있는 내용을 참고하면 되겠다. 또 한가지 간단한 방법은 딕셔너리를 활용하는 것이다. 다만 이 방법은 OCP를 지키는 구조는 아니다. 얘는 default절이 있는 switch문과 동일한 약점이 있기 때문에 case가 자주 변경될 것 같을 때는 피해야하고, 분기문을 없애고 싶을 때 제한적으로 사용하면 좋을 것 같다.

기존 switch문 코드

switch reason {
  case .initializing:
    self.instructionMessage = "Move your phone".localized
  case .excessiveMotion:
    self.instructionMessage = "Slow down".localized
  case .insufficientFeatures:
    self.instructionMessage = "Keep moving your phone".localized
  case .relocalizing:
    self.instructionMessage = "Move your phone".localized
}

딕셔너리 활용하여 분기문을 없앤 코드

//적절한 곳에 딕셔너리 생성
let trackingStateMessages: [TrackingState.Reason : String] 
                         = [.initializing.        : "Move your phone".localized,
                            .excessiveMotion      : "Slow down".localized,
                            .insufficientFeatures : "Keep moving your phone".localized,
                            .relocalizing         : "Move your phone".localized]

//switch문 대체
self.instructionMessage = trackingStateMessages[reason]

Liskov Subtitution Principle

마지막(?)으로 리스코프 치환 원칙은 상위 타입(수퍼클래스)을 하위 타입(서브클래스)의 인스턴스로 바꿔도 프로그램의 동작을 해치지 않아야 한다는 원칙이다. 정의는 어렵지만 이를 지키기 위한 방법은 의외로 간단하다. 자식이 부모의 동작을 제한해서는 안된다. 전형적인 위반 사례로는 직사각형을 상속받아서 만든 정사각형 클래스를 생각하면 된다. 정사각형은 너비와 높이가 같아야 하기 때문에 너비와 높이를 자유롭게 바꿀 수 있는 직사각형 부모 클래스의 동작을 제한해야만 원하는 동작을 만들 수 있다. 이런 경우가 LSP의 위반이다. LSP를 지키기 위해서는 직사각형이 정사각형을 상속받거나, 아니면 너비와 높이를 아예 let으로 만들어버리면 된다. (값을 바꾸는 동작 자체를 없애버리면 제한할 동작이 없기 때문에)

iOS 프레임워크에서도 LSP 위반 사례를 찾아 볼 수 있는데,

var label = UILabel(frame: .zero)
var button = UIButton(frame: .zero)
var segmentedControl = UISegmentedControl(frame: .zero)
var textField = UITextField(frame: .zero)
var slider = UISlider(frame: .zero)
var switchButton = UISwitch(frame: .zero)
var activityIndicator = UIActivityIndicatorView(frame: .zero)
var progressView = UIProgressView(frame: .zero)
var stepper = UIStepper(frame: .zero)
var imageView = UIImageView(frame: .zero)

let views: [UIView] = [...] //위 뷰들을 UIView 어레이에 저장

views.forEach { $0.frame.size.height = 30 } //뷰들의 height를 30으로 변경

let height = views.reduce(0) { $0 + $1.frame.height } 
print(height) //과연 결과는?

10개의 UIView subclass들의 높이를 30으로 바꿨으니 300이 되길 예상할수도 있지만 실제 결과는 272다. 왜냐하면 일부 뷰들은 intrinsicSize를 마음대로 바꿀수 없게 되어 있기 때문이다. 이렇게 UIView(부모 타입)의 동작(높이를 바꾸는 것)을 제한하는 일부 뷰들(UIProgressView, UISwitch 등)이 바로 LSP 위반이다.

또한 iOS 개발을 하다보면 아래의 코드를 본 적이 있고 만들어낸 적도 있을 것이다. 이처럼 부모의 함수를 오버라이드해서 퇴화시켜 버리는 함수는 만들면 LSP 위반이다.

required init?(coder aDecoder: NSCoder) {
  fatalError("init(coder:) has not been implemented")
}

LSP를 절대 어기지 않고 프로그래밍을 하는 것은 어렵다. 하지만 너무 많은 곳에서 LSP를 어긴다면 문제가 생긴다. 상위 클래스를 기준으로 코딩할 수 없어지고, 그렇게되면 상속 자체가 의미가 없어지고 OCP조차 지킬수 없게 된다. 예를 들어 UITableView는 UITableViewCell을 기준으로 만들어져 있기 때문에 우리가 만든 커스텀 셀이 LSP를 지키지 않는다면 테이블뷰도 제대로 동작하지 않을 것이다. 또한 protocol를 만드는 이유도 추상화된 인터페이스를 기준으로 코드를 작성하기 위해서인데 상속받은 타입이 protocol의 메서드를 퇴화시켜버리면 프로토콜을 기준으로 작성된 코드들이 줄줄이 망가질 수 밖에 없다.

상속은 객체 지향 프로그래밍의 중요한 부분이고 잘 쓰면 유용하지만 잘못된 상속을 만들면 문제가 발생할 수 있다. 반면 때에 따라서는 LSP를 위반함으로써 편리함과 단순함을 얻게 될 수도 있다. 결론적으로, LSP를 모든 곳에서 지키려고만 하면 비효율적이고 너무 자주 위반하면 예상하지 못한 결과들 때문에 안정성을 해치게 된다. 그러므로 상속을 만들때는 LSP 위반인지 아닌지를 숙지하고 사용하는 것이 좋겠다.

결론

이 글의 목표는 SOLID 대원칙을 완벽히 이해하려는 것이 아니다. 게다가 모든 원칙을 철저히 다 지키는 코드만 짜려고 하는 것은 비효율적이라고 볼 수도 있다. 예를 들어 모든 것을 OCP로만 짜려고 하면 너어무 불편하다. 분기문을 쓰지 않으려고 매번 프로토콜을 만들고 상속을 해야한다면 개발하는 시간도 오래걸리고 불필요한 복잡성만 증가하는 꼴이다. 따라서 적절한 트레이드 오프가 필요하다. 다섯 가지 원칙 중에서 SRP와 OCP는 이해는 되지만 가장 지키기 어렵고 끊임없이 노력해야하는 부분인 것 같다. 무엇이 정답인지 모르고 헷갈릴때 확실히 정답이 아닌 것을 지워내는 것처럼, 위 원칙을 어기는 코드 습관을 파악해서 일단 그것부터 제거하고 개선해보려고 한다.

끝.

같이 보면 좋은 자료(코드 예제): OOD Principles in Swift, Design Patterns in Swift

이 글의 초안을 읽어준 김형중, 김찬희, 김창기님에게 고마움을 전합니다.

Tags: Objected Oriented Programming, SOLID, Swift, iOS  

Object-oriented Programming Seen Through Swift: Coding Habits to Avoid

Background

Someone who majored in computer and painting and was quite successful said that programmers have more in common with painters than with scientists. Indeed, as I try to do better in programming, it feels more like a sport or art that requires skills built through experience rather than science.

Just as the ability to recognize good paintings and painting well are different, the ability to judge whether a code is good or bad by looking at it and the skills to build good codes, unfortunately, don’t come together. As I see, build, and maintain a lot of codes built by others, it seems like I’ve been equipped with a sense that can judge codes a little through experience. But as I start typing on the keyboard, I face frustration. I get a strong feeling that I shouldn’t build it this way from instinct. Experience? But I don’t have a clear answer to how to build it in a different, better way. When you take a test as a student, there are times when you choose your answer with confidence, but also times when you eliminate the ones you know are not correct and choose your answer from the remaining options. If you exclude the ones that are clearly not the answer, you can spend more time thinking about the remaining options, and there’s a higher probability that you’ll choose the right answer. Coming back to programming, I thought if I knew what a code with a wrong pattern or that is not good for maintenance looks like and avoid it, won’t I be able to use better codes?

Last year, I joined a study group for functional Swift programming in the company. On the first day, we invited a mentor to teach a class in the company about functional Swift programming to get advice for the direction of our study. There, the mentor said something that became the direction of my programming study for the next whole year.

To see how well functional programming is, you need to compare a well-written functional code with a well-written object-oriented code, but since I can’t say I write good object-oriented code it’s difficult to compare them. “Oh.. my unconscious thoughts that thought I was doing OOP because that was the only thing I knew how to do and I code every day in the ‘OOP style’ started to shake from the roots. So I thought I should do well from the basics again, and this is a summary of knowledge I have acquired over the past few months.

Swift and SOLID

SOLID is the 5 fundamental principles of object-oriented programs and design, named by Robert Martin. Swift is a multi-paradigm language, but Cocoa Touch frameworks such as Foundation, UIKit, etc. are fundamentally based on OOP. Looking at the SOLID principles and iOS in relation to each other, I could understand why the several classes that I’m using were created that way, and how I could apply these principles. In the text, I focused on how to code in order to keep these principles, and what it looks like to violate these rules, along with examples of the iOS framework for each rule it has been created for (or has violated). As I said earlier, the concept is to at least know what a wrong code looks like, and avoid it, because you can’t write good code from the beginning. Then I believe you can somehow move toward that direction even if you didn’t understand all the difficult things.

However, this text only deals with SOL, excluding the last two principles. There are several reasons for this. Because I did not understand the latter part enough to explain them, and also because it is much more difficult to judge the situations where these principles are violated for the ID part according to the concept of this text. However, it was enough with only SOL to find countless improvements for my codes and I still can’t see the end!

Single Responsibility Principle

The Single Responsibility Principle is a principle that all classes should have only one responsibility. Robert Martin defined it as a reason to change responsibility. The most significant violation of SRP is the Massive View Controller phenomenon. Many different patterns are being created and tried in order to solve the MVC problem, but what they have in common is that they try to split up the view controller, which has too many roles, and creates multiple classes that have only a single responsibility.

What we can try right now to keep the SRP is to create small classes. Small classes are not sufficient conditions of the SRP but they are essential conditions. To achieve this goal, I’m coding based on the 10-200 rule as suggested by the mentor. The 10-200 rule is to write functions within 10 lines, and classes within 200 lines. But it’s difficult to strictly go by this, and this is still considered an ideal. In reality, if it goes over 200 lines, you alert yourself inside your head and try not to make it longer or try refactoring. If you actually try the 10-200 rule, it makes you think a lot and makes you uncomfortable. However, it seems that learning comes from overcoming these inconveniences.

First, to keep the ‘10 lines for functions’ rule, you’d have to abide by the principle that a function needs to have only one task, and then you start to think about the level of abstraction. The level of abstraction means how much information are you willing to expose in that function. Let’s look at the cooking directions on the back of the Shin Ramyun bag. Boil 550ml of water, and add noodles, soup base, and vegetable mix. It says to boil for another 4 minutes and 30 seconds. The 3 steps of the abstraction level are similar. If you break down the water boiling step, you can say take a pot, turn on the water purifier, get water, put the pot on the stove, turn on the fire, etc. But these are not included in the cooking directions. This is because they are expressed in one step(function) where 500ml of water is boiled. This way, it makes me think about how far I should divide the task(function) and explain(abstract) it.

To keep the ‘200 lines for the class rule, you can’t just create and use properties, functions, and methods anywhere you want. You need to find a reason why they should be there. If you can’t find a reason for them to be in that class, it’s a violation of the SRP. The easiest way to judge is to see how many of the properties or methods of self are being used inside the function or the method. If there aren’t any being used, there is no reason for them to be in that class. Or, the less frequent the calls are made for self, it means it’s less related to the class, so if you have to refactor or take the class through a diet, this will be the first candidate to be kicked out.

SRP is called the beginning of all patterns. If you code without properly complying with the SRP, it’s highly probable that it will not work with any patterns. Classes need to have only one role, it’s the very first principle learned in object-oriented programming, but so hard to comply with.

Open-Closed Principle

The Open-Closed Principle is a principle that should be open to scalability but closed to modifications. Being open means that you can add features or make modifications, and being closed means that you must not modify the codes using the module line per line when adding a feature. A typical example of a violation of OCP is repetitive branching statements for a certain type. That means you need to think about it if you’re repeatedly using if/switch statements in several places for one enum. Because in cases like this, adding a feature is as easy as adding one line of case, but you need to modify every single code that is switching the enum once you add the feature. If it was planned in an exhaustive manner, you can get help from the compiler, but if you added a default statement, this won’t be possible.

OCP can be practiced by not using if/switch as much as possible. Not eliminating all branching statements(impossible) but for points like an enum that branch a type. When I first heard about this, I thought it was ridiculous. Isn’t the if statement the most basic of programming.? How can you branch without an if/switch? It was so awkward and frustrating at first, but as I tried it I’m starting to feel how it is helpful in my codes little by little. It is also complementary to the aforementioned 10-200 rule. Eliminating branching statements alone can greatly reduce the length of functions and classes.

Let me introduce to you two ways to replace if/switch statements. First is creating a protocol (or a class), inheriting and using them. This is a method that directly abides by the OCP. Refer to the contents of this blog for further details. Another simple method is using the dictionary.

But this method does not directly abide by the OCP. Since this has the same weakness as the switch statement which has a default clause, avoid it when it’s likely to have frequent changes, and use it on a limited basis when you want to eliminate branching statements.

Original switch statement code

switch reason {
  case .initializing:
    self.instructionMessage = "Move your phone".localized
  case .excessiveMotion:
    self.instructionMessage = "Slow down".localized
  case .insufficientFeatures:
    self.instructionMessage = "Keep moving your phone".localized
  case .relocalizing:
    self.instructionMessage = "Move your phone".localized
}

Code that deleted branching statements using the dictionary

//dictionary generated in appropriate spots
let trackingStateMessages: [TrackingState.Reason : String] 
                         = [.initializing.        : "Move your phone".localized,
                            .excessiveMotion      : "Slow down".localized,
                            .insufficientFeatures : "Keep moving your phone".localized,
                            .relocalizing         : "Move your phone".localized]

//switch statement replaced대체
self.instructionMessage = trackingStateMessages[reason]

Liskov Subtitution Principle

Lastly, the Liskov Substitution Principle is a principle where changing the upper type (superclass) to a lower type (subclass) instance should not harm the program’s action. The definition is difficult but the way to follow it is surprisingly simple. The child should not restrict the action of its parents. A typical violation would be a square class created by inheriting a rectangle. Since a square has the same width and height, it can only create the desired action only by limiting the action of the rectangle parent class which can freely change its width and height. This is a violation of the LSP. To abide by the LSP, the rectangle can inherit a square, or make the width and height as a let. (If the action of changing the value is removed, there is no action to limit)

An LSP violation case can be also found within the iOS framework.

var label = UILabel(frame: .zero)
var button = UIButton(frame: .zero)
var segmentedControl = UISegmentedControl(frame: .zero)
var textField = UITextField(frame: .zero)
var slider = UISlider(frame: .zero)
var switchButton = UISwitch(frame: .zero)
var activityIndicator = UIActivityIndicatorView(frame: .zero)
var progressView = UIProgressView(frame: .zero)
var stepper = UIStepper(frame: .zero)
var imageView = UIImageView(frame: .zero)

let views: [UIView] = [...] //Save the above views in UIView array

views.forEach { $0.frame.size.height = 30 } //Change the height for the views to 30

let height = views.reduce(0) { $0 + $1.frame.height } 
print(height) //The result?

Since the height of the 10 UIView subclasses has been changed to 30 you may expect it to become 300 but the actual result is 272. This is because the intrinsicSize of some views cannot be changed. These views (UIProgressView, UISwitch, etc) that limit the action(changing the height) of UIViews(parent type) are LSP violations.

As you’re in iOS development, you may have seen the code below and have created it as well. If a function that overrides and degenerates the parent’s function is created, it’s a violation of the LSP.

required init?(coder aDecoder: NSCoder) {
  fatalError("init(coder:) has not been implemented")
}

It’s difficult to program without ever violating the LSP. But if you violate the LSP in too many places, there will be a problem. Code can’t be based on the upper class, and if this happens, the inheritance will be meaningless and the OCP won’t be followed as well. For example, UITableView is created based on the UITableViewCell, so if the custom cell isn’t following the LSP, the table view won’t work properly as well. Also, the reason for creating a protocol is to write codes based on an abstract interface, but if the inherited type degenerates the protocol’s method, the codes written based on the protocol will inevitably break one after another.

Inheritance is an important part of object-oriented programming and is useful if applied well, but could cause problems if wrong inheritances are created. On the other hand, convenience and simplicity may be obtained by violating the LSP. In conclusion, it is inefficient to follow the LSP everywhere, but it harms the stability if the LSP is violated too often because of unexpected consequences. Therefore, know whether it is violating the LSP or not when creating an inheritance.

Conclusion

The purpose of this text is not to perfectly understand the SOLID principles. In addition, trying to write codes that adhere to all principles can be seen as inefficient. For example, it’s very inconvenient if you try to create everything according to the OCP.

If you need to create protocols and inherit every time to avoid using branching statements, it only increases unnecessary complexity and increases development time. Therefore, a proper trade-off is needed. Among the five principles, the SRP and the OCP is understandable, but the hardest to adhere to and need constant hard work.

Just like eliminating the wrong answer when you’re not sure of the correct one, figure out the habit which violates the above principles, and eliminate it to improve.

Additional references (coding example): OOD Principles in Swift, Design Patterns in Swift

이 글의 초안을 읽어준 김형중, 김찬희, 김창기님에게 고마움을 전합니다.

Tags: Objected Oriented Programming, SOLID, Swift, iOS  

스위프트 API 디자인 가이드라인 초초초-요약본

https://swift.org 에서 스위프트 API 디자인 가이드라인의 존재 이유를 다음과 같이 설명하고 있다.

스위프트 3.0의 출시 목표는 프로그래머에게 일관된 사용자 경험을 제공하기 위한 표준을 세우는 것이고, API에 등장하는 이름과 표현법을 통해 이를 달성하고 있다. 이 가이드라인은 여러분의 코드가 더 큰 스위프트 생태계의 일부인 것처럼 느껴지게 하는 방법을 설명하고 있다.

‘프로그래머의 사용자 경험’이라는 단어를 썼다는 것이 어색하기도 하고 놀랍기도 하고 한편으론 정말 애플스럽다. 그런데 왠지 익숙한 표현법 아닌가?

애플 플랫폼과 매끄럽게 어우러지는 훌륭한 앱을 디자인 하기 위한 … (후략)

잘 알려진 아이콘, 텍스트 스타일, 통일성 있는 용어, 시스템이 제공하는 인터페이스 요소들을 활용하여 유저에게 일관된 경험을 제공 … (후략)

Human Interface Guidelines에 등장하는 문장들이다. 최종 사용자들에게 좋은 사용자경험을 주는 애플 플랫폼 앱을 만들기 위해 따라야하는 것이 ‘휴먼 인터페이스 가이드라인’이라면, 스위프트 개발자에게 좋은 사용자경험을 주는 코드를 만들기 위해 따라야하는 것은 ‘스위프트 API 디자인 가이드라인’인 셈이다.

가이드라인에는 꽤 많은 내용이 담겨 있다. 그렇다고 문서가 눈에 쏙쏙 들어오는 편도 아니고 누구나 들었을때 ‘음 그렇지’라고 고개가 끄덕여지는 그런 내용도 아니다. 이런 이유로 가이드라인에 무슨 내용이 담겨 있는지, 어디서부터 시작해야될지 잘 모르겠는 분들이 많은 것 같다. 약 일 년 정도 API 디자인 가이드라인과 애플 개발자 문서를 옆구리에 끼고, 아니 모니터에 띄워놓고 개발했던 경험을 토대로 내 마음속 중요도 순으로 추리고 요약해봤다. 그룹핑도 적용 상황을 기준으로 내 마음대로 재구성했다.

⚠️ 가이드라인의 모든 내용을 담고 있지 않다. 한 20% 정도? 하지만 적용되는 빈도로 따지면 아래 내용들을 합치면 전체 케이스의 50%는 넘는 것 같다.

함수 및 프로퍼티 이름

  • 메서드나 함수는 사이드 이펙트의 유무에 따라 이름 짓는다.

    • 사이드 이펙트 없는 함수는 명사구로 읽혀야한다.

    ⛔️ 잘못된 예

      x.getDistance(to: y) //"y까지의 거리를 가져와라"
    

    ✅ 올바른 예

      x.distance(to: y) //"y까지의 거리"
    

    사이드 이펙트 없이 값을 리턴하는 메서드(일명 “getter”)에 get을 붙이는 것이 대표적인 가이드라인 위배이다. 코코아터치 프레임워크에서 get, fetch, request로 시작하는 메서드는 전부 completion handler를 받는 비동기 작업 뿐이다.

    참고 : Swift 개발자처럼 변수 이름 짓기

    • 사이드 이펙트가 있는 함수는 명령형 동사구로 읽혀야한다. (명령형이라는 것은 동사원형을 쓴다는 것이다.)
      print(x)
      x.sort()
      x.append(y)
    

    위 함수들은 모두 함수 스코프 밖에까지 영향을 미친다(사이드 이펙트). print는 콘솔에 찍히고 sort와 append는 x의 값을 바꿔버린다.

  • mutating과 nonmutating 메서드 쌍은 일관성있게 이름 짓는다. 보통 mutating 함수는 명령형 동사로 쓰고 nonmutating은 “ed”나 “ing”를 뒤에 붙여서 사용한다.

Mutating Nonmutating
x.sort() z = x.sorted()
x.append(y) z = x.appending(y)
  • Bool 메서드나 프로퍼티 이름은 인스턴스에 대한 평서문처럼 읽혀야한다.

    e.g. x.isEmpty, line1.intersects(line2)

    참고 : Bool 변수 이름 제대로 짓기 위한 최소한의 영어 문법

  • 대소문자 규칙을 따른다.

    • 타입이나 프로토콜은 UpperCamelCase를 따르고 그 외에는 lowerCamelCase를 따른다.
    • 대문자 약어는 낙타표기법에 따라 전체 대문자 혹은 소문자로 통일한다.
      var utf8Bytes: [UTF8.CodeUnit]
      var isRepresentableAsASCII = true
      var userSMTPServer: SecureSMTPServer
    

    우리가 많이 쓰는 대문자 약어 중에 URL이 이 조건에 해당될 것이다. 규칙을 따르려면 아래처럼 사용해야 한다.

      let urlString = "https://soojin.ro"
      let blogURL = URL(string: urlString)
    

파라미터명(Parameter Names)

파라미터 명명 규칙은 좀 더 세부적으로 생성자일 때와 메서드일 때로 나뉜다.

생성자

  • 생성자의 첫번째 파라미터명은 타입 이름과 구(phrase)를 이뤄서는 안된다. 다시 말해 생성자의 파라미터명에는 아래처럼 전치사나 접속사 등을 써서 문장처럼 이어지게 만들지 말라는 말이다.

    ⛔️ 잘못된 예

      let foreground = Color(havingRGBValuesRed: 32, green: 64, andBlue: 128)
      let newPart = factory.makeWidget(havingGearCount: 42, andSpindleCount: 14)
      let ref = Link(to: destination)
    

    대신 아래처럼 having, and, to, with 등의 단어들을 제거한다.

    ✅ 올바른 예

      let foreground = Color(red: 32, green: 64, blue: 128)
      let newPart = factory.makeWidget(gears: 42, spindles: 14)
      let ref = Link(target: destination)
    
  • 무손실 타입 변환(value preserving type conversion)을 하는 생성자는 첫번째 파라미터명을 생략한다. 추가적으로, 손실이 일어나는 타입 변환이라면 어떤 손실이 일어나는지 명시해주는 것을 추천한다.

    ✅ 올바른 예

      Int64(someUInt32)
      String(someNumber)
      String(someNumber, radix: 16)
      UInt32(truncating: someUInt64) //64비트 -> 32비트로 손실 발생 명시
    

즉, 무손실 타입 변환이 일어나는 생성자가 아니라면 첫번째 파라미터에는 이름을 꼭 부여한다.

메서드 및 함수

  • 첫번째 파라미터가 전치사(to, in, at, from, with 등등)구의 일부라면 파라미터명을 부여한다.

    ✅ 올바른 예

      x.removeBoxes(havingLength: 12)
      employees.remove(at: x)
    
  • 그렇지 않고 첫번째 파라미터가 자연스럽게 이어지면 생략한다.

    ✅ 올바른 예

      x.addSubview(y)
      allViews.remove(cancelButton)
      x.insert(y, at: z)
      x.append(y)
    
  • 그 외 모든 경우에는 파라미터 이름을 부여한다.

끝.

가이드라인 원본

가이드라인 번역본

Tags: Swift API Design Guidelines, abriged