클린코드(Clean Code) - 10장 클래스 (ver.Swift)

yeton·2024년 4월 5일
0

클래스 작성하는 방법

요약

  • SRP 준수 => 클래스의 변경 이유는 한가지가 되도록 설계
  • 인스턴스 변수를 최소화
  • 클래스는 작게 만들 것

클래스는 작아야 한다!

  • 클래스는 작아야 가독성, 유지보수에 이점이 있음
  • 클래스의 책임의 개수 판단
  • 메소드의 개수는 5개 이하가 적정
  • 클래스 이름은 해당 클래스 책임을 기술하는 최소의 범위로 작성
    (Manaer, Processor가 붙으면 해당 클래스에서 여러 책임을 떠안겼다는 증거)

ex) 책임이 많은 클래스 ❌

  • 메소드의 개수는 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를 위반 (소프트웨어 버전 추적, 포커스된 컴포넌트 추적)

ex) 수정한 클래스 ⭕️

  • 소프트웨어 버전 추적 관련 코드를 제외시켜서, SRP를 준수하도록 설계 (동시에 클래스의 크기도 작아지는 장점이 존재)
  • Version 클래스를 별도로 생성
  • SRP를 준수하고 있으므로 재사용하기 쉬운 구조로 탄생
class Dashboard: SomeModule {
    func getLastFocusedComponent() -> Component
    func setLastFocused()
}

class Version {
    func getMajorVersionNumber() -> Int
    func getMinorVersionNumber() -> Int
    func getBuildNumber() -> Int
}

SRP만 준수해도 클래스를 관리하기 용이하고 추상화 시키는 작업에도 도움됨

클래스의 성질

응집도(Cohesion)

응집도의 개념: 클래스에 속한 메소드와 변수가 서로 의존하며 논리적인 단위로 묶인다는 의미
응집도의 기준: 인스턴스 변수가 메소드에서 많이 사용
클래스는 인스턴스 변수 수가 작아야함
(메소드는 인스턴스 변수를 하나 이상 사용해야 하므로 메소드가 변수를 더 많이 사용할수록 "메소드+클래스" 응집도가 높음)

응집도가 높은 코드

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를 준수하며, 함수의 인수들을 인스턴스로 변경하여 클래스들로 최대한 작게 쪼갤 것

ex) SRP 위반 클래스 ❌

  • SQL문 하나를 수정할 때도 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]) { ... }
}

옳은 방법 ⭕️

  • 공개 인터페이스를 각각 Sql클래스에서 파생하는 클래스로 생성
// 닫힌 클래스 집합
class Sql {
    let table: String
    let columns: [Column]

    init(table: String, columns: [Column]) {
        self.table = table
        self.columns = columns
    }

    func generate() -> String {
        fatalError("This method must be overridden")
    }
}

class CreateSql: Sql {
    override func generate() -> String {
        // Implement generate for CreateSql
    }
}

class SelectSql: Sql {
    override func generate() -> String {
        // Implement generate for SelectSql
    }
}

class InsertSql: Sql {
    let fields: [Any]

    init(table: String, columns: [Column], fields: [Any]) {
        self.fields = fields
        super.init(table: table, columns: columns)
    }

    override func generate() -> String {
        // Implement generate for InsertSql
    }

    private func valueList(fields: [Any], columns: [Column]) -> String {
        // Implement valueList
    }
}

class SelectWithCriteriaSql: Sql {
    let criteria: Criteria

    init(table: String, columns: [Column], criteria: Criteria) {
        self.criteria = criteria
        super.init(table: table, columns: columns)
    }

    override func generate() -> String {
        // Implement generate for SelectWithCriteriaSql
    }
}

class SelectWithMatchSql: Sql {
    let pattern: String

    init(table: String, columns: [Column], pattern: String) {
        self.pattern = pattern
        super.init(table: table, columns: columns)
    }

    override func generate() -> String {
        // Implement generate for SelectWithMatchSql
    }
}

class FindByKeySql: Sql {
    let keyColumn: String
    let keyValue: String

    init(table: String, columns: [Column], keyColumn: String, keyValue: String) {
        self.keyColumn = keyColumn
        self.keyValue = keyValue
        super.init(table: table, columns: columns)
    }

    override func generate() -> String {
        // Implement generate for FindByKeySql
    }
}

class PreparedInsertSql: Sql {
    override func generate() -> String {
        // Implement generate for PreparedInsertSql
    }

    private func placeholderList(columns: [Column]) -> String {
        // Implement placeholderList
    }
}

class Where {
    let criteria: String

    init(criteria: String) {
        self.criteria = criteria
    }

    func generate() -> String {
        // Implement generate for Where
    }
}

class ColumnList {
    let columns: [Column]

    init(columns: [Column]) {
        self.columns = columns
    }

    func generate() -> String {
        // Implement generate for ColumnList
    }
}

struct Column {
    // Implement Column struct members
}

struct Criteria {
    // Implement Criteria struct members
}

이점

  • 메서드 하나를 추가할 때 기존 클래스를 변경할 필요가 없다.
  • 각 클래스는 극도로 단순하므로 보자마자 순식간에 이해되는 형태
  • 테스트 관점에서 모든 논리를 구석구석 증명하기에 용이하기 쉬운 형태
  • 확장에 개방적이고 수정에 폐쇄적이어야 한다는 원칙 OCP(Open Close Principle) 준수 (새 기능 추가 시 기존 코드 변경 x)

DIP를 준수하여 결합도 낮추기

  • DIP(Dependency Inversion Principle): 사용하는 쪽에서 구현체가 아닌 추상화(protocol)에 의존해야하는 원칙이므로, 클래스를 만들땐 protocol을 만든 후 구현을 지향
  • 구현체에 의존하게 된다면, 테스트가 어렵지만 인터페이스에 의존하면, 구현체를 언제든 변경하여 테스트하기 용이한 형태
  • 구현 내용이 바뀌어도 사용하는 쪽에서는 인터페이스인 protocol에만 의존하고 있으므로 구현체의 내용이 언제든 바뀌어도 영향받지 않은 유연한 구조
  • 구현체의 내용이 숨겨짐으로서 사용하는쪽에서 사용하는데 필요한 정보에만 의존이 가능
profile
🤗제 깃허브 링크는 https://github.com/yeeton37🤗

0개의 댓글