ビジターパターンとは、アルゴリズムとオブジェクト構造を分離するソフトウェア設計パターンです。この分離により、既存のオブジェクト構造を変更することなく、新しい操作を追加できます。これは、オブジェクト指向プログラミングとソフトウェアエンジニアリングにおけるオープン/クローズドの原則に従う方法の一つです。
本質的に、ビジタークラスは、既存のクラスを変更することなく、クラス群に新しい仮想関数を追加することを可能にします。代わりに、仮想関数の適切な特殊化をすべて実装するビジタークラスが作成されます。ビジタークラスはインスタンス参照を入力として受け取り、ダブルディスパッチによって目的の処理を実行します。
和型とパターンマッチングを備えたプログラミング言語では、ビジターパターンの利点の多くが不要になります。これは、ビジタークラスがオブジェクトの型に基づいて簡単に分岐できるだけでなく、ビジターがまだ処理していない新しいオブジェクト型が定義された場合にコンパイラエラーを生成できるためです。
ビジター[ 1 ] デザインパターンは、23のGang of Fourデザインパターンの1つです。
新しい操作が頻繁に必要になり、オブジェクト構造が多くの無関係なクラスで構成されている場合、新しい操作が必要になるたびに新しいサブクラスを追加するのは柔軟性に欠けます。なぜなら、「これらの操作すべてをさまざまなノードクラスに分散すると、理解、保守、変更が困難なシステムになる」からです。[ 1 ]
これにより、新しいビジターオブジェクトを追加することで、オブジェクト構造のクラスとは独立して新しい操作を作成することが可能になります。
下記のUMLクラス図およびシーケンス図も参照してください。
ギャング・オブ・フォーは、ビジターを次のように定義している。
オブジェクト構造の要素に対して実行される操作を表します。Visitorを使用すると、操作対象となる要素のクラスを変更することなく、新しい操作を定義できます。
ビジターの性質上、公開APIに組み込むのに理想的なパターンであり、クライアントはソースコードを変更することなく、「ビジティング」クラスを使用してクラスに対する操作を実行できます。[ 2 ]
業務をビジタークラスに移管すると、次のような場合に有益です。
visitしかし、このパターンには欠点があり、新しいクラスを追加するには、通常、各ビジターに新しいメソッドを追加する必要があるため、クラス階層の拡張が難しくなります。
2次元コンピュータ支援設計(CAD)システムの設計を考えてみましょう。その中核には、円、線、円弧といった基本的な幾何学的形状を表すためのいくつかのタイプがあります。これらのエンティティはレイヤーに整理され、タイプの階層の最上位には図面があります。図面は、単にレイヤーのリストにいくつかの追加プロパティを加えたものです。
この型階層における基本的な操作は、図面をシステムのネイティブファイル形式で保存することです。一見すると、階層内のすべての型にローカル保存メソッドを追加するのが望ましいように思えるかもしれません。しかし、図面を他のファイル形式で保存することも有用です。さまざまなファイル形式への保存メソッドを次々と追加していくと、元の幾何データ構造が複雑化する可能性があります。
この問題を解決する単純な方法は、ファイル形式ごとに個別の関数を用意することです。このような保存関数は、入力として図面を受け取り、それを走査して、特定のファイル形式にエンコードします。この処理をファイル形式ごとに繰り返すと、関数間の重複が蓄積されます。たとえば、円形をラスター形式で保存する場合、使用するラスター形式に関係なく、非常に似たコードが必要となり、他のプリミティブ形状とは異なります。線や多角形などの他のプリミティブ形状の場合も同様です。そのため、コードはオブジェクトを走査する大きな外側のループと、ループ内部でオブジェクトのタイプを照会する大きな決定木で構成されます。このアプローチのもう1つの問題は、1つ以上の保存関数で形状を見落としたり、新しいプリミティブ形状が導入されても、保存ルーチンが1つのファイルタイプにしか実装されておらず、他のファイルタイプには実装されていないため、コードの拡張と保守の問題が発生する可能性があることです。同じファイルのバージョンが増えるにつれて、保守はより複雑になります。
代わりに、ビジターパターンを適用できます。このパターンでは、階層全体に対する論理演算(つまり save(image_tree))を、ツリーを走査するための共通メソッドを実装し、フォーマット固有の動作のために実装される仮想ヘルパーメソッド(つまり save_circle、save_square など)を記述する 1 つのクラス(つまり Saver)にエンコードします。CAD の例の場合、このようなフォーマット固有の動作は、Visitor のサブクラス(つまり SaverPNG)によって実装されます。このようにして、型チェックと走査手順の重複がすべて解消されます。さらに、共通の基本走査/保存関数でシェイプが想定されるようになったため、シェイプが省略された場合はコンパイラがエラーを出力します。
ビジターパターンは、イテレータパターンと同様に、コンテナのようなデータ構造の反復処理に使用できますが、機能は限定されています。[ 3 ] : 288例えば、ディレクトリ構造の反復処理は、より一般的なループパターンではなく、関数クラスで実装できます。これにより、反復処理コードを再利用しながら、各項目にビジター機能を実装することで、ディレクトリの内容からさまざまな有用な情報を抽出できます。これはSmalltalkシステムで広く採用されており、C++でも見られます。[ 3 ] : 289ただし、このアプローチの欠点は、ループから簡単に抜け出したり、並行して反復処理(つまり、単一の変数で同時に2つのコンテナを走査する)を実行したりできないことです。[ 3 ] : 289後者を実現するには、ビジターにこれらの機能をサポートする追加機能を記述する必要があります。[ 3 ] : 289i

