Visitorパターン
概要
共通のアルゴリズムを対象から切り出して管理できる
共通の処理だが、結合度が高すぎる処理を切り出して一括管理することができる
使い方
bad
ケース1: 結合度の高い共通の処理がそれぞれのクラスに実装されてる
interface Model {
toDatabaseLog: () => string;
toPrometheusLog: () => string;
}
class UserModel implements Model {
//...
toDatabaseLog() {/* ... */}
toPrometheusLog() {/* ... */}
}
class PostModel implements Model {
//...
toDatabaseLog() {/* ... */}
toPrometheusLog() {/* ... */}
}
UserModel / PostModel がDBやPrometheusという外部の関心事まで知っており、責務が混在している
外部の関心が関係ないModel固有の処理(toJsonとか)であれば、この設計で問題ない
ケース2: 結合度の高い処理を分離できたが、クラスが分離先の処理を知りすぎている
class DatabaseLogVisitor {
fromUserModel(model: UserModel) {
//...
}
fromPostModel(model: PostModel) {
//...
}
}
const databaseLogVisitor = new DatabaseLogVisitor()
models.forEach((model) => {
if(model instanceof UserModel) {
databaseLogVisitor.fromUserModel(model)
} else if {
databaseLogVisitor.fromPostModel(model)
}
})
処理自体は分離できているが、呼び出し側がその型を判断している
つまり、型に合わせた適切なメソッドを呼び出す責務が呼び出し側に漏れてしまう
databaseLog, prometheusLogなどいろいろな共通処理が増えたときの記述が冗長になっていく
good
interface Visitor<T> {
visitUserModel: (model: UserModel) => T;
visitPostModel: (model: UserModel) => T;
}
class UserModel {
//...
accept(visitor: Visitor) {
return visitor.visitUserModel(this);
}
}
class PostModel {
//...
accept(visitor: Visitor) {
return visitor.visitPostModel(this);
}
}
class DatabaseLogVisitor implements Visitor<string> {
visitUserModel(model) {
//...
}
visitPostModel(model) {
//...
}
}
class PrometheusLogVisitor implements Visitor<string> {
visitUserModel(model) {
//...
}
visitPostModel(model) {
//...
}
}
const userModel = new UserModel();
const dbLogVisitor = new DatabaseLogVisitor();
const log = userModel.accept(dbLogVisitor);
型に合わせた適切なvisitorを呼び出す責務を型側に持たせる。
visitor処理を共通のインターフェースで統一することで、処理内容を増やしやすくする
使い所
- 複数の型で横断的に共通の目的の処理があるが、それぞれのクラスに書いたら結合度が高くなりすぎてしまうケース
- それぞれのクラスに書いたら外の情報を知りすぎてる
- 上のようなケースの処理が増える可能性があるケース
- 分岐する型も増えるケースではvisitor全てを変更する必要が出てくるため、(modelという分岐しやすいものを例にしたが、)比較的に分岐する型が少ない方が向いている