클린 코드 10장 - 클래스

French Marigold·2024년 1월 4일
0

클린코드

목록 보기
10/13

캡슐화 (172p)

  • 변수나 함수는 되도록이면 숨기는 것이 좋다. ⭐️⭐️
  • 캡슐화를 풀어주는 결정은 언제나 최후의 수단이다.

클래스는 작아야 한다 (172p)

  • 함수와 마찬가지로 클래스도 크기가 작아야 함. ⭐️⭐️
  • 그렇다면 클래스는 얼마나 작아야 하는가?
    • 클래스는 클래스가 맡은 책임을 센다. ⭐️
    • 한 클래스가 맡은 책임이 너무 많다면 해당 클래스는 크기가 큰 클래스이다.
    • 클래스가 얼마나 작아야 하냐면 클래스가 하나의 책임만 가지고 있을 정도로 작아야 한다.
  • 클래스의 이름은 해당 클래스가 가진 책임을 기술하면 된다. ⭐️⭐️

단일 책임 원칙 (SRP) (175p)

  • 하나의 클래스는 하나의 기능만 담당하여 책임지도록 한다.
  • 여러 가지 기능을 책임지는 클래스를 여러 개의 단일 책임 클래스로 쪼개라.

응집도 (177p)

  • 응집도는 한 모듈 내의 구성 요소 간의 밀접한 정도를 뜻한다.
  • 응집도가 높다는 것은 다음과 같은 기준을 만족시키는 것을 의미함.
    • 클래스는 인스턴스 변수 수가 일단 적어야 하며,
    • 클래스가 하나의 기능(책임)만을 갖고 있어야 하며,
    • 메소드가 인스턴스 변수를 많이 사용할수록 응집도가 더 높다.
  • 일반적으로 이렇게 응집도가 가장 높은 클래스는 (모든 인스턴스 변수를 메소드마다 사용하는 클래스) 가능하지도 바람직하지도 않다. 하지만 응집도가 높은 클래스를 만드는 것은 매우 중요하다.
// 응집도가 높은 클래스의 대표적인 예시 ⭐️⭐️
// 1. 클래스가 "계산(Calculate)" 이라는 하나의 책임만 가지고 있으며
// 2. 인스턴스 변수가 적고
// 3. 각 메소드마다 인스턴스 변수를 사용하고 있다. 

class Calculator {
    var result: Int = 0

    func add(_ value: Int) {
        result += value
    }

    func subtract(_ value: Int) {
        result -= value
    }

    func reset() {
        result = 0
    }
}
  • 몇 몇 메소드만 사용하고 있는 인스턴스 변수가 클래스에 많아지고 있다면, 새로운 클래스로 쪼개야 한다는 신호이다.

OCP (188p)

  • OCP란 기존의 코드를 변경하지 않고 기능을 추가할 수 있도록 클래스를 설계해야 한다는 원칙을 의미한다.
  • 기능을 추가하기 위해 기존의 코드를 변경해야 한다면 코드를 유지보수하기가 굉장히 어려워지기 때문이다.
  • 이상적인 시스템이라면 새 기능을 추가할 때 시스템을 확장할 뿐 기존 코드를 변경하지는 않는다. ⭐️⭐️
// 인사를 잘하는 HelloMan 클래스가 있다고 해보자. 
final class HelloMan {
    func greet(name: String) {
        print("안녕하세요, \(name)님!")
    }
}

// 그런데 이 HelloMan이 영어로도 인사를 하게 하고 싶다고 가정하자. 
// OCP를 지키지 않은 코드는 다음과 같다.
final class HelloMan {
    func greet(name: String, language: String) {
        if language == "Korean" {
            print("안녕하세요, \(name)님!")
        } else if language == "English" {
            print("Hello, \(name)!")
        }
    }
}

// 기능을 추가하기 위해 기존에 존재하는 코드를 수정하였으므로 OCP를 위반한 코드이다.
// 위 코드는 새로운 언어가 추가될 때마다 greet 메소드를 수정해야 하므로 OCP를 위반한다. ⭐️⭐️

// OCP를 지키며 코드를 만드는 방법은 다음과 같다. 