上記のUMLクラス図では、ElementAクラスは新しい操作を直接実装しません。代わりに、リクエストを「承認されたビジターオブジェクト」()に「ディスパッチ」(委任)するディスパッチ操作ElementAを実装します。クラスは操作()を実装します。 次に、にディスパッチすることでを実装します。クラスは操作()を実装します。accept(visitor)visitor.visitElementA(this)Visitor1visitElementA(e:ElementA)ElementBaccept(visitor)visitor.visitElementB(this)Visitor1visitElementB(e:ElementB)
UMLシーケンス図は、 実行時の相互作用を示しています。オブジェクトはオブジェクト構造 ( )の要素を走査し、各要素に対して を呼び出します。 まず、はを 呼び出し、 は受け入れられたオブジェクトに対して を呼び出します。要素自体 ( ) は に渡され、が を「訪問」 ( を呼び出す)できるようにします。 その後、は を 呼び出し、 は を「訪問」( を呼び出す)するを呼び出します。ClientElementA,ElementBaccept(visitor)Clientaccept(visitor)ElementAvisitElementA(this)visitorthisvisitorElementAoperationA()Clientaccept(visitor)ElementBvisitElementB(this)visitorElementBoperationB()


ビジターパターンでは、一般的なオブジェクト指向言語(C++、Java、Smalltalk、Objective-C、Swift、JavaScript、Python、C#など)のように、シングルディスパッチをサポートするプログラミング言語が必要です。この条件の下で、それぞれ何らかのクラス型を持つ2つのオブジェクトを考えます。一方を要素、もう一方をビジターと呼びます。
ビジターはvisit、要素の各クラスに対して、要素を引数として受け取るメソッドを宣言します。具体的なビジターはビジタークラスから派生し、これらのvisitメソッドを実装します。各メソッドは、オブジェクト構造に対して動作するアルゴリズムの一部を実装します。アルゴリズムの状態は、具体的なビジタークラスによってローカルに維持されます。
この要素はaccept、ビジターを引数として受け取り、ビジターを受け入れるメソッドを宣言します。要素クラスから派生した具象要素はaccept、このメソッドを実装します。最も単純な形式では、これはビジターのメソッドを呼び出すだけですvisit。子オブジェクトのリストを保持する複合要素は、通常、これらのリストを反復処理し、各子オブジェクトのメソッドを呼び出しますaccept。
クライアントは、直接的または間接的にオブジェクト構造を作成し、具体的なビジターをインスタンス化します。ビジターパターンを使用して実装された操作を実行する場合、クライアントはaccept最上位要素のメソッドを呼び出します。
プログラム内でメソッドが呼び出されるとaccept、その実装は要素の動的型とビジターの静的型の両方に基づいて選択されます。関連付けられたvisitメソッドが呼び出されると、その実装はビジターの動的型と要素の静的型の両方に基づいて選択されます。静的型acceptはメソッドの実装内で確認でき、要素の動的型と同じです。(補足として、ビジターが指定された要素の型の引数を処理できない場合、コンパイラがエラーを検出します。)
したがって、メソッドの実装は、visit要素の動的型とビジターの動的型の両方に基づいて選択されます。これにより、実質的にダブルディスパッチが実装されます。オブジェクトシステムがシングルディスパッチだけでなくマルチディスパッチもサポートする言語(Common LispやDynamic Language Runtime (DLR)経由のC#など)では、単純な関数オーバーロードを使用して訪問されるすべてのケースをカバーできるため、ビジターパターンの実装が大幅に簡素化されます(別名ダイナミックビジター)。ダイナミックビジターは、公開データのみを操作する限り、オープン/クローズドの原則(既存の構造を変更しないため)と単一責任の原則(ビジターパターンを別のコンポーネントで実装するため)に準拠します。
このようにして、要素のグラフを走査するアルゴリズムを1つ記述することができ、要素とビジターの動的な型に基づいて、要素と相互作用するさまざまな種類のビジターを提供することで、走査中にさまざまな種類の操作を実行できます。
ExpressionPrintingVisitorこの例では、印刷処理を担う別のクラスを宣言しています。新しい具体的なビジターを導入する場合は、Visitorインターフェースを実装する新しいクラスが作成され、Visitメソッドの新しい実装が提供されます。既存のクラス(LiteralとAddition)は変更されません。
名前空間Wikipedia.Examples ;Systemを使用します。interface IVisitor { void Visit ( Literal literal ); void Visit ( Addition addition ); }class ExpressionPrintingVisitor : IVisitor { public void Visit ( Literal literal ) { Console . WriteLine ( literal . Value ); }public void Visit ( Addition addition ) { double leftValue = addition . Left . GetValue (); double rightValue = addition . Right . GetValue (); double sum = addition . GetValue (); Console . WriteLine ( $"{leftValue} + {rightValue} = {sum}" ); } }abstract class Expression { public abstract void Accept ( IVisitor visitor ); public abstract double GetValue (); }class Literal : Expression { public Literal ( double value ) { this . Value = value ; }public double Value { get ; set ; }public override void Accept ( IVisitor visitor ) { visitor.Visit ( this ) ; } public override double GetValue ( ) { return Value ; } }class Addition : Expression { public Addition ( Expression left , Expression right ) { Left = left ; Right = right ; }public Expression Left { get ; set ; } public Expression Right { get ; set ; }public override void Accept ( IVisitor visitor ) { Left.Accept ( visitor ) ; Right.Accept ( visitor ) ; visitor.Visit ( this ) ; } public override double GetValue ( ) { return Left.GetValue ( ) + Right.GetValue ( ) ; } }public static class Program { public static void Main ( string [] args ) { // 1 + 2 + 3 の加算をエミュレートしますe = new ( new Addition ( new Literal ( 1 ), new Literal ( 2 ) ), new Literal ( 3 ) );ExpressionPrintingVisitor printingVisitor = new (); e . Accept ( printingVisitor ); Console . ReadKey (); } }この場合、ストリーム上に自身を出力する方法を知るのはオブジェクト自身の責任である。したがって、ここでのビジターはストリームではなく、オブジェクトである。
「クラスを作成するための構文はありません。クラスは、他のクラスにメッセージを送信することによって作成されます。」WriteStreamサブクラス: #ExpressionPrinter instanceVariableNames: '' classVariableNames: '' package: 'Wikipedia' .ExpressionPrinter >>write: anObject "オブジェクトにアクションを委譲します。オブジェクトは特別なクラスである必要はありません。メッセージ #putOn:" anObject putOn: self . ^ anObject を理解できるだけで十分です。オブジェクトサブクラス: #Expression instanceVariableNames: '' classVariableNames: '' package: 'Wikipedia' 。式のサブクラス: #Literal instanceVariableNames: 'value' classVariableNames: '' package: 'Wikipedia' 。Literalクラス>>with: aValue "Literal クラスのインスタンスを構築するためのクラス メソッド" ^ self new value: aValue ; yourself 。リテラル>>value: aValue "値のセッター" value := aValue 。Literal >>putOn: aStream "Literal オブジェクトは自身を出力する方法を知っている" aStream nextPutAll: value asString 。式のサブクラス: #Addition instanceVariableNames: 'left right' classVariableNames: '' package: 'Wikipedia' .Additionクラス>>left: a right: b "Addition クラスのインスタンスを構築するためのクラス メソッド" ^ self new left: a ; right: b ; yourself .追加>>left: anExpression "left のセッター" left := anExpression .追加>>right: anExpression "right のセッター" right := anExpression .追加>>putOn: aStream "追加オブジェクトは、自身を出力する方法を知っている" aStream nextPut: $( . left putOn: aStream . aStream nextPut: $+ . right putOn: aStream . aStream nextPut: $) .オブジェクトサブクラス: #Program instanceVariableNames: '' classVariableNames: '' package: 'Wikipedia' 。プログラム>> main | expression stream | expression := Addition left: ( Addition left: ( Literal with: 1 ) right: ( Literal with: 2 )) right: ( Literal with: 3 ) . stream := ExpressionPrinter on: ( String new: 100 ) . stream write: expression . Transcript show: stream contents . Transcript flush .Go はメソッドのオーバーロードをサポートしていないため、訪問メソッドには異なる名前が必要です。典型的なビジターインターフェースは次のようになります。
type Visitor interface { visitWheel ( wheel Wheel ) string visitEngine ( engine Engine ) string visitBody ( body Body ) string visitCar ( car Car ) string }次の例はJava言語で記述されており、ノードツリーの内容 (この場合は車のコンポーネントを記述) を印刷する方法を示しています。print各ノードサブクラス ( Wheel、Engine、 、BodyおよびCar) ごとにメソッドを作成する代わりに、1 つのビジタークラス ( CarElementPrintVisitor) が必要な印刷アクションを実行します。異なるノードサブクラスでは、適切に印刷するためにわずかに異なるアクションが必要となるため、 は、CarElementPrintVisitorメソッドに渡される引数のクラスに基づいてアクションをディスパッチしますvisit。CarElementDoVisitor異なるファイル形式の保存操作に相当する も同様です。

パッケージorg.wikipedia.examples ;import java.util.List ;interface CarElement { void accept ( CarElementVisitor visitor ); }interface CarElementVisitor { void visit ( Body body ); void visit ( Car car ); void visit ( Engine engine ); void visit ( Wheel wheel ); }class Wheel implements CarElement { private final String name ;public Wheel ( final String name ) { this.name = name ; }public String getName () { return name ; }@Override public void accept ( CarElementVisitor visitor ) { /* * Wheel の accept(CarElementVisitor) は CarElement の accept(CarElementVisitor) を実装しているため、 * accept の呼び出しは 実行時にバインドされます。これは * 最初のディスパッチと考えることができます。ただし、 * visit(Wheel) を呼び出すかどうかの決定 ( visit(Engine) などではなく) は、 コンパイル時に 'this' が Wheel であることがわかっているため、 * コンパイル時に行うことができます。さらに、 * CarElementVisitor の各実装は visit(Wheel) を実装しており、これは* 実行時に行われる別の決定です。これは * 2 番目のディスパッチと考える ことができます。 */ visitor . visit ( this ); } }class Body implements CarElement { @Override public void accept ( CarElementVisitor visitor ) { visitor . visit ( this ); } }class Engine implements CarElement { @Override public void accept ( CarElementVisitor visitor ) { visitor . visit ( this ); } }class Car implements CarElement { private final List < CarElement > elements ;public Car ( ) { this.elements = List.of ( new Wheel ( " front left" ), new Wheel ( "front right " ), new Wheel ( "back left" ), new Wheel ( " back right" ), new Body (), new Engine () ); }@Override public void accept ( CarElementVisitor visitor ) { for ( CarElement element : elements ) { element.accept ( visitor ) ; } visitor.visit ( this ) ; } }class CarElementDoVisitor implements CarElementVisitor { @Override public void visit ( Body body ) { System . out . println ( "Moving my body" ); }@Override public void visit ( Car car ) { System . out . println ( "Starting my car" ); }@Override public void visit ( Wheel wheel ) { System . out . printf ( "Kicking my %s wheel%n" , wheel . getName ()); }@Override public void visit ( Engine engine ) { System . out . println ( "エンジンの始動" ); } }class CarElementPrintVisitor implements CarElementVisitor { @Override public void visit ( Body body ) { System . out . println ( "Visiting body" ); }@Override public void visit ( Car car ) { System.out.println ( " Visiting car " ) ; }@Override public void visit ( Engine engine ) { System.out.println ( "エンジンを訪問しています" ) ; }@Override public void visit ( Wheel wheel ) { System . out . printf ( "Visiting %s wheel%n" , wheel . getName ()); } }public class VisitorDemo { public static void main ( String [] args ) { Car car = new Car ();car.accept ( new CarElementPrintVisitor ()) ; car.accept ( new CarElementDoVisitor ( ) ) ; } }左前輪を訪問 右前輪を訪問 左後輪を訪れる 右後輪を訪れる 訪問団体 訪問エンジン 訪問車 左前輪を蹴る 右前輪を蹴る 左後輪を蹴る 右後輪を蹴る 体を動かす エンジンを始動する 車を始動する
( defclass auto () (( elements :initarg :elements )))( defclass auto-part () (( name :initarg :name :initform "<unnamed-car-part>" )))( defmethod print-object (( p auto-part ) stream ) ( print-object ( slot-value p 'name ) stream ))( defclass wheel ( auto-part ) ())( defclass body ( auto-part ) ())( defclass engine ( auto-part ) ())( defgeneric traverse ( function object other-object ))( defmethod traverse ( function ( a auto ) other-object ) ( with-slots ( elements ) a ( dolist ( e elements ) ( funcall function e other-object ))));; 何かをする訪問;; 全てをキャッチ( defmethod do-something ( object other-object ) ( format t "~s と ~s がどのように相互作用すべきかわかりません~%" object other-object ));; ホイールと整数を含む訪問( defmethod do-something (( object wheel ) ( other-object integer )) ( format t "ホイールを ~s ~s 回~% 蹴る" object other-object ));; ホイールとシンボルを含む訪問( defmethod do-something (( object wheel ) ( other-object symbol )) ( format t "シンボル ~s~% を使用してホイール ~s を象徴的にキックします" object other-object ))( defmethod do-something (( object engine ) ( other-object integer )) ( format t "エンジンを ~s ~s 回 ~% 起動します" object other-object ))( defmethod do-something (( object engine ) ( other-object symbol )) ( format t "エンジン ~s をシンボル ~s~% を使用してシンボル的に起動します" object other-object ))( let (( a ( make-instance 'auto :elements ` ( , ( make-instance 'wheel :name "front-left-wheel" ) , ( make-instance 'wheel :name "front-right-wheel" ) , ( make-instance 'wheel :name "rear-left-wheel" ) , ( make-instance 'wheel :name "rear-right-wheel" ) , ( make-instance 'body :name "body" ) , ( make-instance 'engine :name "engine" ))))) ;; 要素を出力するためにトラバースします;; ここでは、ストリーム *standard-output* が other-object の役割を果たします( traverse #' *standard-output* を出力します)( terpri ) ;; 改行を出力;; 他のオブジェクトから任意のコンテキストでトラバース( traverse #' do-something a 42 );; 他のオブジェクトから任意のコンテキストでトラバースする( traverse #' do-something a 'abc ))「左前輪」 「右前輪」 「後輪左」 「右後輪」 "体" "エンジン" 左前輪を42回蹴る 右前輪を42回蹴る 後輪(左後輪)を42回蹴る 右後輪を42回蹴る 「body」と「42」がどのように相互作用すべきかはわかりません エンジン「エンジン」を42回起動 左前輪を蹴る様子を、記号ABCを用いて象徴的に表現する。 キックホイール「右前輪」を記号ABCを用いて象徴的に表現する 左後輪を蹴る様子を象徴的にABC記号で表す 後輪を蹴る「右後輪」を象徴的にABC記号で表す 「体」とABCがどのように相互作用すべきか分からない エンジンの始動を記号ABCを用いて象徴的に表す。
では、このother-objectパラメータは不要ですtraverse。理由は、字句的にキャプチャされたオブジェクトを使用して目的のターゲットメソッドを呼び出す匿名関数を使用できるためです。
( defmethod traverse ( function ( a auto )) ;; 他のオブジェクトを削除( with-slots ( elements ) a ( dolist ( e elements ) ( funcall function e )))) ;; ここからも;; ...;; print-traverse の別の方法( traverse ( lambda ( o ) ( print o *標準出力* )) a );; a の要素と整数 42 を使って何かを行う別の方法( traverse ( lambda ( o ) ( do-something o 42 )) a )多重ディスパッチは匿名関数の本体から発行される呼び出しで行われるため、これはtraverseオブジェクトの要素に関数適用を分散するマッピング関数にすぎません。したがって、マッピング関数を除いて、ビジターパターンの痕跡はすべて消え去ります。マッピング関数には、2つのオブジェクトが関与している証拠は一切ありません。2つのオブジェクトが存在し、それらの型に基づいてディスパッチが行われるという知識はすべてラムダ関数の中にあります。
Pythonは、古典的な意味でのメソッドオーバーロード(渡されるパラメータの型に応じて多態的に動作する)をサポートしていないため、異なるモデルタイプの「visit」メソッドには異なる名前を付ける必要があります。
"""訪問者パターンの例。"""from abc import ABCMeta , abstractmethod from typing import NoReturnNOT_IMPLEMENTED : str = "これを実装する必要があります。"class CarElement ( metaclass = ABCMeta ): @abstractmethod def accept ( self , visitor : CarElementVisitor ) -> NoReturn : raise NotImplementedError ( NOT_IMPLEMENTED )class Body ( CarElement ): def accept ( self , visitor : CarElementVisitor ) -> None : visitor . visit_body ( self )class Engine ( CarElement ): def accept ( self , visitor : CarElementVisitor ) -> None : visitor . visit_engine ( self )class Wheel ( CarElement ): def __init__ ( self , name : str ) -> None : self . name = namedef accept ( self , visitor : CarElementVisitor ) - > None : visitor.visit_wheel ( self )class Car ( CarElement ): def __init__ ( self ) -> None : self . elements : list [ CarElement ] = [ Wheel ( "front left " ), Wheel ( "front right " ), Wheel ( "back left " ), Wheel ( "back right " ), Body (), Engine () ]def accept ( self , visitor ) : for element in self.elements : element.accept ( visitor ) visitor.visit_car ( self )class CarElementVisitor ( metaclass = ABCMeta ): @abstractmethod def visit_body ( self , element : CarElement ) -> NoReturn : raise NotImplementedError ( NOT_IMPLEMENTED )@abstractmethod def visit_engine ( self , element : CarElement ) -> NoReturn : raise NotImplementedError ( NOT_IMPLEMENTED )@abstractmethoddefvisit_wheel(self,element:CarElement)->NoReturn:raiseNotImplementedError(NOT_IMPLEMENTED)@abstractmethoddefvisit_car(self,element:CarElement)->NoReturn:raiseNotImplementedError(NOT_IMPLEMENTED)classCarElementDoVisitor(CarElementVisitor):defvisit_body(self,body:Body)->None:print("Moving my body.")defvisit_car(self,car:Car)->None:print("Starting my car.")defvisit_wheel(self,wheel:Wheel)->None:print(f"Kicking my {wheel.name} wheel.")defvisit_engine(self,engine:Engine)->None:print("Starting my engine.")classCarElementPrintVisitor(CarElementVisitor):defvisit_body(self,body:Body)->None:print("Visiting body.")defvisit_car(self,car:Car)->None:print("Visiting car.")defvisit_wheel(self,wheel:Wheel)->None:print(f"Visiting {wheel.name} wheel.")def visit_engine ( self , engine : Engine ) -> None : print ( "エンジンを訪問しています。" )if __name__ == "__main__" : car : Car = Car () car . accept ( CarElementPrintVisitor ()) car . accept ( CarElementDoVisitor ())左前輪を訪ねる。右前輪を訪ねる。左後輪を訪ねる。右後輪を訪ねる。体を訪ねる。エンジンを訪ねる。車を訪ねる。左前輪を蹴る。右前輪を蹴る。左後輪を蹴る。右後輪を蹴る。体を動かす。エンジンをかける。車を始動する。Python 3以降を使用すると、acceptメソッドの汎用的な実装が可能になります。
class Visitable : def accept ( self , visitor : Visitor ) -> Any : lookup : str = f "visit_ { self . __qualname__ . replace ( "." , "_" ) } " return getattr ( visitor , lookup )( self )既に実装済みのクラスにフォールバックしたい場合は、この方法を拡張してクラスのメソッド解決順序を反復処理することもできます。また、サブクラスフック機能を使用して、ルックアップを事前に定義することも可能です。
{{cite web}}: CS1 maint: url-status (リンク)