루비로 배우는 객체지향 디자인 | 5장 오리 타입으로 비용 줄이기

날짜
May 22, 2024
태그
OOP
설명
객체지향 디자인의 목표는 수정 비용을 줄이는 것이다. 애플리케이션 디자인의 핵심은 메시지라는 사실과 엄격하게 정의된 퍼블릭 인터페이스가 얼마나 중요한지를 알고 있다.
 
오리타입은 특정 클래스에 종속되지 않는 퍼블릭 인터페이스이다. 애플리케이션을 값비싼 의존으로부터 유연하게 만들어준다. 어떤 객체가 하나의 인터페이스에만 반응할 수 있다고 생각할 필요가 없다. 서로 다른 여러 개의 인터페이스를 구현할 수 있다. 진짜 중요한 것은 객체가 무엇인가가 아니라 어떻게 행동하는가이다. 모든 상황에서 모든 객체가 예상한 바대로 움직인다고 믿을 수 있다면, 어떤 타입이든 될 수 있다고 믿을 수 있다면 디자인의 무한한 가능성이 보일 것이다. 하지만 반대로 해독할 수 없는 혼란하고 끔찍한 디자인을 만들어 낼 수도 있다.
 
class Trip: def __init__(self, bicycles, customers, vehicle): self.bicycles = bicycles self.customers = customers self.vehicle = vehicle def prepare(mechanic): mechanic.prepare_bicycles(bicycles) class Mechanic: def prepare_bicycle(bicycles): for bicycle in bicycles: prepare_bicycle(bicycle) def prepare_bicycle(bicycle): ...
Trip의 prepare는 인자로 받은 mechanic에게 자전거를 준비하라고 요청한다.
 
Trip은 mechanic에게 bicycles를 준비(prepare)하라고 말하면서 스스로도 준비를 한다.
Trip은 mechanic에게 bicycles를 준비(prepare)하라고 말하면서 스스로도 준비를 한다.
 
prepare 메서드 자체는 Mechanic 클래스에 의존하고 있지 않다. 하지만 prepare_bicycles 메서드에 반응할 수 있는 개체를 수신해야 한다는 사실에 의존하고 있다. Trip의 prepare 메서드는 여행준비를 담당하는 객체(여기선 mechanic)를 인자로 받았다고 확신하고 있다.
 

여행 준비가 더 복잡해진다면

class Trip: def __init__(self, bicycles, customers, vehicle): self.bicycles = bicycles self.customers = customers self.vehicle = vehicle def prepare(self, preparers): for preparer in preparers: if isinstance(preparer, Mechanic): preparer.prepare_bicycles(self.bicycles) elif isinstance(preparer, TripCoordinator): preparer.buy_food(self.customers) elif isinstance(preparer, Driver): preparer.gas_up(self.vehicle) preparer.fill_water_tank(self.vehicle) class Mechanic: def prepare_bicycles(self, bicycles): for bicycle in bicycles: self.prepare_bicycle(bicycle) def prepare_bicycle(self, bicycle): # ... pass class TripCoordinator: def buy_food(self, customers): # ... pass class Driver: def gas_up(self, vehicle): # ... pass def fill_water_tank(self, vehicle): # ... pass
클래스 기반 관점으로 발전한 의존성이 켜켜이 쌓인 코드
 
Trip이 구체 클래스와 메서드를 너무 많이 알고 있다.
Trip이 구체 클래스와 메서드를 너무 많이 알고 있다.
 
요구 사항이 변경되어, 여행 준비에 정비공 뿐아니라 여행 보조인과 운전수도 필요해졌다. 그리고 각각에 어울리는 책임도 부여했다. 새로 만든 클래스의 책임은 간단하고 괜찮아 보인다. 하지만 Trip 클래스의 prepare 메서드는 문제가 있다. 세개의 서로 다른 클래스를 참조하고 있고, 각 클래스가 구현하고 있는 메서드의 이름을 정확히 알고 있다.
 

오리타입 찾기

