일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- map
- uitableview
- 애니메이션
- swift documentation
- Clean Code
- 리펙토링
- SWIFT
- rxswift
- Observable
- 클린 코드
- Protocol
- Xcode
- 스위프트
- RxCocoa
- collectionview
- MVVM
- UICollectionView
- UITextView
- HIG
- clean architecture
- ribs
- 리펙터링
- ios
- swiftUI
- Human interface guide
- tableView
- uiscrollview
- Refactoring
- 리팩토링
- combine
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - swift] Clean Code(클린 코드) - 9. 클래스 (SRP, Cohesion) 본문
[iOS - swift] Clean Code(클린 코드) - 9. 클래스 (SRP, Cohesion)
jake-kim 2021. 11. 23. 00:01클린한 클래스 작성하는 방법
* 아래에서 해당 개념 설명 예정
- SRP 준수: 클래스의 변경 이유는 한가지가 되도록 설계
- Cohesion 준수: 인스턴스 변수를 최소화
클래스는 작게 만들 것
- 함수에서의 클린 코드 내용과 같이 클래스 역시도 작아야 가독성, 유지보수에 이점이 있는 코드
- 함수에서는 내용의 길이를 행의 수 20줄도 긴 수치라고 했었지만, 클래스는 맡은 책임의 개수를 보고 판단
- 클래스의 책임의 개수 판단
- 메소드의 개수는 5개 이하가 적정
- 클래스 이름은 해당 클래스 책임을 기술하는 최소의 범위로 작성
(Manaer, Processor가 붙으면 해당 클래스에서 여러 책임을 떠안겼다는 증거)
ex) 책임이 많은 클래스
WRONG
- 메소드의 개수는 5개 이지만, SuperDashboard이름에서 Super라는 작명에 의하여 책임이 너무 많은 점 존재
class SuperDashboard: SomeModule {
func getLastFocusedComponent() -> Component
func setLastFocused()
func getMajorVersionNumber() -> Int
func getMinorVersionNumber() -> Int
func getBuildNumber() -> Int
}
- 단일 책임 원칙 (SRP)
- 변경할 이유가 단 한나뿐이어야 한다는 원칙
- SRP는 `책임`이라는 개념을 정의하며 적절한 클래스 크기를 제시
- 위 SuperDashboard 클래스의 변경할 이유는 2가지 이므로 SRP를 위반 (소프트웨어 버전 추적, 포커스된 컴포넌트 추적)
RIGHT
- `소프트웨어 버전 추적`관련 코드를 제외시켜서, SRP를 준수하도록 설계 (동시에 클래스의 크기도 작아지는 장점이 존재)
- Version 클래스를 별도로 생성
- SRP를 준수하고 있으므로 재사용하기 쉬운 구조로 탄생
class Dashboard: SomeModule {
func getLastFocusedComponent() -> Component
func setLastFocused()
}
class Version {
func getMajorVersionNumber() -> Int
func getMinorVersionNumber() -> Int
func getBuildNumber() -> Int
}
> SRP만 준수해도 클래스를 관리하기 용이하고 추상화 시키는 작업에도 도움
클래스의 성질
- 응집도(Cohesion)
- 응집도의 개념: 클래스에 속한 메소드와 변수가 서로 의존하며 논리적인 단위로 묶인다는 의미
- 응집도의 기준: 인스턴스 변수가 메소드에서 많이 사용
- 클래스는 인스턴스 변수 수가 작아야함
(메소드는 인스턴스 변수를 하나 이상 사용해야 하므로 메소드가 변수를 더 많이 사용할수록 "메소드+클래스" 응집도가 높음)
RIGHT
ex) elements란 인스턴스가 3개의 메소드에서 모두 사용되고 있으므로 응집도가 가장 높은 코드
class Stack {
var elements = [Int]()
func size() -> Int {
return elements.count
}
func push(_ element: Int) {
elements.append(element)
}
func pop() -> Int? {
guard !elements.isEmpty else { return nil }
let element = elements.removeLast()!
return element
}
}
- 응집도를 유지하면 작은 클래스가 여러개가 탄생
ex) 응집도를 유지하여 새로운 클래스로 분리되는 과정
변수 4가지를 사용하는 큰 함수가 존재
> 큰 함수 일부를 함수 하나로 빼내고 싶은 상황
> 빼내려는 코드가 큰 함수에 정의된 변수 4가지를 사용
> 변수 4개를 새 함수의 인수로 넘기지 말고, 클래스의 인스턴스로 지정
> 클래스의 인스턴스가 여러개 생겨났으므로 독자적인 클래스로 분리
SRP를 이용하여 변경하기 쉬운 클래스 만들기
핵심: SRP를 준수하며, 함수의 인수들을 인스턴스로 변경하여 클래스들로 최대한 작게 쪼갤 것
WRONG
ex) SRP 위반 클래스
- SQL문 하나를 수정할 때도 Sql 클래스에 손대야하는 상황
- select문에 내장된 select문을 지원하려면 Sql클래스를 고쳐하는 상황
- selectWithCriteria(criteria:) 이 메소드는 특정 select문을 처리할때만 사용되므로 class이름 Sql에 어울리지 않은 메소드
class Sql {
var table: String
var columns: [Column]
init(table: String, columns: [Column]) {
self.table = table
self.columns = columns
}
func create() -> String { ... }
func insert(table: [Any]) { ... }
func selectAll() { ... }
func findByKey(keyColumn: String, keyValue: String) { ... }
func select(column: Column, pattern: String) { ... }
func select(criteria: Criteria) { ... }
func preparedInsert() { ... }
private func columnList(columns: [Column]) { ... }
private func valuesList(fields: [Any], columns: [Column]) { ... }
private func selectWithCriteria(criteria: Criteria) { ... }
private func placeholderList(columns: [Column]) { ... }
}
RIGHT
- 공개 인터페이스를 각각 Sql클래스에서 파생하는 클래스로 생성
- valueList(fields:columns:)와 같은 비공개 메소드는 해다하는 파생 클로스로 옮긴 형태
- 모든 파생 클래스가 공통적으로 사용하는 비공개 메소드는 Where과 ColumnList라는 두 유틸리티 클래스로 생성
class Sql {
var table: String
var columns: [Column]
init(table: String, columns: [Column]) {
self.table = table
self.columns = columns
}
func create() -> String { ... }
func insert(table: [Any]) { ... }
func selectAll() { ... }
func findByKey(keyColumn: String, keyValue: String) { ... }
func select(column: Column, pattern: String) { ... }
func select(criteria: Criteria) { ... }
func preparedInsert() { ... }
private func columnList(columns: [Column]) { ... }
private func valuesList(fields: [Any], columns: [Column]) { ... }
private func selectWithCriteria(criteria: Criteria) { ... }
private func placeholderList(columns: [Column]) { ... }
}
protocol Sql {
func generate() -> String
}
extension Sql {
func generate() -> String { return ... }
}
// 편의를 위해서 생성자 생략
class CresteSql: Sql {
let table: String
let columns: [Column]
}
class SelectSql: Sql {
let table: String
let columns: [Column]
}
class InsertSql: Sql {
let table: String
let columns: [Column]
let fields: [Any]
}
class SelectWithCriteriaSql: Sql {
let table: String
let columns: [Column]
let criteria: Criteria
}
class SelectWithMatchSql: Sql {
let columns: [Column]
let column: Column
let pattern: String
}
class FindByKeySql: Sql {
let table: String
let columns: [Column]
let keyValue: String
let keyColumn: String
}
class PreparedInsertSql: Sql {
let table: String
let columns: [Column]
}
class Where {
}
class ColumnList {
}
> 이점
- 각 클래스는 극도로 단순하므로 보자마자 순식간에 이해되는 형태
- 테스트 관점에서 모든 논리를 구석구석 증명하기에 용이하기 쉬운 형태
- 확장에 개방적이고 수정에 폐쇄적이어야 한다는 원칙 OCP(Open Close Principle) 준수 (새 기능 추가 시 기존 코드 변경 x)
DIP를 준수하여 결합도 낮추기
- DIP(Dependencies Interface Principle): 사용하는 쪽에서 구현체가 아닌 추상화(protocol)에 의존해야하는 원칙이므로, 클래스를 만들땐 protocol을 만든 후 구현을 지향
- 구현체에 의존하게된다면, 테스트가 어렵지만 인터페이스에 의존하면, 구현체를 언제든 변경하여 테스트하기 용이한 형태
- 구현내용이 바뀌어도 사용하는 쪽에서는 인터페이스인 protocol에만 의존하고 있으므로 구현체의 내용이 언제든 바뀌어도 영향받지 않은 유연한 구조
- 구현체의 내용이 숨겨짐으로서 사용하는쪽에서 사용하는데에 필요한 정보에만 의존이 가능
* 참고: Clean Code (로버트 C. 마틴)
'Clean Code (클린 코드)' 카테고리의 다른 글
[iOS - swift] Clean Code(클린 코드) - 11. 창발성 (4가지의 규칙을 따라서 클린 코드 유지방법) (0) | 2021.11.24 |
---|---|
[iOS - swift] Clean Code(클린 코드) - 10. 시스템 (Abstract Factory 패턴, DI) (0) | 2021.11.23 |
[iOS - swift] Clean Code(클린 코드) - 8. Unit Test (단위 테스트) (0) | 2021.11.19 |
[iOS - swift] Clean Code(클린 코드) - 7. 소프트웨어의 경계 (0) | 2021.11.18 |
[iOS - swift] Clean Code(클린 코드) - 6. 오류 처리 (0) | 2021.11.17 |