nikkie-ftnextの日記

イベントレポートや読書メモを発信

読書&写経ログ | #ちょうぜつ本 第8章 Visitorパターン 〜Sphinx拡張開発の経験、そういうことだったのか!〜

はじめに

変更しやすいコードが書けないのにソフトウェア開発とか舐めているのですか

天使様、ごめんなさい〜、nikkieです1

「かわいい」と技術書が夢の合体を果たした、ちょうぜつ本(『ちょうぜつソフトウェア設計入門』2)!🤗
昨年から読書会を共同主催しており、現在は第8章「デザインパターン」を読み進めています。
直近読んだ範囲から、Visitorを取り上げます。来訪者編です!

目次

前回のちょうぜつ本!

前から順に読み進めて第8章に突入し、読書会では範囲を細かくしてじっくりと読み進めています。

Compositeパターンは、自己再帰的なデータ構造。
複数と単数の同一視は、頭いいですね。

Visitorパターン

Compositeパターンのデータの各要素を操作する場合に、visitor(訪問者)を渡して訪問者に要素の操作をしてもらいます

  • データ構造の各要素は、訪問者を受け入れるようにしておくだけ
    • 自身を訪問者に渡して、訪問者が要素を操作する
  • 操作を拡張可能にできる(要素を操作する訪問者を拡張して渡せばよい)

操作対象が操作の詳細を決められないとき、Visitor を受け入れられるようにだけしておけば、操作の詳細を除いて先に安定させてしまえる、というのがこのパターンです。

公開時期的にもマッチしたサンタクロースのたとえです(煙突はvisitor(サンタクロース)をaccept)。

今回はピンときたVisitorパターン

私は木構造のデータに関心があり(Sphinx拡張3や抽象構文4)、Visitorパターン自体は名前を知っていました。
Visitorという概念をつかもうと、手を動かすのと並行して結城先生本を紐解きもしましたが、いまいちピンときていませんでした。
ちょうぜつ本のコードは具体のVisitorには全く言及していないがゆえに、私には分かりやすかったです(登場人物が多くて私が混乱していたふしがあります)

データ構造

  • Node
    • accept()メソッドでvisitorを受け入れる
      • 自身をvisitorにどう渡すかだけ実装
  • Branch
    • Nodeを継承=BranchNodeである
    • accept()メソッドでvisitorを受け入れる
      • 自身と子要素をvisitorにどう渡すかだけ実装
class Node:
    def accept(self, visitor) -> None:
        visitor(self)


class Branch(Node):
    def __init__(self, *args: Node) -> None:
        self.children = list(args)

    def accept(self, visitor) -> None:
        super().accept(visitor)

        for child in self.children:
            child.accept(visitor)

visitorの実装は読者に委ねられていたので、要素をprint()するだけとしました。

class ExampleVisitor:
    def __call__(self, acceptable: VisitorAcceptable) -> None:
        print(acceptable)

データ構造を用意します。

classDiagram
    root *-- node
    root *-- branch
    branch *-- left
    branch *-- right

root.accept(ExampleVisitor())rootにvisitorを渡して呼び出すだけで、visitorが各要素を処理します!

<__main__.Branch object at 0x103143210>
<__main__.Node object at 0x1031431d0>
<__main__.Branch object at 0x103143150>
<__main__.Node object at 0x1031430d0>
<__main__.Node object at 0x103143110>

結城先生本では、要素がvisitorを受け入れて自身をvisitorに渡し、visitorが各要素を処理するという部分が当時の私には理解しきれませんでした(まだるっこしいという誤解をしました)。
ちょうぜつ本の例はvisitorの実装に言及していないので、「こういうデータ構造にvisitorを渡すのか」と気づけて、私にはブレイクスルーとなりました。

Sphinx(やdocutils)とつながりました!

(ここのトピック、詳しくは別記事としたいと思います)

docutils(バージョン 0.20.1)のソースコード(docutils/nodes.py)を覗くと、Visitorパターンがありました。

class Node:
    def walk(self, visitor):
        # 省略  # 自身をvisitorにどう渡すかを実装している

    def walkabout(self, visitor):
        # 省略


class NodeVisitor:

    """
    "Visitor" pattern [GoF95]_ abstract superclass implementation for
    document tree traversals.

    省略
    """

    def dispatch_visit(self, node):
        # 省略  # Nodeの操作を実装している

    def dispatch_departure(self, node):
        # 省略

また、SphinxではdocutilsのNodeVisitorを継承したクラスがトランスレイター5です!
https://github.com/sphinx-doc/sphinx/blob/v7.2.6/sphinx/builders/__init__.py#L62

from docutils import nodes

class Builder:
    default_translator_class: type[nodes.NodeVisitor]

つまり、Sphinx拡張を書くとき、私は知らず知らず(さらに理解至らず至らず)のうちにVisitorパターンを使っていたのです!

Sphinxはユーザが要素を追加できるんですよね。
ユーザが追加する要素への操作は、docutilsやSphinx側のソースコードにあらかじめ定義しておけません。
要素を追加したユーザがvisitor(トランスレイター)も合わせて追加することで成り立っているという理解です。
つまり、要素を追加できるというSphinxの拡張性は、Visitorパターンに支えられていると思われます。

また、ユーザが要素を拡張して作ったデータ構造について、要素の取り出し方(イテレータ)を事前に定義するのも難しそうです。
どの要素にもvisitorを渡すことで操作するというのは、とてもかしこいですね!

終わりに

ちょうぜつ本 8章よりVisitorパターンでした。
データ構造の各要素は訪問者を受け入れるようにしておき、訪問者が要素を操作します。
これにより、操作の詳細は訪問者で定義すればよく、拡張も可能になります(Sphinxの例)

来訪者編は、いいぞ!


  1. ちょうぜつ本読書ログシリーズではおなじみのこちらの書き出し。元は『お隣の天使様にいつの間にか駄目人間にされていた件』の「家事ができないのに一人暮らしとか舐めているのですか」です。配信では1話の17:40くらいです! 天使様は2024年の干支だと思っています(過激派)
  2. 技術書界のきららとして知られます。アニメ化してほしいな〜
  3. 応援ください!
  4. 一例です
  5. ビルダーとトランスレイター