Visitorパターンって知ってたけど、使うところがなかったこともあり、実装したことがなかった。そこで、ちゃんと理解してみようと思い、複数のサンプルを書いてみて理解してみました。
Visitor パターン
定義
オブジェクト構造に対するオペレーションを実行するものを表します。Visitorは、オブジェクト構造を変更させることなく、新しいオペレーションを追加します。
と書いてあるが、文面だけだとよくわからないだろう。図を見てみよう。この記事はVisitorを参考に記述している。
対象の問題
大きく分けると、Visitorのインターフェイスの配下のオブジェクト、そして、Elementの配下のオブジェクトが存在する。このElementはオブジェクトのツリーだったりしますので、ここにゴリゴリにメソッドを実行してもよいわけです。もし、LeafとCompositeの構造をもっていたら、コンポジットパターンになります。そうではなくて、ここにメソッドを出来るだけ追加することなく、操作を追加しかったらどうしたらいいだろう?という問題を解きます。
さらに言うと、それぞれのElementのクラスごとに振る舞いを変えたい場合はどうするのでしょうか?
そのような時にうまく使えるかもしれないのが、Visitorパターンです。ここはコードを見たほうがわかりやすいでしょう。
Visitor パターンのポイントはElement側にインジェクトしたい操作を表すVisitorに Visit(SomeElement e)
があり、ここが拡張点になります。一方、Element側(ここでは、Employee)のほうは、Acceptメソッドが生えただけです。ClerkやDirectorやPresident側は、これが生えるだけです。ポイントとしては、このVisitのThisが、このクラスのオブジェクトの型であるということです。ClerkならClerk、DirectorならDirectorです。ということは、Visitorのクラスを見ると、型ごとにメソッドが生えているのがわかります。
public override void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
ここに、それぞれの処理を書きます。Element側ではないのがポイントです。型ごとに違う処理を記述することができます。ただし、元のオブジェクトのアクセスレベルが低い場合はアクセスできなくなったりするので注意が必要です。場合によっては、アクセス権を変更する必要があります。
public void Visit(Clerk employee)
{
employee.VacationDays += 3;
Console.WriteLine("{0} {1}'s new vacation days: {2}",
employee.GetType().Name, employee.Name,
employee.VacationDays);
}
使用するときは、Element側のオブジェクトを差し込んであげることで、Visit側の処理が各オブジェクト毎に実行されます。
foreach (Employee e in employees)
{
e.Accept(visitor);
}
Console.WriteLine();
Element側がもしオブジェクトツリーになっている場合はこのイメージでしょう。
root = new President("name");
bucho = new Director("name2");
root.Add(bucho);
root.Accept(new IncomeVisitor());
Console.WriteLine($"Total Income: {root.Income}");
```csharp
using System;
using System.Collections.Generic;
using System.Text;
namespace VisitorPattern
{
interface IVisitor
{
void Visit(Clerk employee);
void Visit(Director employee);
void Visit(President employee);
}
class IncomeVisitor : IVisitor
{
public void Visit(Clerk employee)
{
employee.Income *= 1.10;
Console.WriteLine("{0} {1}'s new income: {2:C}",
employee.GetType().Name, employee.Name, employee.Income);
}
public void Visit(Director employee)
{
employee.Income *= 2;
Console.WriteLine("{0} {1}'s new income: {2:C}",
employee.GetType().Name, employee.Name, employee.Income);
}
public void Visit(President employee)
{
employee.Income *= 10;
Console.WriteLine("{0} {1}'s new income: {2:C}",
employee.GetType().Name, employee.Name, employee.Income);
}
}
class VacationVisitor: IVisitor
{
public void Visit(Clerk employee)
{
employee.VacationDays += 3;
Console.WriteLine("{0} {1}'s new vacation days: {2}",
employee.GetType().Name, employee.Name,
employee.VacationDays);
}
public void Visit(Director employee)
{
employee.VacationDays *= 2;
Console.WriteLine("{0} {1}'s new vacation days: {2}",
employee.GetType().Name, employee.Name,
employee.VacationDays);
}
public void Visit(President employee)
{
employee.VacationDays *= 1;
Console.WriteLine("{0} {1}'s new vacation days: {2}",
employee.GetType().Name, employee.Name,
employee.VacationDays);
}
}
abstract class Element
{
public abstract void Accept(IVisitor visitor);
}
class Employee : Element
{
public string Name { get; private set; }
public double Income { get; set; }
public int VacationDays { get; set; }
public Employee(string name, double income, int vacationDays)
{
this.Name = name;
this.Income = income;
this.VacationDays = vacationDays;
}
public override void Accept(IVisitor visitor)
{
throw new NotImplementedException();
}
}
class Employees
{
private List<Employee> employees = new List<Employee>();
public void Attach(Employee employee)
{
employees.Add(employee);
}
public void Detach(Employee employee)
{
employees.Remove(employee);
}
public void Accept(IVisitor visitor)
{
foreach (Employee e in employees)
{
e.Accept(visitor);
}
Console.WriteLine();
}
}
class Clerk : Employee
{
public Clerk() : base("Hank", 25000.0, 14)
{
}
public override void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}
class Director : Employee
{
public Director() : base ("Elly", 35000.0, 16)
{
}
public override void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}
class President : Employee
{
public President() : base ("Dick", 45000.0, 21)
{
}
public override void Accept(IVisitor visitor)
{
visitor.Visit(this);
}
}
}
まとめ
若干複雑度が上がる代わりに、Element側に変更をあまり加えず、追加処理を差し込むことができるのが良い点のようです。実際にやってみると、コンポジットパターンで実装したときに、クラス同士に依存関係が発生して、気持ち悪いケースがあったのですが、その時にVisitorを使うと、操作側は同じところにあるので、すっきりしました。上にも書きましたが、リファクタリングで使うと、例えば protected
のスコープは、Visitor側からアクセスできなくなるので、スコープの変更が必要になったりしました。今は理解できてすっきりしました。