タイプスクリプトのビジターパターン


あなたが別の図形を描くことができるプログラムを書いていることを想像してください:サークル、三角形、正方形など、対応するデータ型でそれらを表します.使用する言語によっては、これらの図形は異なるクラス、構造体、いくつかの代数的データ型の列挙体または部分のメンバーになります.あなたもこれらの形で何かをすることができるので、あなたはその振舞いをどこかに述べます.たとえば、クラスとして図形を表現することを選択した場合、動作はこれらのクラスのメソッドとして記述できます.つの基本的な動作をサポートすることを決定すると仮定します.
この記事の目的は、あなたの形をドロドロにし、一日を呼び出すいくつかの簡単な方法を示すことではありません.代わりに、そのようなプログラムのアウトラインを実装する前に、どのように我々のコードを構成できるかを考えてみましょう.我々の形と彼らの(可能な)ふるまいの単純なテーブルから始めましょう.

我々のコードのどこかで我々はちょうど電話したいですdraw() そして、それは魔法のように、現在選択されているオプションに応じて正しい形を描画します.我々は確かに我々はユーザーのクリックを処理するのと同じ場所に図面の詳細を自分自身に懸念したくない.しかし、各々の形は異なって描かれます、それで、我々は3回行動を記述する必要があります-各々の形のために一度.4つの図形があれば、4回の振る舞いを記述する必要があります.
私が言ったように、正確に我々がこの行動を記述しなければならないところでは、我々が選ぶ言語に依存します.いくつかの言語では、これを行うには1つ以上の方法を許可し、どちらが良いかを決定する簡単なタスクです.それは「表現問題」とさえ呼ばれました、そして、ボブNystromはそうしましたa nice short explanation in his book Crafting Interpreters . 「問題」とは、私たちが多くの形と多くの振舞いを持っているとき、いくつかの言語が新しい形を加えるだけでたくさんの仕事をする必要があるということです.容易な妥協はない.しかし、(おそらく既に推測しているように)このような状況で私たちの生活を容易にするデザインパターンがあります.
JavaScriptとTypesScriptは他のものより少し自由を私たちに与えるそれらの言語の中にあります.今日、私はタイプスクリプトについて特に話をしたいと思います.なぜなら、ビジターパターンのようなタイプセーフなパターンが可能であり、それ以外のダイナミックな言語で役に立つからです.
だから、typescriptで我々は我々が望むものを達成するための束の束がありますが、それらのすべては良いです.私たちには、我々の形を表す3つのクラスがあると仮定します.
class Square {}
class Circle {}
class Triangle {}

type Shape = Square | Circle | Triangle
悪い方法は、1つだけを持つことですdraw() 特定の図形を描画する機能と使用条件
function draw(shapes: Array<Shape>) {
  for (const shape of shapes) {
    if (shape instanceof Square) {
      // draw Square
    } else if (shape instanceof Circle) {
      // draw Circle
    }
  }
}
このアプローチの問題はタイプセーフではないということです.コンパイラは、私たちがTriangle ケース.これにより、aを描画しようとするとランタイムエラーが発生しますTriangle . HaskellやRustのようなパターンマッチング機能を持つ言語では、コンパイラは未処理の場合について警告します.
つのタイプセーフ代替はDrawable インターフェイス.interface ここでは、大抵他の多くのOOP言語でも同じことを意味します.
interface Drawable {
  draw: () => void
}
我々がわずかに我々を変えるならばdraw 配列を期待する関数Drawable 物事だけでなくShape s、それが実装しない何かを含む配列を通過しようとするならば、我々はコンパイルエラーを得ますdraw() .
class Square {
  draw() {}
}

class Triangle {}

function draw(shapes: Array<Drawable>) {
  for (const shape of shapes) {
    shape.draw() // Square, etc...
  }
}

draw([new Triangle()]) // Compile error!
良い.我々があらゆる形を強制するならば、さらによりよいimplement もう一つの良いことは、typescriptで可能です!
class Square implements Drawable {
  draw() {}
}

class Circle implements Drawable {
  draw() {}
}

class Triangle implements Drawable {
  draw() {}
}
いくつかの点で想像してくださいarea() . これは、私が上記の「表現問題」に遭遇するところです.まず新しいインターフェースを定義する必要があります.
interface Area {
  area: () => number
}
そして、それぞれの形状に加えて、それを実装するDrawable !
class Square implements Drawable, Area {
  draw() {}
  area() {}
}

class Triangle implements Drawable, Area {
  draw() {}
  area() {}
}
// omitted
それで、どのように我々は我々が新しい振舞いを加える一回ごとに触れなければならないコードの量を減らすことができますか?どのように我々は特定の共有上の特定の動作を処理することを忘れていないことを確認することができますか?訪問者のパターンを満たす.

ビジターパターン


