「iOS」および「Swift」ユニバースのアーキテクチャパターン「Visitor」

「Visitor」は、教科書「Gang of Four」、「GoF」、「Design Patterns:Elements of Reusable Object-Oriented Software」に記載されている行動パターンの1つです。 」)

要するに、テンプレートは、互いに接続されていない異なるタイプのオブジェクトのグループに対して同じタイプのアクションを実行できるようにする必要がある場合に役立ちます。 または、言い換えれば、同じタイプの特定の操作または単一のソースを使用して、この一連のタイプの機能を拡張することです。 同時に、拡張可能な型の構造と実装は影響を受けません。
アイデアを説明する最も簡単な方法は、例を使用することです。



例が架空のものであり、アカデミックな目的のために構成されていることをすぐに予約したいと思います。 つまり この資料は、OOPの受け入れを紹介することを目的としており、高度に専門化された問題については説明しません。



また、例のコードが設計手法を研究するために書かれているという事実にも注意を喚起したいと思います。 私はその(コード)の欠点と、実際のプロジェクトで使用するために改善する可能性を認識しています。





UITableViewController



いくつかのサブタイプを使用するUITableViewCell



サブタイプがあるとしUITableViewCell







 class FirstCell: UITableViewCell { /**/ } class SecondCell: UITableViewCell { /**/ } class ThirdCell: UITableViewCell { /**/ } class TableVC: UITableViewController { override func viewDidLoad() { super.viewDidLoad() tableView.register(FirstCell.self, forCellReuseIdentifier: "FirstCell") tableView.register(SecondCell.self, forCellReuseIdentifier: "SecondCell") tableView.register(ThirdCell.self, forCellReuseIdentifier: "ThirdCell") } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { /**/ return FirstCell() /**/ return SecondCell() /**/ return ThirdCell() } }
      
      





異なるサブタイプのセルの高さが異なっている必要があるとします。



もちろん、高さの計算は各セルタイプの実装に直接配置できます。 しかし、セルの高さがセルのタイプだけでなく、外部条件にも依存する場合はどうでしょうか? たとえば、セルタイプは、高さの異なるさまざまなテーブルで使用できます。 この場合、 UITableViewCell



サブクラスが「スーパービュー」または「ビューコントローラー」のニーズを認識しないようにする必要があります。



次に、 UITableViewController



メソッドで高さの計算を行うことができますUITableViewCell



を高さの値で初期化するかUITableViewCell



インスタンスを特定のサブタイプにUITableViewCell



し、 tableView(_:heightForRowAt:)



異なる値を返します。 しかし、このアプローチは柔軟性がなくなり、「if」演算子の長いシーケンスまたはかさばる「switch」構造に変わる可能性があります。



「Visitor」テンプレートを使用して問題を解決する



もちろん、「Visitor」テンプレートはこの問題を解決できるだけでなく、非常にエレガントにそれを行うことができます。



これを行うには、まず、実際にセルタイプの「訪問者」となるタイプと、テーブルセルの高さを計算するだけの責任を持つオブジェクトを作成します。



 struct HeightResultVisitor { func visit(_ ell: FirstCell) -> CGFloat { return 10.0 } func visit(_ ell: SecondCell) -> CGFloat { return 20.0 } func visit(_ ell: ThirdCell) -> CGFloat { return 30.0 } }
      
      





タイプは、使用される各サブタイプを認識し、各サブタイプに必要な値を返します。



次に、 UITableViewCell



各サブタイプは、この「訪問者」を「受信」できる必要があります。 これを行うには、このような「受信」メソッドを使用してプロトコルを宣言します。これは、使用されるすべてのセルタイプによって実装されます。



 protocol HeightResultVisitable { func accept(_ visitor: HeightResultVisitor) -> CGFloat } extension FirstCell: HeightResultVisitable { func accept(_ visitor: HeightResultVisitor) -> CGFloat { return visitor.visit(self) } } extension SecondCell: HeightResultVisitable { func accept(_ visitor: HeightResultVisitor) -> CGFloat { return visitor.visit(self) } } extension ThirdCell: HeightResultVisitable { func accept(_ visitor: HeightResultVisitor) -> CGFloat { return visitor.visit(self) } }
      
      





UITableViewController



のサブクラス内では、次のように機能を使用できます。



 override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { let cell = tableView.cellForRow(at: indexPath) as! HeightResultVisitable return cell.accept(HeightResultVisitor()) }
      
      





良くなるかも!



