본문 바로가기
DesignPattern

[Design Pattern] Visitor Pattern

by 봄석 2019. 10. 21.

[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

 

qjatjr1108/DesignPattern

DesignPattern Sample. Contribute to qjatjr1108/DesignPattern development by creating an account on GitHub.

github.com

 

댓글