このパターンを説明する方法はたくさんあります.記事の冒頭で与えた悪い例の観点から理解しやすいと思います.ここで繰り返してみましょう.
function draw(shapes: Array<Shape>) {
  for (const shape of shapes) {
    if (shape instanceof Square) {
      // draw Square
    } else if (shape instanceof Circle) {
      // draw Circle
    }
  }
}
つの場所ですべての可能なケースをグループ化する方法があったならば、ちょうど我々が1つの機能でグループ化された条件のように?次のような方法があります.
interface ShapeVisitor {
  visitCircle(shape: Circle): void
  visitSquare(shape: Square): void
  visitTriangle(shape: Triangle): void
}
visit 奇妙な言葉ですが、この文脈では基本的に「ハンドル」を意味します.ちょうどあなたが文句を言いたい場合には、私がパターンを考え出した人でないということを知っていてください.さて、このインターフェイスを実装するクラスは、図形を描画するために必要な具体的な手順を記述するすべてのメソッドを持たなければならないクラスです.一部のクラスがこれらの「ハンドラ」のすべてを実装するのを確実にするために、我々はちょうどimplement キーワード.クラスのみimplement 私たちがクラスをつくる関数の代わりにtypescriptのもの.Drawer , 誰の責任を描画することです.
class Drawer implements ShapeVisitor {
  visitCircle(shape: Circle) {}

  visitSquare(shape: Square) {}

  visitTriangle(shape: Triangle) {}
}
ここでの目標は、各クラスに新しい行動を加える必要性を取り除くことです.これは古いインターフェースですDrawabledraw メソッドは動作しません.変えましょうDrawable インタフェース:
interface Drawable {
  accept(visitor: ShapeVisitor): void
}
accept ? それはこのパターンのちょうど別の慣例です.あなたが欲しいものは何でも名前を付けることができますaccept それはあなたがパターンに従うしようとしていることを明確にします.このメソッドの仕事は、訪問者を取るし、この特定の図形を描画するために使用する訪問者のメソッドのいずれかを選択することです.実装しましょうDrawable 我々の形の一つに
class Square implements Drawable {
  accept(visitor: ShapeVisitor) {
    visitor.visitSquare(this)
  }
}

// similar for every other shape
これは最終的に私たちを追加することができますdraw メソッドDrawer .
class Drawer implements ShapeVisitor {
  /* visit functions */

  draw(shape: Drawable) {
    shape.accept(this)
  }
}
かなり多くの間接的ですが、うまくいけば、現在、あなたはそれがどのように働くかについて見ます.コードのどこかで、次のような形を描きます.
const drawer = new Drawer()
drawer.draw(new Square())
もう一つの形をサポートすると決めたら、例えばStar , 私たちは、この新しいクラスにあらゆる可能な振舞いのためにコードを加える必要はありません.その代わりに、我々はそれを目に見えるようにして、そして、関連する訪問客の詳細を実行します.もちろん、訪問者は新しい方法を必要とするでしょうvisitStar . 我々は、インターフェイスに追加を開始しますShapeVisitor すべてのクラスを確認するimplements それはvisitStar メソッド.
interface ShapeVisitor {
  visitCircle(shape: Circle): void
  visitSquare(shape: Square): void
  visitTriangle(shape: Triangle): void
  visitStar(shape: Star): void
}
これは、我々が条件の束で持つことができなかったタイプ安全です.
名前visit and accept あなたが何が起こっているかを描くならば、しかし、完全にランダムではありません.

時々、コード全体を読むのが一番いいので、ここまで書いたものです.
interface Drawable {
  accept(visitor: ShapeVisitor): void
}

interface ShapeVisitor {
  visitCircle(shape: Circle): void
  visitSquare(shape: Square): void
  visitTriangle(shape: Triangle): void
}

class Drawer implements ShapeVisitor {
  visitCircle(shape: Circle) {}

  visitSquare(shape: Square) {}

  visitTriangle(shape: Triangle) {}

  draw(shape: Drawable) {
    shape.accept(this)
  }
}

class Square implements Drawable {
  accept(visitor: ShapeVisitor) {
    visitor.visitSquare(this)
  }
}

class Circle implements Drawable {
  accept(visitor: ShapeVisitor) {
    visitor.visitCircle(this)
  }
}

class Triangle implements Drawable {
  accept(visitor: ShapeVisitor) {
    visitor.visitTriangle(this)
  }
}
あなたは、呼び出しをする必要がないことに気づいたかもしれませんDrawable インターフェースDrawable . それは本当です.ShapeVisitor は、多くの異なるクラスDrawer でもFilesystem or Animate または何でも.我々はできるようになりたいaccept すべての形のクラスを編集せずにそれらのすべて.そういうわけで、それはちょうどそれを呼ぶだけの意味があるでしょうVisitableShape または何か.

警告


あなたが鋭い読者であるならば、あなたはおそらく何も我々がこれをするのを妨げると気がつきませんでした:
class Triangle implements Drawable {
  accept(visitor: ShapeVisitor) {
    visitor.visitSquare(this) // Attention here.
  }
}
私はいくつかの他の言語のような箱から動作すると予想しました、しかし、それはそうしませんでした.