// 1. 우선 인사하는 메소드를 protocol로 만든다. 
// 즉, 수정이 자주 일어날 코드를 protocol화 시킨다. ⭐️⭐️
protocol Greeting {
    func greet(name: String)
}

// 2. 단일 책임을 갖는 클래스 여러 개를 만들고 각각 protocol을 채택한다.
class KoreanGreeting: Greeting {
    func greet(name: String) {
        print("안녕하세요, \(name)님!")
    }
}

class EnglishGreeting: Greeting {
    func greet(name: String) {
        print("Hello, \(name)!")
    }
}

// 3. HelloMan은 protocol 타입의 인스턴스 변수를 생성
// protocol을 채택하는 모든 클래스는 다 실행할 수 있게끔 코드를 구현. 
// 이렇게 코드를 만들면 HelloMan 클래스는 기존의 코드를 변경하지 않으면서도 
// 기능을 무한정으로 추가해낼 수 있다. ⭐️⭐️
final class HelloMan {
    private let greeting: Greeting

    init(greeting: Greeting) {
        self.greeting = greeting
    }

    func greet(name: String) {
        greeting.greet(name: name)
    }
}

let englishGreeting = EnglishGreeting()
let koreanGreeting = KoreanGreeting()

let hellomanSpeaksKorean = HelloMan(greeting: koreanGreeting)
let hellomanSpeaksEnglish = HelloMan(greeting: englishGreeting)

hellomanSpeaksKorean.greet(name: "홍필")
hellomanSpeaksEnglish.greet(name: "Marigold")

// 안녕하세요, 홍필님!
// Hello, Marigold!

변경으로부터 격리 (DIP) (189p) ⇒ OCP와 결을 같이하는 느낌임

  • DIP란 클래스가 상세한 구현이 아니라 추상화에 의존해야 한다는 원칙이다.
  • 구체적인 클래스와 추상 클래스의 특징은 다음과 같다.
    • 구체적인 클래스
      • 상세한 구현을 포함한다.
      • 구현이 바뀌면 문제가 발생한다.
      • 테스트 코드를 작성하기 어렵다
    • 추상 클래스
      • 개념만 포함한다.
      • 구현이 바뀌어도 변경으로부터 격리되었기 때문에 문제가 발생하지 않는다.
      • 테스트 코드를 작성하기 쉽다.
// 1. 추상적인 개념을 포함하는 protocol을 만든다.
protocol StockExchange {
    func currentPrice(symbol: String) -> Double
}

// 2. 구체적인 클래스에서 추상적인 개념을 채택한다. 
// 이렇게 코드를 만들면 테스트 코드를 만들기에도 매우 용이해진다. 
class Portfolio {
    private let exchange: StockExchange
    
    init(exchange: StockExchange) {
        self.exchange = exchange
    }
    
    func getCurrentPrice(symbol: String) {
				exchange.currentPrice(symbol: String)
		}
}

// 3. 가짜 포트폴리오 값을 보내는 클래스를 만든다. 
// 이 때, 추상적인 개념을 포함하는 protocol인 StockExchange를 채택하여 
// 개발자가 임의로 값을 지정할 수 있도록 만든다.
// 이는 테스트 코드를 만들 때에 매우 유용하다. 
final class FixedStockExchangeStub: StockExchange {
    private var prices = [String: Double]()
    
    func fix(symbol: String, price: Double) {
        prices[symbol] = price
    }
    
    func currentPrice(symbol: String) -> Double {
        return prices[symbol] ?? 0
    }
}

import XCTest
@testable import Portfolio

class PortfolioTests {
		var exchange: FixedStockExchangeStub!
    var portfolio: Portfolio!
    
    override func setUpWithError() throws {
        try super.setUpWithError()
    }
    
    override func tearDownWithError() throws {
        try super.tearDownWithError()
    }
    
    func setup() {
        exchange = FixedStockExchangeStub()
        exchange.fix(symbol: "MSFT", price: 500)
        portfolio = Portfolio(exchange: exchange)
    }
    
    func test_GivenFiveMSFTTotalShouldBe500() {
				setup()
        XCTAssertEqual(portfolio.value(), 500)
    }
}
profile
꽃말 == 반드시 오고야 말 행복

0개의 댓글