이런 의존성을 제거하기 위해서는 의도를 파악해야 한다. prepare 메서드는 하나의 의도를 가지고 있다. 내부의 모든 인자는 같은 사명을 띄고 모였다. 우리는 ‘인자의 클래스가 무엇을 할 줄 아는지’ 알고 있다. ‘prepare가 무엇을 원하는지‘에 집중하자. prepare 메서드는 여행을 준비하고 싶어한다.
 
Trip은 모든 인자가 ‘여행을 준비하는 객체(preparer)’처럼 동작하기를 바란다.
Trip은 모든 인자가 ‘여행을 준비하는 객체(preparer)’처럼 동작하기를 바란다.
 
Trip은 준비할 줄 알고 있다고 믿고 있는 오리 타입 객체(preparer duck)과 협업한다.
Trip은 준비할 줄 알고 있다고 믿고 있는 오리 타입 객체(preparer duck)과 협업한다.
 
class Trip: def __init__(self, bicycles, customers, vehicle): self.bicycles = bicycles self.customers = customers self.vehicle = vehicle def prepare(self, preparers): for preparer in preparers: preparer.prepare_trip(self) class Mechanic: def prepare_trip(self, trip): for bicycle in trip.bicycles: self.prepare_bicycle(bicycle) def prepare_bicycle(self, bicycle): print(f"Preparing bicycle {bicycle}") class TripCoordinator: def prepare_trip(self, trip): self.buy_food(trip.customers) def buy_food(self, customers): print(f"Buying food for {len(customers)} customers") class Driver: def prepare_trip(self, trip): vehicle = trip.vehicle self.gas_up(vehicle) self.fill_water_tank(vehicle) def gas_up(self, vehicle): print(f"Gassing up the vehicle {vehicle}") def fill_water_tank(self, vehicle): print(f"Filling the water tank of the vehicle {vehicle}") bicycles = ['Bicycle1', 'Bicycle2'] customers = ['Customer1', 'Customer2'] vehicle = 'Vehicle1' trip = Trip(bicycles, customers, vehicle) mechanic = Mechanic() trip_coordinator = TripCoordinator() driver = Driver() preparers = [mechanic, trip_coordinator, driver] trip.prepare(preparers)
오리 타입을 통해 여행 준비 과정이 간단해졌다.
 
이제 prepare 메서드를 구정하지 않고도 새로운 Preparer를 추가할 수 있다. 여행 준비가 복잡해져도 손쉽게 새로운 Preparer를 추가할 수 있다.
 

오리 타입을 사용해서 얻는 이점

최초 구체 클래스는 구체적이기 때문에 이해가 쉽지만 확장에는 불리하다. 이를 오리 타입에 의존하게 하면서 확장과 수정에 용이하도록 했다. 객체를 클래스에 정의된 것이 아니라 행동을 통해 정의된 것으로 이해하기 시작할 때 우리는 표현력있고 유연한 디자이너가 될 수 있다.
 

숨겨진 오리타입 찾아내기

클래스에 따라 변경되는 조건문
class Trip: def __init__(self, bicycles, customers, vehicle): self.bicycles = bicycles self.customers = customers self.vehicle = vehicle def prepare(self, preparers): for preparer in preparers: if isinstance(preparer, Mechanic): preparer.prepare_bicycles(self.bicycles) elif isinstance(preparer, TripCoordinator): preparer.buy_food(self.customers) elif isinstance(preparer, Driver): preparer.gas_up(self.vehicle) preparer.fill_water_tank(self.vehicle) class Mechanic: def prepare_bicycles(self, bicycles): pass class TripCoordinator: def buy_food(self, customers): pass class Driver: def gas_up(self, vehicle): pass def fill_water_tank(self, vehicle): pass
isinstance()
if isinstance(preparer, Mechanic): preparer.prepare_bicycles(bicycles) elif isinstance(preparer, TripCoordinator): preparer.buy_food(customers) elif isinstance(preparer, Driver): preparer.gas_up(vehicle) preparer.fill_water_tank(vehicle)
hasattr()
if hasattr(preparer, 'prepare_bicycles'): preparer.prepare_bicycles(bicycles) elif hasattr(preparer, 'buy_food'): preparer.buy_food(customers) elif hasattr(preparer, 'gas_up'): preparer.gas_up(vehicle) preparer.fill_water_tank(vehicle)
 
 

댓글

guest