[Design Pattern] Visitor Pattern
Visitor Pattern에 대하여 알아보고, java와 kotlin 예제를 알아보도록 하겠습니다.
Visitor Pattern이란??
Visitor Pattern은 데이터 구조와 연산을 분리하여 데이터 구조의 원소들을 변경하지 않고
새로운 연산을 추가할 수 있는 패턴입니다.
새로운 연산을 추가하려면 새로운 방문자를 추가하기만 하면 됩니다.
구조
Visitor Pattern은 기본적으로 위와 같은 구조로 작성됩니다.
Element interface를 구현하는 Concrete가 여러 개 생성되고,
Visitor Interface를 구현하는 Concrete가 accept메서드의 파라미터로 전달됩니다.
Visigtor Pattern은 Composite Pattern과 같이 쓰이는 경우가 많습니다.
위 그림의 우측처럼 Element클래스를 구현하는 클래스를 다룰때 Composite Pattern을 이용하여 개별 객체와 그 객체들을 목록으로 가지고 있는 상위 객체를 구현합니다.
Entry가 Composite의 역할을 하고 File과 Directory는 Leaf입니다.
Visitor Pattern with Java
public interface Element {
public void accept(Visitor v);
}
public abstract class Entry implements Element {
String name;
public Entry(String name) {
this.name = name;
}
public abstract void add(Entry entry);
}
public class Directory extends Entry {
ArrayList<Entry> directory = new ArrayList();
public Directory(String name) {
super(name);
}
public void add(Entry entry) {
directory.add(entry);
}
public void accept(Visitor v) {
v.visit(this);
}
}
public class File extends Entry {
public File(String name) {
super(name);
}
public void add(Entry entry) {
}
public void accept(Visitor v) {
v.visit(this);
}
}
public abstract class Visitor {
public abstract void visit(File file);
public abstract void visit(Directory directory);
}
public class ViewVisitor extends Visitor {
private String Path = "";
public void visit(File file) {
System.out.println(Path + "/" + file.name);
}
public void visit(Directory dic) {
Path = Path + "/" + dic.name;
System.out.println(Path);
for (int i = 0; i < dic.directory.size(); i++) {
dic.directory.get(i).accept(this);
}
}
}
public class Client {
public static void main(String[] args){
Directory root = new Directory("root");
Directory bin = new Directory("bin");
Directory Lkt = new Directory("Lkt");
File file1 = new File("file1");
File file2 = new File("file2");
File file3 = new File("file3");
File file4 = new File("file4");
root.add(file1);
bin.add(file2);
bin.add(file3);
Lkt.add(file4);
root.add(Lkt);
root.add(bin);
root.accept(new ViewVisitor()); //경로 출력
}
}
//result
/root
/root/file1
/root/Lkt
/root/Lkt/file4
/root/Lkt/bin
/root/Lkt/bin/file2
/root/Lkt/bin/file3
Visitor Pattern with Kotlin
elements
interface PricingElement {
fun <R> accept(visitor: ReportVisitor<R>): R
}
ConcreteElements
//고정 가격 계약
class FixedPriceContract(val costPerYear: Long) : PricingElement {
override fun <R> accept(visitor: ReportVisitor<R>): R = visitor.visit(this)
}
//매달 지원금이 지출되는 계약
class SupportContract(val costPerMonth: Long) : PricingElement {
override fun <R> accept(visitor: ReportVisitor<R>): R = visitor.visit(this)
}
//시간당 비용이 지출되는 계약
class TimeAndMaterialsContract(val costPerHour: Long, val hours: Long) : PricingElement {
override fun <R> accept(visitor: ReportVisitor<R>): R = visitor.visit(this)
}
Visitor
interface ReportVisitor<out R> {
fun visit(contract: FixedPriceContract): R
fun visit(contract: TimeAndMaterialsContract): R
fun visit(contract: SupportContract): R
}
ConreteVisitor
//매달나가는 비용의 보고서를 만드는 Visitor
class MonthlyCostReportVisitor : ReportVisitor<Long> {
override fun visit(contract: FixedPriceContract): Long =
contract.costPerYear / 12
override fun visit(contract: TimeAndMaterialsContract): Long =
contract.costPerHour * contract.hours
override fun visit(contract: SupportContract): Long =
contract.costPerMonth
}
//연간 지출비용 보고서를 만드는 visitor
class YearlyReportVisitor : ReportVisitor<Long> {
override fun visit(contract: FixedPriceContract): Long =
contract.costPerYear
override fun visit(contract: TimeAndMaterialsContract): Long =
contract.costPerHour * contract.hours
override fun visit(contract: SupportContract): Long =
contract.costPerMonth * 12
}
class VisitorTest {
@Test
fun `Visitor`() {
val projectAlpha = FixedPriceContract(costPerYear = 10000)
val projectBeta = SupportContract(costPerMonth = 500)
val projectGamma = TimeAndMaterialsContract(hours = 150, costPerHour = 10)
val projectKappa = TimeAndMaterialsContract(hours = 50, costPerHour = 50)
val projects = arrayOf(projectAlpha, projectBeta, projectGamma, projectKappa)
val monthlyCostReportVisitor = MonthlyCostReportVisitor()
val monthlyCost = projects.map { it.accept(monthlyCostReportVisitor) }.sum()
println("Monthly cost: $monthlyCost")
Assert.assertThat(monthlyCost, CoreMatchers.`is`(5333L))
val yearlyReportVisitor = YearlyReportVisitor()
val yearlyCost = projects.map { it.accept(yearlyReportVisitor) }.sum()
println("Yearly cost: $yearlyCost")
Assert.assertThat(yearlyCost, CoreMatchers.`is`(20000L))
}
}
고정가격인 계약(FixedPriceContract)과 달마다 지원금이 나가는 계약(SupportContract)
시간 당계 약(TimeAndMaterials) 2개
총 4개의 프로젝트가 있고
4개의 프로젝트의 MonthlyCostReport를 보여주는 Visitor
4개의 프로젝트의 yearlyCostReport를 보여주는 Visitor
의 계산 결과를 확인하는 예제입니다.
//result
Monthly cost: 5333
Yearly cost: 20000
정리
Visitor Pattern은 데이터 구조와 연산을 분리하여
데이터 구조의 원소들을 변경하지 않고 새로운 연산을 추가할 수 있도록 하는 패턴입니다.
위와 같은 구조로 작성하게 되면 OCP원칙을 지키면서
기존 소스를 변경하지 않으며 확장에는 유연한 구조로 만들 수 있게 됩니다.
샘플 소스 보러 가기
https://github.com/qjatjr1108/DesignPattern
'DesignPattern' 카테고리의 다른 글
[Design Pattern] State Pattern (0) | 2019.10.21 |
---|---|
[Design Pattern] Chain of Responsibility Pattern (0) | 2019.10.21 |
[Design Pattern] Command Pattern (0) | 2019.10.21 |
[Design Pattern] Interpreter Pattern (0) | 2019.10.21 |
[Design Pattern] Iterator Pattern (0) | 2019.10.18 |
댓글