自動プログラミングの設計パターン



アルゴリズムとオートマトンの理論から、 有限状態機械の概念知られています。これは、有限モデルの状態およびこれらの状態間の遷移を開始するイベントに関して抽象モデルの動作を説明します。 この理論に基づいて、かなり広範な自動プログラミングのパラダイムが開発され、それを使用して、有限オートマトンの観点から問題が解決されます。 状態とイベントに関して。 明示的な形式では、このパラダイムはレクサーを作成するときにプログラミング言語で広く使用されます。 しかし、ある意味で、このパラダイムに基づいて実装された膨大な数のソフトウェアシステムの例を見つけることができます。



この記事では、ステートマシンに基づいてシステムを実装するときに、 Visitor / Double DispatchおよびStateテンプレートを使用する機能について説明します。 さらに、この記事は、Habrahabrのデザインパターンに関する一連の出版物の続きと考えることができます。





やる気



自動プログラミングは、主題領域の概念に近い用語で問題を解決できる、非常に便利で柔軟なツールです。 たとえば、高層ビルのエレベーターの動作をプログラミングするタスクは、「エレベーターが上がる」、「エレベーターが下がる」、「エレベーターがN階に立つ」、家の居住者からのイベントが「ダウンボタンが押される」状態の非常に形式化されたマシンのモデルに変わりますN階で」および「N階で上ボタンが押された」。



ただし、このアプローチの明らかな利点に加えて、1つの小さな欠点があります。そのようなシステムのコーディングは、非常に不快なプロセスであり、多数のブランチとトランジションの使用を伴います。



この問題を解決するために、OOPと訪問者と状態のテンプレートがあります。





次の問題を考慮してください。 「ラジオ」と「CDプレーヤー」の2つの動作モードをサポートするプリミティブなカーラジオを設計および実装する必要があります。 モード間の切り替えは、コントロールパネルのトグルスイッチを使用して実行されます(記事の冒頭の図を参照)。 さらに、ラジオは、設定モードに応じて、ラジオ局またはトラックの切り替えメカニズム(「次へ」ボタン)をサポートします。



ネストされたCブランチに基づいてこの問題を解決する方法の優れた例は、 Wikipediaにあります。 その最も重要な領域を検討してください。



switch( state ) { case RADIO: switch(event) { case mode: /* Change the state */ state = CD; break; case next: /* Increase the station number */ stationNumber++; break; } break; case CD: switch(event) { case mode: /* Change the state */ state = RADIO; break; case next: /* Go to the next track */ trackNumber++; break; } break; }
      
      







この例のコードは絶対に読みにくく、拡張が困難です。 たとえば、新しい状態またはイベントの追加は非常に時間のかかる操作であるように見えるため、大量のコードを変更する必要があります。 さらに、そのようなスパゲッティコードは、特定のセクションを複製するように開発者を誘導します(異なる状態で同じイベントが同じ方法で処理される必要がある状況が可能です)。



Visitorテンプレートとその特殊なケースであるDouble Dispatchは 、状態とハンドラーの概念を分離することでこの問題を解決するように設計されています。 同時に、イベント処理アルゴリズムの最終的な実装は、イベントの種類とハンドラーの種類の2つの要因に基づいて、実行中に選択されます(そのため「Double Dispatch」という名前です)。



したがって、新しいタイプのイベントまたはハンドラーをシステムに追加するには、必要なクラス、「イベント」または「ハンドラー」クラスの相続人をそれぞれ実装するだけで十分です。



クラス図



システムの主要なエンティティ:







この図で最も重要なのはGramophoneHandlerインターフェイスです。これは、Visitor構造の一部(ビジター自体)と自己完結型のState構造(Gramophoneの状態)の両方です。 つまり 考慮された例では、2つのパターンの一種の合成が使用されていると想定できます。



実装



