레벨 2 지하철노선도 미션에서
OCP를 지키기 위해
Pure Fabrication 중 하나인 팩토리 패턴 을 사용했다.
책 "오브젝트" 9장 "유연한 설계" 챕터를 참조했다.
객체 지향 디자인 패턴의 기본 원칙은 확장에 있어서는 열려 있어야 하며, 수정에 있어서는 닫혀 있어야 한다는 것이다.
(OCP, Open Closed Principle)
코드를 수정하지 않아도 모듈의 기능을 확장하거나 변경 할 수 있어야 한다.
때문에, 수정이 일어날 가능성이 큰 부분과 그렇지 않은 부분을 분리하는 것이 좋다.
객체를 생성하기 위한 인터페이스를 정의하는데,
어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하게 만든다.
즉 팩토리 메소드 패턴을 이용하면 클래스의 인스턴스를 만드는 일을 서브클래스에게 맡기는 것.
모든 generator를 통한 요금정책을 따로 뺀 factory 클래스를 만들었다.
요금정책 객체 생성의 책임을 할당했다.
public class PolicyFactory {
public static DistancePolicy createDistance(int distance) {
return DistancePolicyGenerator.of(distance);
}
public static AgeDiscountPolicy createAgeDiscount(int age) {
return AgeDiscountPolicyGenerator.of(age);
}
public static LineExtraFeePolicy createLineFee(List<Line> lines) {
return new LineExtraFeePolicy(lines);
}
}
각각의 요금정책 별 if문 분기들은 Generator 이름의 enum으로 따로 빼 처리했다.
특정 조건에 맞게 세부 정첵이 담긴 구현체
(예:어린이의 요금정책)
가 반환된다.
public enum AgeDiscountPolicyGenerator {
BABY((age) -> 0 <= age && age <= 5,
new BabyDiscountPolicy()),
CHILD((age) -> 5 < age && age <= 12,
new ChildDiscountPolicy()),
TEENAGER((age) -> 12 < age && age <= 18,
new TeenagerDiscountPolicy()),
ADULT((age) -> age > 18,
new AdultDiscountPolicy());
...
}
서비스에서는 팩토리에 요금정책 객체를 만들라 명령하고,
private PathResponse toPathResponse(Path path, int age) {
List<FarePolicy> policies = List.of(
PolicyFactory.createLineFee(path.getLines()),
PolicyFactory.createAgeDiscount(age)
);
BasePolicy basePolicy = PolicyFactory.createBase(path.getDistance());
return new PathResponse(
toStationResponse(path.getStations()),
path.getDistance(),
new Fare(policies, basePolicy).getFare());
}
해당 정책들을 반영하는 Fare 클래스에 기본정책, 추가 정책 리스트를 주입한다.
public class Fare {
private final int fare;
public Fare(List<FarePolicy> policies, BasePolicy basePolicy) {
this.fare = calculateTotalFare(policies, basePolicy);
}
public int getFare() {
return fare;
}
private int calculateTotalFare(List<FarePolicy> policies, BasePolicy basePolicy) {
double result = basePolicy.getFare();
for (FarePolicy policy : policies) {
result = policy.calculate(result);
}
return (int) result;
}
}
이를 통해 객체 생성, 객체 주입, 객체 사용과정을 모두 분리했고
각 정책별 분기별 추가/삭제에 대한 유연성을 확보했다.
정책을 인터페이스로 추상화하고 조건 별 요금 정책을 각각의 구현체로 만든다면 기존 요금 정책이 변경되는 게 아니라 새로운 요금 정책이 추가되는 경우 기존 구현체에는 거의 영향이 없다는 점에서 OCP를 지킨다고 할 수 있겠네요. 전체 요금 정책을 한눈에 파악하기는 어렵지만 만약 이후의 요금 정책 변경까지 고려해서 지금의 설계로 구현하셨다면 저는 이견 없습니다.
미래에 기능이 어떻게 변할지는 아무도 모르기 때문에 어느 부분을 어떻게 추상화할 것인지는 순전히 개발자의 역량에 달렸다고 생각해요. 예상했던 변경이 예상했던 방향으로 일어날 수도 있지만 그렇지 않은 경우도 많거든요 :)
오버 엔지니어링이다.
그래도 새로운 설계를 시도해봤다는 점에서 좋았다.
극한의 유연성을 추구해본 설계였다고 생각한다.
👍