본문 바로가기
책/파이브 라인스 오브 코드

5장 - 유사한 코드 융합하기

by jeounpar 2024. 2. 17.

유사 클래스 통합

상수 메서드를 공통으로 가진 두 개 이상의 클래스에서 각 클래스에 따라 다른 값을 반환하는 패턴이 보인다면 하나의 클래스로 통합할 수 있다.

아래 코드는 세 개의 신호등 클래스를 나타내고 있다.

function nextColor(t: TrafficColor) {
	if (t.color() === "green") return new Green();
	else if (t.color() === "yellow") return new Yellow();
	else if (t.color() === "red") return new Red();
}

class Red implements TrafficColor {
	color(): string {
		return "red";
	}
	check(car: Car): void {
		car.stop();
	}
}

class Yellow implements TrafficColor {
	color(): string {
		return "yellow";
	}
	check(car: Car): void {
		car.stop();
	}
}

class Green implements TrafficColor {
	color(): string {
		return "green";
	}
	check(car: Car): void {
		car.drive();
	}
}

세 개의 신호등 클래스는 각각 다른 상수를 리턴하므로 하나의 클래스로 통합할 수 있다.

생성자를 사용해서 color 를 직접 주입할 수 있도록 바꾸면,

function nextColor(t: TrafficColor) {
	if (t.color() === "green") return new Green("green");
	else if (t.color() === "yellow") return new Yellow("yellow");
	else if (t.color() === "red") return new Red("red");
}

class Red implements TrafficColor {
	constructor(private col: string) {}
	color(): string {
		return this.col;
	}
	...
}
...

Green, Yello, Red 클래스를 생성할 때 어떤 색상의 문자열이 생성자 파라미터로 넘어가는것에 따라 달리자므로 세 개의 클래스를 하나로 합치면,

function nextColor(t: TrafficColor) {
	if (t.color() === "green") return new Color("green");
	else if (t.color() === "yellow") return new Color("yellow");
	else if (t.color() === "red") return new Color("red");
}

class Color implements TrafficColor {
	constructor(private col: string) {}
	color(): string {
		return this.col;
	}
	check(car: Car): void {
		if (this.col === "yello" || this.col === "red") car.stop();
		else car.drive;
	}
}

이렇게 하나의 클래스로 관리할 수 있다.

 

if 문 결합

아래에 송장(invoice)으로 무엇을 할지 결정하는 코드가 있다.

if (today.getDate() === 1 && accout.getBalance() > invoice.getAmount()) {
	account.pay(bill);
} else if (invoice.isLasyDayOfPayment() && invoice.isApproved()) {
	account.pay(bill);
}

 

if 문과 else-if 문 내부 동작이 account.pay(bill)로 같기 때문에 하나의 if문으로 합치면 다음과 같다.

if ((today.getDate() === 1 && accout.getBalance() > invoice.getAmount()) ||
		(invoice.isLasyDayOfPayment() && invoice.isApproved())) {
	account.pay(bill);
}

 

복잡한 조건 통일하기

복잡한 if 조건문을 하나로 합칠때 한가지 주의해야할 것이 있다.

바로 if문에는 '순수 조건' 을 사용해야 한다는 것이다.

'순수' 라는 말은 조건에 부수적인 동작이 없음을 의미한다.

'부수적인 동작' 이란 조건이 변수에 값을 할당하거나 예외를 발생시시키거나, 출력 & 파일 쓰기 등과같이 I/O와 상호작용 하는 것을 의미한다.

순수한 조건을 갖는 것은 여러가지 이유로 중요한데,

첫 번째로 부수적인 동작이 존재하는 조건으로 인해 1장부터 언급한 리팩터링 규칙들을 사용할 수 없다.

두 번째로 부수적인 동작은 조건문에서 흔하게 사용하지 않기 때문에 조건에 부수적인 동작이 있을 것으로 예상하지 않는다.

즉, 리팩터링이 어려워지고 디버깅이 힘들어지기 때문에 순수 조건을 사용하는 것이 좋다.

 

