루비로 배우는 객체지향 디자인 2장

2018-09-13

루비로 배우는 객체 지향 디자인


2장 단일 책임 원칙을 따르는 클래스 디자인하기

수정하기 쉽도록 코드 구성

수정하기 쉽다

  • 수정이 예상치 못한 부작용을 낳지 않는다
  • 요구사항이 조금 변했을 때, 연관된 코드들을 조금만 수정하면 된다.
  • 현재 코드를 다시 사용하기 쉽다.
  • 코드를 수정하는 가장 쉬운 방법은 이미 수정하기 쉬운 코드에 새로운 코드를 추가하는 것이다.

좋은 코드

  • 투명하다 : 수정된 코드 속에서 그리고 이 코드와 연관된 코드 속에서, 수정의 결과가 뚜렷하게 드러나야 한다.
  • 적절하다 : 모든 수정 비용은 수정결과를 통해 얻은 이득에 비례해야한다.
  • 사용가능하다 : 예상치 못한 새로운 상황에서도 현재 코드를 사용할 수 있어야한다.
  • 모범이 된다 : 코드 자체가 나중에 수정하는 사람이 위의 특징을 이어갈 수 있게 도와줘야 한다.

이런 코드를 짜기 위한 첫 단계는 모든 클래스들이 하나의, 잘 정의된 책임을 갖도록 해야된다.

단일 책임 원칙의 중요성

한 개 이상의 책임이 있는 클래스는 재사용이 어렵다.

클래스 전체가 아니라 특정 행동만 재사용 하려고 하기 위한 복사 붙여넣기는 유지보수를 어렵게 하고 버그를 만들어낸다.

한 클래스가 너무 많은 책임을 지고 있으면 예상치 못한 오류가 발생할 가능성도 높아진다.

클래스에게 하나의 책임만 있는지 물어보기
  1. 클래스가 구현하고 있는 모든 메서드를 하나씩 질문 형태로 바꾸면 말이 되는 질문이 만들어져야한다.
    • “Gear씨, 당신의 기어비는 무엇인가요?”
  2. 클래스의 책임을 한 문장으로 만들어 보는 것이다.
    • 가장 단순한 표현이 ‘그리고’,’또는’을 사용한다면, 클래스는 하나 이상 또는 서로 연관되지 않은 둘 이상 책임을 가지고 있을 수 있다.
언제 디자인을 결정할지 판단하기
  1. 클래스를 현재 상태 그대로 두자.

클래스는 아무런 의존성도 없고, 코드는 투명하고 적절하지만 디자인이 훌륭하지 않다. 만약 다른 객체와의 의존성이 생긴다면 그 때 클래스는 투명함과 적절성을 잃게 되므로 코드 재구성을 해야 할 때이다.

  1. 클래스를 지금 수정해야 한다.

현재 클래스 구조는 미래 개발자에게 지금 디자인 의도를 전달하는 메세지이다. 미래 개발자는 지금의 디자인 패턴을 참조할 것이다. 다른 누군가는 이 클래스의 패턴을 따라 새로운 클래스를 만들 수 있고, 오해할 수 있는 여지가 존재한다.

**좋은 디자이너는 지금 당장 개선하기와 나중에 개선하기 사이의 긴장감을 이해하고, 심사숙고 하여 개선 비용을 최소화한다. **

변화를 받아들일 수 있는 코드 작성

데이터(data)가 아니라 행동(behavior)에 기반한 코드를 작성

하나의 책임만 지는 클래스를 만들면 각각의 작은 행동들은 단 한 곳에만 존재하므로 클래스 행동을 수정하기 위해 오직 한 부분만 수정하면 된다.

객체는 행동과 함께 데이터를 가진다. 데이터를 접근하는 방식은 2가지가 존재한다. 인스턴스 변수를 직접 참조하거나 또는 access method를 만들어 메소드에 접근하는 방법이다.

인스턴스 변수 숨기기
class Gear
    attr_reader :chainring, :cog
    def initalize(chainring, cog)
        @chainring = chainring
        @cog = cog
    end
    
    def ratio
        chinring / cog.to_f

주어진 클래스가 직접 선언한 변수에 접근하더라도 변수를 메서드로 감싸서 클래스로부터 감추는 편이 좋다.

attr_reader는 변수를 감쌀 수 있는 간단한 wrapper method를 만들어준다.

# attr_reader를 통해 구현
def cog
    @cog
end
데이터 구조 숨기기
class ObscuringReferences
    attr_reader :data
    def initalize(data)
        @data = data
    end
    
    def diameters
        # 0은 rim, 1은 tire
        data.collect {|cell|
            cell[0] + (cell[1] * 2)}
    end
    
