티스토리 뷰
[iOS] Enums as configuration: the anti-pattern - 번역 / Enum을 이용한 설정
Kim_Baechu 2021. 8. 24. 15:17원본 출처 : https://www.jessesquires.com/blog/2016/07/31/enums-as-configs/
Objective-C(혹은 Swift)에서 클래스의 configurations을 위해 가장 많이 사용하는 패턴 중에 하나는 enum을 사용하는 것입니다. 예를 들어, 특정 방식으로 스타일을 지정하기 위해 열거형을 UIView에 전달하는 것입니다. 이 글에서는 왜 이것이 안티 패턴이라 생각하는지 설명하고, 더 강력하고 모듈적이며 확장 가능한 방법을 제공합니다.
설정 문제
해결하고자 하는 문제를 정의해봅시다. 몇 가지 다른 컨텍스트에서 사용되는 클래스가 있다고 가정해봅시다. 여기서는 각자 약간식 다른 설정이 필요합니다. 각각의 컨텍스트마다 클래스의 동작이 달라져야 한다는 것입니다. 이 클래스는 view, 클라이언트 네트워킹 또는 다른 것들을 나타낼 수 있습니다. 인스턴스화 되었을 때, 유저는 클래스의 구현 세부 사항을 전혀 모르거나 수정하지 않고 클래스의 동작을 현재 컨텍스트에 맞게 명시하거나 수정해야합니다.
UITableViewCell을 예로 들어봅시다. 이미지, 라벨, 악세서리뷰를 가진 셀이 있다고 가정해봅시다. 이 레이아웃은 매우 일반적이고 우리는 같은 셀을 앱의 다른 뷰에서 재사용하고 싶습니다. 로그인 뷰는 특정 색상과 폰트 등 스타일을 만든다고 합시다. 이 셀을 세팅에서 다른 스타일로 재사용하고 싶습니다. 셀에 사용되는 모든 뷰는 기본셀의 레이아웃을 가지지만 다른 시각적인 효과를 줘야합니다.
Using an enum for configuration
enum CellStyle {
case login
case profile
case settings
}
class CommonTableCell: UITableViewCell {
var style: CellStyle {
didSet {
configureStyle()
}
}
// ...
func configureStyle() {
switch cellStyle {
case .login:
// configure style for login view
textLabel?.textColor = .red()
textLabel?.font = .preferredFont(forTextStyle: UIFontTextStyleBody)
detailTextLabel?.textColor = .blue()
detailTextLabel?.font = .preferredFont(forTextStyle: UIFontTextStyleTitle3)
accessoryView = UIImageView(image: UIImage(named: "chevron"))
case .settings:
// configure style for settings view
textLabel?.textColor = .purple()
textLabel?.font = .preferredFont(forTextStyle: UIFontTextStyleTitle1)
detailTextLabel?.textColor = .green()
detailTextLabel?.font = .preferredFont(forTextStyle: UIFontTextStyleCaption1)
accessoryView = UIImageView(image: UIImage(named: "checkmark"))
case .profile:
// configure style for profile view
// ...
}
}
// ...
}
class SettingsViewController: UITableViewController {
// ...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// create and configure cell
cell.style = .settings
return cell
}
// ...
}
UITalbeViewCell과 UITableViewController를 상속하고 스타일을 enum으로 정했습니다. 각 뷰컨트롤러에서 셀을 만들고 정할때, 적당한 스타일을 설정하면 끝입니다. 쉽네요
왜 enum 설정이 나쁜가
라이브러리나 프레임워크를 작성할 때, " 열거형으로 설정" 패턴이 클라이언트에게 유연한 것으로 촉진됩니다.(장려된다) - "이 모든 설정 옵션을 당신에게 제공합니다~". 이것은 분명 좋게 의도된 디자인이지만, 속지마세요. 진짜 모듈화된, 적응형 API를 제공하는 대신 불필요하게 제한적이고 유지보수가 성가시며 오류발생 가능성이 높습니다.
열거형은 정의에 따르면(유한한 값을 가짐) 이것은 유연하지 않기 때문에 "당신이 원하는 어떤 스타일이든 설정"한다는 것이 이 다지인이 유연하다고 말하는건 아이러니입니다. 만약에 이것이 당신의 앱의 일부인 경우, 새 컨텍스트가 필요할때마다 새 case 추가하고 switch문을 업데이트 해야합니다.
라이브러리라면, 클라이언트가 새로운 케이스를 더하거나 새로운 스타일을 정의할 수 없습니다. 클라이언트는 새로운 스타일을 추가해달라고 요청하거나 그 구현에대해 pull request를 올려야합니다. 게다가 새로운 열거형 값을 추가하는 것은 라이브러리 변경 사항입니다. 만약에 클라이언트가 이 열거형을 그들의 앱의 다른 부분에서 switch문을 사용하고 있다면 Swift에서는 모든 switch문을 사용해야하기 때문에 새로운 case를 추가하는 것은 에러가됩니다.
Objective-C에서는 더 심각한데, 여기에는 불완전한 switch문에 대해서 에러나 경고가 없습니다. 그리고 break를 방출하기 쉽고 뜻하지 않게 다음 케이스로 넘겨버리기 쉽습니다.
이 접근 방식은 취약하고 imperative하고 중복코드를 많이 만듭니다.
설정 모델
단순히 열거형만 드러내는 것으로 일어나는 일을 애매하게 만드는 것이 아니라, inversion of control(제어 역전)이라고 알려진 기술을 이용해서 API를 열 수 있습니다. 우리의 예를 들어서, Cell 스타일을 나타내는 완전히 새로운 모델을 만드는건 어떨까요? 다음을 참고하세요.
struct CellStyle {
let labelColor: UIColor
let labelFont: UIFont
let detailColor: UIColor
let detailFont: UIFont
let accessory: UIImage
}
class CommonTableCell: UITableViewCell {
// ...
func apply(style: CellStyle) {
textLabel?.textColor = style.labelColor
textLabel?.font = style.labelFont
detailTextLabel?.textColor = style.detailColor
detailTextLabel?.font = style.detailFont
accessoryView = UIImageView(image: style.accessory)
}
// ...
}
enum 대신에 우리 셀 스타일을 나타내는 구조체를 만들 수 있습니다. 스타일에 대한 모든 속성을 명확하게 정의할 뿐만 아니라 이제 이 값을 셀에 보다 절차적이고 선언적인 방법으로 map할 수 있습니다. 다른 시나리오에서 클래스의 지정 이니셜라이저에 설정을 전달할 수 있습니다.
우리는 이 클래스에서 수많은 코드와 복잡성을 제거하여 더 작고, 더 쉽게 읽히고, 더 쉽게 추론할 수 있게되었습니다. 스타일 속성에서 셀 속성으로 잘 정의된 일대일 매핑이 있습니다. 우리는 더 이상 거대한 switch 문의 유지에 대한 부담도, 오류가 발생하는 경향도 가지고 있지 않습니다. 마지막으로, 클라이언트는 스타일을 무한정 많이 표현할 수 있을 뿐만 아니라 새로운 스타일을 도입하면서 더 이상 원래 클래스에 변경을 야기하지 않습니다. 또한 라이브러리를 작성할 경우에도 breaking change를 야기하지 않습니다.
기본값 제공과 사용자 정의 값
이 디자인이 강력한 다른 이유는 default값을 제공하고, non-breaking방법으로 새로운 스타일을 추가할 수 있기 때문입니다. 기본 파라미터 값, extensions 그리고 타입추론에서 스위프트의 특징이 여기서 빛을 발합니다. 스위프트 언어는 이런 타입의 패턴이 좋지만 Objective-C서툴고 지루하며 장황하게 느껴집니다.
스위프트에서는 이니셜라이저에서 기본값을 제공할 수 있습니다.
struct CellStyle {
let labelColor: UIColor
let labelFont: UIFont
let detailColor: UIColor
let detailFont: UIFont
let accessory: UIImage
init(labelColor: UIColor = .black(),
labelFont: UIFont = .preferredFont(forTextStyle: UIFontTextStyleTitle1),
detailColor: UIColor = .lightGray(),
detailFont: UIFont = .preferredFont(forTextStyle: UIFontTextStyleCaption1),
accessory: UIImage) {
self.labelColor = labelColor
self.labelFont = labelFont
self.detailColor = detailColor
self.detailFont = detailFont
self.accessory = accessory
}
}
앞에서 enum 을 사용하여 정의한 라이브러리 제공 스타일에 대해서, extension으로 프로퍼티를 정의할 수 있습니다.
extension CellStyle {
static var settings: CellStyle {
return CellStyle(labelColor: .purple(),
labelFont: .preferredFont(forTextStyle: UIFontTextStyleTitle1),
detailColor: .green(),
detailFont: .preferredFont(forTextStyle: UIFontTextStyleCaption1),
accessory: UIImage(named: "checkmark")!)
}
}
// usage:
cell.apply(style: .settings)
Notice the call site can actually remain unchanged due to Swift’s type inference. 전에는 enum 의 .settings값을 참조했지만 지금은 extension에 있는 static var 프로퍼티를 참조합니다. 간결함이나 명확성의 저하 없이 더 모듈화하고 확장 가능한 API를 제공할 수 있습니다.
위에서 언급한 것처럼, 클라이언트는 이제 쉽게 그들의 스타일을 extension에 추가할 수 있습니다. 게다가 몇몇의 기본 프로퍼티를 override하기만 할 수도 있습니다.
extension CellStyle {
static var custom: CellStyle {
// uses default fonts
return CellStyle(labelColor: .blue(),
detailColor: .red(),
accessory: UIImage(named: "action")!)
}
}
동작에 관한 설정
우리의 예제에서 뷰의 스타일링에만 신경썼지만, 일반적인 동작을 모델링하는 데 강력한 패턴임을 반복 강조합니다. 네트워크를 담당하는 클래스를 생각해봅시다. 이 설정은 프로토콜을 명시하고, 실패에 대한 재시도 정책, 캐시 만료 등을 할 수 있습니다. 기존의 개별적 프로퍼티의 장황함을 가지고 있던 곳에, 이제 이러한 프로퍼티를 단일 단위에 번들로 묶고 default 동작을 제공하거나 customization을 제공할 수 있습니다.
실제 예시
생략...
결론
이 글을 통해 SOLID패턴의 O 인 open/closed 규칙을 따르는 것을 보여드렸습니다.
확장에 대해 열려있고, 수정에 대해 닫혀있어야 한다.
enum을 통해 이 원칙을 구현하려는 시도는 클라이언트에게 제한적이고, 오류가 발생하기 쉬우며, 유지보수에 부담이 됩니다. 구성과 동작 객체 또는 data sources 및 delegate을 사용하여 코드를 단순화하고 오류를 제거하며, 명확성과 간결성을 유지하며, 클라이언트를 위한 확장 가능한 모듈식 API를 제공하고, 변경 사항을 위반하지 않도록 할 수 있습니다.
'iOS' 카테고리의 다른 글
[iOS] Tuist 사용법 - 프로젝트 생성 (1) (0) | 2021.09.17 |
---|---|
[iOS] WWDC2017 Engineering for Testability / Testable App Code - 번역 (0) | 2021.08.25 |
[iOS] URL Scheme vs Universal Link 차이 (deep link?) (2) | 2021.08.23 |
[iOS] API Design Guidelines - Naming 애플식 네이밍 (0) | 2021.08.23 |
[iOS] NavigationBar 숨기기, Custom 네비게이션바 (0) | 2021.07.21 |