GoF - Visitor 패턴

October 10, 2022

Visitor Pattern

public interface Item {
	int getPrice();
}
public class Phone implements Item {
	private final int price;
	private final boolean android;

	public Phone(int price, boolean android) {
		this.price = price;
		this.android = android;
	}

	@Override
	public int getPrice() {
		return price;
	}

	public boolean isAndroid() {
		return android;
	}
}
public class Pad implements Item {
	private final int price;
	private final boolean screenCrack;

	public Pad(int price, boolean screenCrack) {
		this.price = price;
		this.screenCrack = screenCrack;
	}

	@Override
	public int getPrice() {
		return price;
	}

	public boolean isScreenCrack() {
		return screenCrack;
	}
}
public class Laptop implements Item {
	private final int price;

	public Laptop(int price) {
		this.price = price;
	}

	@Override
	public int getPrice() {
		return price;
	}
}

위 코드처럼 물건을 나타내는 Item 인터페이스가 있고, 이를 구현하는 Phone, Pad, Laptop이 있다고 하자. 이 물건들을 중고 업자에게 넘기는 비즈니스가 생겨서 중고 업자의 매입가를 계산하는 기능이 필요하다고 해보자.

public class Phone implements Item {
	private final int price;
	private final boolean android;

	public Phone(int price, boolean android) {
		this.price = price;
		this.android = android;
	}

	@Override
	public int getPrice() {
		return price;
	}

	public boolean isAndroid() {
		return android;
	}

	// 중고 업자 매입 가격 계산
	public int getPurchasePrice() {
		if (isAndroid()) {
			return price < 10 ? price : 10;
		} else {
			return price / 2;
		}
	}
}

이렇게 하면 될까? 상황에 따라 다르겠지만 단순히 메서드를 추가하는게 찝찝한 경우도 있다. 위 케이스에서 ’Item에 메서드를 추가하는게 단일 책임 원칙을 만족하는걸까?‘라는 생각이 들 수도 있고, 알고보니 Item의 구현체 내용들이 이미 너무 복잡해서 함부로 건드렸다가는 어떤 사이드이펙트가 발생할지 모르니 추가하기 싫다는 생각이 들 수도 있다.

비지터 패턴을 사용하면 여러 객체에 새로운 기능을 추가할 때 기존 코드의 변경을 최소화하면서 추가할 수 있다. 방문자라는 사전적 의미 그대로 비지터 객체를 통해서 다른 객체를 방문하여 원하는 작업을 처리하게 할 수 있다.

visitor-pattern

패턴 적용

public interface Item {
	int getPrice();
	int accept(SecondHandDealerVisitor secondHandDealerVisitor);
}
public class Phone implements Item {
	private final int price;
	private final boolean android;

	public Phone(int price, boolean android) {
		this.price = price;
		this.android = android;
	}

	@Override
	public int getPrice() {
		return price;
	}

	public boolean isAndroid() {
		return android;
	}

	@Override
	public int accept(SecondHandDealerVisitor secondHandDealerVisitor) {
		return secondHandDealerVisitor.visit(this);
	}
}
public class Pad implements Item {
	private final int price;
	private final boolean screenCrack;

	public Pad(int price, boolean screenCrack) {
		this.price = price;
		this.screenCrack = screenCrack;
	}

	@Override
	public int getPrice() {
		return price;
	}

	public boolean isScreenCrack() {
		return screenCrack;
	}

	@Override
	public int accept(SecondHandDealerVisitor secondHandDealerVisitor) {
		return secondHandDealerVisitor.visit(this);
	}
}
public class Laptop implements Item {
	private final int price;

	public Laptop(int price) {
		this.price = price;
	}

	@Override
	public int getPrice() {
		return price;
	}

	@Override
	public int accept(SecondHandDealerVisitor secondHandDealerVisitor) {
		return secondHandDealerVisitor.visit(this);
	}
}
public interface SecondHandDealerVisitor {
	int visit(Phone phone);
	int visit(Pad pad);
	int visit(Laptop laptop);
}
public class SecondHandDealer implements SecondHandDealerVisitor {
	@Override
	public int visit(Phone phone) {
		int originalPrice = phone.getPrice();
		if (phone.isAndroid()) {
			return originalPrice < 10 ? originalPrice : 10;
		} else {
			return originalPrice / 2;
		}
	}

	@Override
	public int visit(Pad pad) {
		int originalPrice = pad.getPrice();
		if (pad.isScreenCrack()) {
			return originalPrice / 2 - 100;
		} else {
			return originalPrice / 2;
		}
	}

	@Override
	public int visit(Laptop laptop) {
		int originalPrice = laptop.getPrice();
		if (originalPrice > 2000) {
			return originalPrice / 2;
		} else {
			return originalPrice / 3;
		}
	}
}
public class VisitorTestDrive {
	public static void main(String[] args) {
		final List<Item> items = List.of(
			new Phone(1500, false),
			new Phone(900, true),
			new Pad(1200, true),
			new Laptop(2600)
		);

		final SecondHandDealerVisitor secondHandDealer = new SecondHandDealer();
		final int sum = items.stream()
			.mapToInt(item -> item.accept(secondHandDealer))
			.sum();

		System.out.println("sum: " + sum);
	}
}

구현한 패턴을 보면 비지터 객체가 수행하는 연산들을 한 곳에 모아 둘 수 있는 장점이 있다. 하지만 visit 메서드에서 객체의 타입을 구분하기 위해 Item이라는 추상화된 형태로 사용하는 것이 아니라 구현체 타입을 사용하여 오버로딩하는 수 밖에 없다. 비지터 패턴에서는 이처럼 캡슐화가 깨지는 현상이 발생한다.


참고


songmk 🙁