拡張/変更を考慮した構造
あらためてもう一度、今度は拡張/変更を考慮した構造に仕立てましょう(C#,VB,Java版はおマケのサンプルファイルに納めてあります)。インターフェース名は.NETの流儀に則り'I'を頭に付けることにします。
まずVisitorおよびVisitorの訪問を受理するElementをインターフェースとします:
class IVisitor {
public:
virtual ~IVisitor() {}
};
class IElement {
public:
virtual void accept(IVisitor& visitor) =0;
};
脱力するほどナカミのないこの2つをベースにVisitorパターンを組み上げましょう。サンプルに用いるのは「自作PCのお値段調べ」です。僕の愛用するマシンは、ざっくりこんな部品構成になっています:
Case
|
+-- PowerSupply
|
+-- MotherBoard
|
+-- CPU
|
+-- Memory(n枚)
Case内にPowerSupplyとMotherBoard、MotherBoardはCPUとMemoryを内包しています。各部品には名前と価格が付いていて、総額おいくらになるのかをVisitorパターンで求めてみます。
Equipment(部品)およびCompositeElement(他の部品を内包する部品)を定義します。前述の例ではShopとShoppingmallに相当しますね:
class Equipment : virtual public IElement {
public:
Equipment(const string& name, int price)
: name_(name), price_(price) {}
virtual ~Equipment() {}
virtual string name() const { return name_; }
int price() const { return price_; }
virtual void accept(IVisitor& visitor) =0;
private:
string name_;
int price_;
};
class CompositeEquipment : public Equipment {
public:
CompositeEquipment(const string& name, int price)
: Equipment(name,price) {}
void add(Equipment* eq) {
contains_.push_back(unique_ptr<Equipment>(eq));
}
virtual void accept(IVisitor& visitor) {
for_each(contains_.begin(), contains_.end(),
[&](unique_ptr<Equipment>& eq) { eq->accept(visitor); });
}
private:
vector<unique_ptr<Equipment>> contains_;
};
EquipmentからPowersupply,CPU,Memoryを、CompositeEquipmentからCaseとMotherBoardを導出します。
class CPU : public Equipment {
public:
CPU(const string& name, int price)
: Equipment(name,price) {}
virtual string name() const {
return "CPU: " + Equipment::name();
}
virtual void accept(IVisitor& visitor) {
dynamic_cast<IEquipmentVisitor&>(visitor).visitCPU(this);
}
};
// PowerSupply, Memory は CPUと同様なので省略
class Case : public CompositeEquipment {
public:
Case(const string& name, int price)
: CompositeEquipment(name,price) {}
virtual string name() const {
return "Case: " + CompositeEquipment::name();
}
virtual void accept(IVisitor& visitor) {
dynamic_cast<IEquipmentVisitor&>(visitor).visitCase(this);
}
};
// MotherBoard は Caseと同様なので省略
各部品を訪ねて回るVisitorのインターフェースをIEquipmentVisitorとしましょうか。
class IEquipmentVisitor : virtual public IVisitor {
public:
virtual void visitCPU(CPU*) =0;
virtual void visitMotherBoard(MotherBoard*) =0;
virtual void visitCase(Case*) =0;
virtual void visitMemory(Memory*) =0;
};
各部品を訪れてprice()の返す価格を積み上げるPricingVisitorはもちろんIEquipmentVisitorを実装することになりますな:
class PricingVisitor : virtual public IEquipmentVisitor {
public:
PricingVisitor() : total_(0) {}
int totalPrice() const { return otal_; }
virtual void visitCPU(CPU* eq) { accumulate(eq->price(); }
virtual void visitMotherBoard(MotherBoard* eq) { accumulate(eq->price(); }
virtual void visitCase(Case* eq) { accumulate(eq->price(); }
virtual void visitMemory(Memory* eq) { accumulate(eq->price(); }
protected:
void accumulate(int price);
private:
int total_;
};
これででき上がり。PCを組み立て、総額おいくらになるかPricingVisitorに集計させます:
int main() {
Case pc("Lian Lee CX200", 10000);
MotherBoard* mb = new MotherBoard("GYGABYTE Z68", 9800);
mb->add(new CPU("Intel i7-2600K", 27000));
mb->add(new Memory("Kingston 4GB", 4800, 4));
mb->add(new Memory("Kingston 4GB", 4800, 4));
mb->add(new Memory("Kingston 4GB", 4800, 4));
mb->add(new Memory("Kingston 4GB", 4800, 4));
PricingVisitor visitor;
pc.accept(visitor);
cout << "total price \\" << visitor.totalPrice() << endl;
}
...おっといけねぇ、ストレージデバイス:HDとDVDを忘れてます。IEquipmentVisitor,PricingVisitorに手を加えることなく、HDとDVDに対応するVisitorを用意することで対応できます:
class HD;
class DVD;
// 追加分(HD,DVD)のVisitor
class IStorageVisitor : virtual public IVisitor {
public:
virtual void acceptHD(HD*) =0;
virtual void acceptDVD(DVD*) =0;
};
class HD : public Equipment {
public:
...
virtual void accept(IVisitor& visitor) {
dynamic_cast<IStorageVisitor&>(visitor).visitHD(this);
}
};
// DVD は HDと同様なので省略
// 追加分(HD,DVD)も勘定するPricingVisitor
class PricingVisitorEx : public PricingVisitor, virtual public IStorageVisitor {
public:
virtual void visitHD(HD* eq) { accumulate(eq->price()); }
virtual void visitDVD(DVD* eq) { accumulate(eq->price()); }
};
int main() {
Case pc("Lian Lee CX200", 10000);
MotherBoard* mb = new MotherBoard("GYGABYTE Z68", 9800);
mb->add(new CPU("Intel i7-2600K", 27000));
mb->add(new Memory("Kingston 4GB", 4800, 4));
mb->add(new Memory("Kingston 4GB", 4800, 4));
mb->add(new Memory("Kingston 4GB", 4800, 4));
mb->add(new Memory("Kingston 4GB", 4800, 4));
pc.add(mb);
pc.add(new HD("SAMSUNG 1TB", 7800, 1000));
pc.add(new DVD("SonyOptiarc super-multi", 2800));
PricingVisitorEx visitor;
pc.accept(visitor);
cout << "total price \\" << visitor.totalPrice() << endl;
}
登場したクラスの関係をまとめた図を示します。
IElementを頂点とした構造と、IVisitorを頂点とした操作とがキレイに分離されてますね。これでどうにかエラソーな顔して1時間のレクチャができそうです。