ほとんどの場合、特定の機能に厳密に関連付けられたそのようなコードは必要ありません。 おそらく、セルのセットに新しい機能を追加できるようにしたいのですが、セルの高さだけでなく、たとえば、背景色、セル内のテキストなどについても、戻り値の型に結び付けられないようにします。 relatedtypeのプロトコル( 「Protocol with Associated Type」、「PAT」 )は、ここで役立ちます。



 protocol CellVisitor { associatedtype T func visit(_ cell: FirstCell) -> T func visit(_ cell: SecondCell) -> T func visit(_ cell: ThirdCell) -> T }
      
      





セルの高さを返すための実装:



 struct HeightResultCellVisitor: CellVisitor { func visit(_ cell: FirstCell) -> CGFloat { return 10.0 } func visit(_ cell: SecondCell) -> CGFloat { return 20.0 } func visit(_ cell: ThirdCell) -> CGFloat { return 30.0 } }
      
      





「ホスト」側では、このタイプの「訪問者」のために、共通のプロトコルとその唯一の実装だけで十分です。 「訪問者」パーティのみが、さまざまなタイプの戻り値を認識します。



タイプの「受信ビジター」(本「GoF」ではこの側は「要素」と呼ばれます)のプロトコルは次の形式を取ります。



 protocol Visitableell where Self: UITableViewCell { func accept<V: CellVisitor>(_ visitor: V) -> VT }
      
      





(実装タイプに制限はないかもしれません。しかし、この例では、 UITableViewCell



サブクラスによってこのプロトコルを実装することは意味がありません。)



UITableViewCell



サブタイプでの実装:



 extension FirstCell: Visitableell { func accept<V: CellVisitor>(_ visitor: V) -> VT { return visitor.visit(self) } } extension SecondCell: Visitableell { func accept<V: CellVisitor>(_ visitor: V) -> VT { return visitor.visit(self) } } extension ThirdCell: Visitableell { func accept<V: CellVisitor>(_ visitor: V) -> VT { return visitor.visit(self) } }
      
      





最後に、次を使用します。



 override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { let cell = tableView.cellForRow(at: indexPath) as! Visitableell return cell.accept(HeightResultCellVisitor()) }
      
      



したがって、「訪問者」のさまざまな実装を使用して、一般的にほとんど何でも作成でき、新しい機能をサポートするために「受信側」からは何も必要ありません。 このパーティーは、「ゲスト」が正確に何を授けたのかさえ知らないでしょう。

別の例



同様の「訪問者」を使用して、セルの背景色を変更してみましょう。



 struct ColorResultCellVisitor: CellVisitor { func visit(_ cell: FirstCell) -> UIColor { return .black } func visit(_ cell: SecondCell) -> UIColor { return .white } func visit(_ cell: ThirdCell) -> UIColor { return .red } }
      
      





このビジターの使用例:



 override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { cell.contentView.backgroundColor = (cell as! Visitableell).accept(ColorResultCellVisitor()) }
      
      





このコードの一部は紛らわしいはずです...最初は、「訪問者」がクラスに機能を追加できると言われていました。 では、単に値を取得するだけでなく、セルの背景色を変更するすべての機能を「隠す」ことは可能ですか? できます。 次に、 associatedtype



Void



は値Void



(別名()



-空のタプル)
を取ります。



 struct BackgroundColorSetter: CellVisitor{ func visit(_ cell: FirstCell) { cell.contentView.backgroundColor = .black } func visit(_ cell: SecondCell) { cell.contentView.backgroundColor = .white } func visit(_ cell: ThirdCell) { cell.contentView.backgroundColor = .red } }
      
      





使用法:



 override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { (cell as! Visitableell).accept(BackgroundColorSetter()) }
      
      







結論の代わりに





このパターンは、一見したところほとんど好きかもしれませんが、慎重に使用する必要があります。 コードでのその外観は、多くの場合、アーキテクチャのより一般的な欠陥の兆候です。 おそらく、接続すべきでないものを接続しようとしているのでしょう。 おそらく、追加された機能は、何らかの方法で抽象化の1レベル上に取り出される必要があります。



いずれにせよ、ほとんどすべてのパターンには長所と短所があり、それを使用する前に、常に意識的に判断し、決定する必要があります。 パターンは、一方では、コードの読み取りと議論を容易にするためにプログラミング手法を一般化する方法です。 そしてもう一方-問題を解決する方法(人為的に導入されることもあります)。 そして、もちろん、どのような場合でも、その使用の事実だけのために、既知のすべてのパターンに狂信的にコードを持ち込まないでください。




終わったと思います! すべての美しいコードと少ない「バグ」!



デザインパターンに関する他の記事:






All Articles