システムを使用するためのオプションの1つは次のようになります。



 public static void main(String args[]) { Gramophone gramophone = new Gramophone(); // it's CD by default gramophone.enable(); // start event loop gramophone.dispatch(new ToogleEvent()); // toogle to radio gramophone.dispatch(new NextEvent()); // next station gramophone.dispatch(new ToogleEvent()); // toogle to CD player gramophone.dispatch(new NextEvent()); // next CD track gramophone.disable(); // stop event loop }
      
      







システムに送られる外部イベントの可能なオプションについて説明します。



 /** * Events */ interface GramophoneEvent { void apply(GramophoneHandler handler); } class ToogleEvent implements GramophoneEvent { @Override public void apply(GramophoneHandler handler) { handler.handle(this); } } class NextEvent implements GramophoneEvent { @Override public void apply(GramophoneHandler handler) { handler.handle(this); } }
      
      







上記のコードでは、apply()メソッドの実装はすべての子孫で同じです。 これがテンプレートの主な考え方です。コード実行のプロセスにおけるイベントのタイプの多態的な定義です。 つまり イベント自体のタイプ(thisリンクなど)に応じて、handle()メソッドがハンドラーで呼び出されます。



ポリモーフィズムをサポートしていない言語(JavaScriptなど)では、メソッド名で処理されているイベントのタイプに関する情報をカプセル化できます。 この場合、メソッドはhandleNext(イベント)とhandleToogle(イベント)のようになり、呼び出しコードは次のようになります。



 var NextEvent = function() { this.apply = function(handler) { handler.handleNext(this); } }
      
      







システムの可能な状態の実装は次のコードです。 この例では、state = handlerです。



 /** * Visitor */ interface GramophoneHandler { void handle(ToogleEvent event); void handle(NextEvent event); } class RadioHandler implements GramophoneHandler { private Gramophone gramophone; public RadioHandler(Gramophone gramophone) { this.gramophone = gramophone; } @Override public void handle(ToogleEvent event) { gramophone.toogle(new CDHandler(gramophone)); } @Override public void handle(NextEvent event) { gramophone.nextStation(); } } class CDHandler implements GramophoneHandler { private Gramophone gramophone; public CDHandler(Gramophone gramophone) { this.gramophone = gramophone; } @Override public void handle(ToogleEvent event) { gramophone.toogle(new RadioHandler(gramophone)); } @Override public void handle(NextEvent event) { gramophone.nextTrack(); } }
      
      







最後に、システムのメインクラスであるラジオ(Gramophone)の実装を検討します。



 /** * FSM (Finit-State-Machine) implementation */ class Gramophone implements Runnable { private GramophoneHandler handler = new CDHandler(this); private Queue<GramophoneEvent> pool = new LinkedList<GramophoneEvent>(); private Thread self = new Thread(this); private int track = 0, station = 0; private boolean started = false; public void enable() { started = true; self.start(); } public void disable() { started = false; self.interrupt(); try { self.join(); } catch (InterruptedException ignored) { } } public synchronized void dispatch(GramophoneEvent event) { pool.offer(event); notify(); } @Override public void run() { for (;!pool.isEmpty() || started;) { for (;!pool.isEmpty();) { GramophoneEvent event = pool.poll(); event.apply(handler); } synchronized (this) { try { wait(); } catch (InterruptedException ignored) { } } } } void toogle(GramophoneHandler handler) { this.handler = handler; System.out.println("State changed: " + handler.getClass().getSimpleName()); } void nextTrack() { track++; System.out.println("Track changed: " + track); } void nextStation() { station++; System.out.println("Station changed: " + station); } }
      
      







上記の実装では、toogle()、nextTrack()、およびnextStation()メソッドの有効範囲はパッケージ内のみです。 これは、システムを直接の外部呼び出しから保護するために行われます。 さらに、実際の条件では、原因となるストリームの性質をさらに確認することをお勧めします。 たとえば、各メソッドに次の検証コードを追加できます。



 void nextTrack() { if (Thread.currentThread() != self) { throw new RuntimeException(“Illegal thread”); } track++; System.out.println("Track changed: " + track); }
      
      







さらに、 イベントループのイディオムを実装するrun()メソッドにも注意する必要があります。 この場合、メソッドには2つのネストされたループが含まれ、キューにイベントがない場合にストリームがスリープ状態になることを保証します。 同時に、各イベントをキューに追加すると(dispatch()メソッドを参照)、それが起動します。



おわりに



この記事は、OOPとデザインパターンの使用に関する宣伝ではありませんが、これらのツールを使用して特定の問題を解決する機能のみを明らかにしています。



この例のコードはGitHubで入手できます 。 そこでは、他のパターンの例を見つけることができます。これについては、すでにいくつかの記事Habrahabrで書かています。



All Articles