end

data 메소드를 전송하는 객체는 배열의 어느 위치에 어떤 데이터가 들어있는지 모두 알고 있어야 한다.

diameters 메소드는 지름을 계산하는 방법만 알고 있는 것이 아니라 배열의 어디에서 지름과 높이를 찾아야 하는지도 알고 있다.

이러한 구조는 배열의 구조에 의존적 이다. 만약 구조가 바뀌면 코드도 변경되어야 한다.

복잡한 구조를 직접 참조하면 진짜 데이터가 무엇인지 드러내지 않기 때문에 헷갈리게 된다. 배열의 구조가 바뀔때마다 모든 참조지점을 찾아서 수정해야 하기 때문에 유지보수가 어렵다.

바퀴의 지름[0]에서 찾을 수 있다는 지식은 중복되어서는 안되고 한군데에서 관리해야한다.

class RevealingReferences
    attr_reader :wheels
    def initalize(data)
        @wheels = wheelify(data)
    end
    
    def diameters
        wheels.collect{|wheel|
            wheel.rim + (wheel.tire * 2)}
    end
    
    Wheel = Struct.new(:rim, :tire)
    def wheelify(data)
        data.collect{|cell|
            Wheel.new(cell[0],cell[1])}
    end
end

입력받은 배열의 구조에 대한 모든 지식은 wheelify 메소드 속에 격리되었고, 입력값이 변한다면 이 지점만 변경하면 된다.

모든 곳에 단일 책임 원칙을 강제하라

클래스가 아닌 다른 부분에도 단일 책임 원칙을 적용할 수 있다.

메소드에서 추가적인 책임을 뽑아내기

클래스처럼 메서드 역시 하나의 책임만을 져야한다. 메서드가 하나의 책임만을 지닐 때 메서드는 수정하기도 쉽고 재사용하기도 쉽다.

def gear_inches
    ratio * (rim + (tire *2))
end

Gear_inches 메서느 자체가 하나 이상의 책임을 지고 있다.

하나의 책임만 있는 두 개의 메서드로 분리하면,

def gear_inches
    ratio * diameter
end

def diameter(wheel)
    wheel.rim + (wheel.tire * 2)
end

Gear에게는 gear_inches를 계산해야 할 책임이 있지만, Gear가 바퀴의 지름을 계산해서는 않된다.

여러 메서드가 각각 하나의 책임을 질때 다음과 같은 이득을 얻을 수 있다.

  1. 예전에는 몰랐던 특성이 드러난다.
  2. 주석을 넣어야 할 필요가 없어진다.
  3. 재사용을 유도한다.
  4. 다른 클래스로 옮기기 쉽다.
클래스의 추가적인 책임들을 격리시켜 놓아라

Gear클래스가 하나의 책임만 지려면, 바퀴의 것으로보이는 method를 걷어내야 한다. 하지만 진짜로 필요하기 전에 내린 모든 결정은 단지 추측에 불과하므로 최대한 나중에 결정할 수 있는 여지를 남겨둬야한다.

class Gear
    attr_reader :chainring, :cog, :wheel
    def initalize(chainring, cog, rim, tire)
        @chainring = chainring
        @cog = cog
        @wheel = Wheel.new(rim, tire)
    end
    
    def ratio
        chinring / cog.to_f
    end
    
    def gear_inches
    	ratio * diameter
	end

	Wheel = Struct.new(:rim, :tire) do
        def diameter
            rim + (tire * 2)
        end
    end
end

이제 자신의 지름을 계산할 줄 아는 Wheel을 갖게 되었다. 이제 이 Gear 클래스는 깨끗해졌고, Wheel에 대한 판단을 미룰 수 있게 되었다.

의문점

단일 책임의 메소드를 생성을 하게 될 경우, 많은 메소드가 생성 될텐데 그러면 메소드 관리가 힘들지 않을까?

어디에 어떤 메소드들이 있었는 지 확인할 수 있는 방법이 없지 않을까?

협업을 하다보면 다른 사람이 만들어놓은 메소드들을 잘 파악하지 못하고 똑같은 기능을 하는 메소드들을 또 만들어 내지 않을까?

마지막 처럼 임시적으로 struct를 만들어놓을 경우, 다른 사람이 계속 저 구조로 쓰게 되면 어떡하지?

메소드를 사용하는 데 일부분만 수정해야되는 데 다른곳에서 쓰고 있어서 바꾸면 안되는 경우에는 어떡하지?