전략 패턴의 도입

배열에서 최솟값을 찾는 메서드를 갖는 클래스와 배열의 모든 원소의 합을 구하는 메서드를 갖는 클래스가 있다.

class ArrayMinimum {
	constructor(private accmulator: number) {}
	process(arr: number[]) {
		for (let i = 0; i < arr.length; i++) {
			if (this.accmulator > arr[i]) this.accmulator = arr[i];
		}
		return this.accmulator;
	}
}

class ArraySum {
	constructor(private accmulator: number) {}
	process(arr: number[]) {
		for (let i = 0; i < arr.length; i++) {
			this.accmulator += arr[i];
		}
		return this.accmulator;
	}
}

배열에서 최소값을 구하는 로직과 배열의 합을 구하는 로직을 분리해서 이 것을 전략패턴으로 추출하면 다음과 같다.

interface Strategy {
	processElement(e: number): void;
	getAccmulator(): number;
}

class MinimumStrategy implements Strategy {
	constructor(private accmulator: number) {}
	processElement(e: number) {
		if (this.accmulator > e) this.accmulator = e;
	}
	public getAccmulator(): number {
		return this.accmulator;
	}
}

class SumStrategy implements Strategy {
	constructor(private accmulator: number) {}
	processElement(e: number) {
		this.accmulator += e;
	}
	public getAccmulator(): number {
		return this.accmulator;
	}
}

Strategy라는 인터페이스를 구현하는 MinimumStrategy와 SumStrategy 클래스를 구현해서 최소값을 구하는 전략과 합을 구하는 전략으로 나눈다.

완성된 코드는 다음과 같다.

class ArrayMinimum {
	private strategy: Strategy;
	constructor(accmulator: number) {
		this.strategy = new MinimumStrategy(accmulator);
	}
	process(arr: number[]) {
		for (let i = 0; i < arr.length; i++) this.strategy.processElement(arr[i]);
		return this.strategy.getAccmulator();
	}
}

class ArraySum {
	private strategy: Strategy;
	constructor(accmulator: number) {
		this.strategy = new SumStrategy(accmulator);
	}
	process(arr: number[]) {
		for (let i = 0; i < arr.length; i++) this.strategy.processElement(arr[i]);
		return this.strategy.getAccmulator();
	}
}

ArrayMinimum 클래스와 ArraySum 클래스는 내부 구현에서 Strategy 인터페이스를 구현하도록 하드코딩 되어있다.

Strategy 인터페이스 구현체를 생성자에서 주입 받도록 변경하면 다음과 같다.

class ArrayProcess {
	private strategy: Strategy;
	constructor(strategy: Strategy) {
		this.strategy = strategy;
	}
	process(arr: number[]) {
		for (let i = 0; i < arr.length; i++) this.strategy.processElement(arr[i]);
		return this.strategy.getAccmulator();
	}
}

let arrayMinimum: ArrayProcess = new ArrayProcess(new MinimumStrategy(0));
let arraySum: ArrayProcess = new ArrayProcess(new SumStrategy(0));

여전히 생성자에 직접 구현 전략을 넣어줘야 하는 불편함이 있는데, 요거는 Dependency Injection을 사용해 해결할 수 있을 것 같다.

 

구현체가 하나뿐인 인터페이스를 만들지 말 것

요 내용은 Service, Repository 코드를 작성할 때 항상 고민이 되었던 문제이다.

책에서는 다음과 같은 이유를 설명하고 있다.

1. 구현 클래스가 하나밖에 없는 인터페이스는 가독성에 도움이 되지 않는다.

2. 구현 클래스를 수정하려는 경우 인터페이스를 수정해야하는 오버헤드가 발생한다.

3. 구현 클래스가 하나만 있는 인터페이스는 도움이 되지 않는 일반화의 한 형태이다. (메서드 전문화 규칙)

 

그래서 나는 Service 레이어 코드를 작성할 때 인터페이스를 작성하지 않는데... 이게 맞는건지 잘 모르겠다..