インターフェイスを完全に新しいものにすることが決定され、兄のそれよりも何倍も簡単になりました。 また、Macでの経験が重要であるため、ParallelsのスタッフはQtの代わりにネイティブのCocoaをGUIフレームワークとして使用することをアドバイスし、別の有名な会社の人々がこのソリューションの正確さを確認しました。 彼らは自分の経験に疑問を持たないことに決めました。
最終的に、Cocoaのフロントエンドを既存のコードに書き込もうとすることになりました。 製品をリリースし、すでにHabréでそれについて書いています。今日、このプロセスのアーキテクチャと技術の詳細を共有したいと思います。
パッシブビュー
彼らはパッシブビューパターンを新しいアーキテクチャの中心に置くことにしました。その初期の説明はFowlerから読むことができます。
パターン自体はいシンプルです。 MVC / MVPの古典的なトライアドのように、ビュー、モデル、およびプレゼンター(他の用語ではコントローラー)があります。 他の同様のパターンとの違いは、名前が示すように、ビューは「受動的」または単純な方法では「ダム」であるということです。モデルについては何も知らず、プレゼンターはモデルとタイプのすべての調整を担当します。
なぜまさにこのアプローチなのか?
- テスト可能性は、このパターンの最大のプラスです。 ビューとモデルは分離されており、変更に署名したオブザーバーに関するものを除き、外部の世界については何も知りません。 プレゼンターは、依存症の注入を通じて、外部からほぼすべての知識を受け取ります。 フォームの実装用のテストを記述したり、モデルの実装用のテストを記述したり、プレゼンターのロジックの正しい動作に関するテストを記述したりできます。
- 明快さ-特定の作品のすべてのロジックが1か所-プレゼンターに集中しており、タイプごとに分散されていません。
- 再利用可能性と構成可能性-プレゼンターはインターフェースを介してビューとモデルを操作するため、プレゼンターで設計された同じロジックを使用して、プログラムのさまざまな場所で使用できます。
コンポーネントは次のように相互作用します。プレゼンターはビューを調整し、ビューとモデルのイベントをサブスクライブし、ビューを表示し、モデルとビューのイベントを処理します。
このパターン自体は、最も自己のふりをするものではありません。データがビュー自体によって描画される場合、MVCアプローチなどを使用すると、特定のことを行う方が何倍も便利です。 たとえば、この方法では、回復ダイアログでファイルブラウザが作成されました。 パッシブビューは、ビューへのデータフローがあまりない場合に適しています。
コード!
階層で編成したタイプとプレゼンター。 メインウィンドウのプレゼンターは、イベントハンドラーで他のプレゼンターを生成し、その役割を果たします。 概念的には、すべて次のようになります。
struct ModelObserver { // various callbacks virtual void OnModelChanged() = 0; } struct Model : Observable<ModelObserver> { // virtual getters, setters, etc } struct ViewObserver { // various callbacks virtual void OnViewButtonClicked() = 0; } struct View : Observable<ViewObserver> { // virtual setters, etc virtual void Show() = 0; // event loop, QDialog::exec() } struct PresenterParent : ModelObserver, ViewObserver { Model M; // injected in ctor View V; // injected in ctor void Run() { M.AttachObserver(this); V.AttachObserver(this); V.Show(); } void OnModelChanged() { // V.SetSomething(M.GetSomething()); } void OnViewButtonClicked() { // // V // ViewFactory PresenterChild p(M, V.CreateChildView()); p.Run(); } } void main(argc, argv) { Model m(CreateModel()); // - , View v(CreateParentView()); // - , Qt Cocoa PresenterParent p(m, v); p.Run(); }
締め切りがかなり厳しかったため、これらの注入はすべて最初から非常に役立ち、作業を並列化できました。モデルをスタブに置き換え、本格的なモデルが並列に実装されている間にインターフェイスの動作をテストするために最善を尽くしました。 スタブ自体は、単体テストに再利用できます。
ある時点で、モデルの抽象性は、いくつかのサブシステムのホイールを再発明するのではなく、True Image for Windowsのすべての低レベルおよび中レベルのロジックを使用するという(右の)決定が下されたときに、プロジェクトの期限を守ると言えます。 その結果、モデルはファサードまたは既存のロジックレイヤーへのアダプターによってさまざまな厚さで実装され、True Imageの両方のバージョンは、Macのみで発生した古代のバグの修正を含む、これに依存するすべてのボーナスを受け取りましたMSVCよりもGCCにあります)。
ココアを留める
ネイティブのココアをこの構造にどのようにねじ込んだかを言及する価値があります。誰かが役に立つかもしれません。 Objective-C ++およびARCを使用し、Interface Builderでペイントされたウィンドウ。 プロセスは次のとおりです。
- xibウィンドウとそのobj-c ++コントローラーを作成します。ほとんどの場合、バインダーを使用してウィンドウの状態を制御します
@interface ViewCocoa : NSWindowController { Observable<ViewObservable>* Callbacks; } @property NSNumber* Something; - (id)initWithObservable:(Observable<ViewObservable>*)callbacks; - (IBAction)OnButtonClicked:(id)sender; @end @implementation ViewCocoa { - (id)initWithObservable:(Observable<ViewObservable>*)callbacks { if (self = [super initWithWindowNibName:@"ViewCocoa"]) { Callbacks = callbacks; } return self; } } - (IBAction)OnButtonClicked:(id)sender { // Callbacks->NotifyObservers(bind(&ViewObserver::OnViewButtonClicked, _1)); } @end
- obj-c ++アダプターを作成します。これは、プレゼンターに既に挿入できます。
struct ViewCocoaAdapter : View { ViewCocoa* Adaptee = [[ViewCocoa alloc] initWithObservable:this]; virtufal void Show() { // [NSApp runModalForWindow:Adaptee.window]; } // Adaptee virtual void SetContent(int something) { // , performSelectorOnMainThread, [Adaptee performSelectorOnMainThread:@selector(setSomething:) withObject:[NSNumber numberWithInt:something] waitUntilDone:NO]; } }
ボーナスコマンドラインインターフェース
種の抽象性と受動性により、代替CLIインターフェースを作成することが可能になりました。これは、Mac用の自動テストに積極的に使用されています。 タイプごとにビジネスロジックなしで1つのクラスのみを実装すれば十分なので、メンテナンスは非常に簡単です。
struct ViewCli : View { virtual void Show() { for (;;) { // , - std::string cmd; std::cin >> cmd; if (cmd == "ls") { std::cout << " , ..." << std::endl; } else if (cmd == "x") { break; } else if (cmd == "click") { NotifyObservers(bind(&ViewObserver::OnViewButtonClicked, _1)); } } } // , , "ls" }
マルチスレッド
当初から、1つの重要な仮定を立てました。すべての型をスレッドセーフと見なすことです。 これにより、プレゼンターのコードが大幅に簡素化されました。 トリックは、ほとんどすべてのGUIフレームワークがメインGUIスレッドで非同期操作を実行する能力を持ち、これを利用したことです。
- qtの場合、QMetaObject :: invokeMethod with Qt :: QueuedConnection、またはQCoreApplication :: postEvent with operation event
- cocoaにはdispatch_async + dispatch_get_main_queue、またはperformSelectorOnMainThreadがあります
- cliにはmutexがあります
長所
- 繰り返しますが、テスト容易性:単体テスト、自動テスト...はい、あらゆる種類のテストです!
- 明確に定義された場所へのロジックの集中:実際には、必要なコードを見つけて補完するのは非常に簡単です。
- ロジックの再利用性:同じプレゼンターをカスタマイズ可能にできるため、結果として型の動作が異なり、同時に「ダム」のままであり、コードの重複はほとんどありません。
- 主なアプローチは基本的に同じであるため、イベントループウィンドウ、モーダルウィンドウ、非モーダルウィンドウなど、さまざまなGUIフレームワークの下
で何回もロジックを記述できます。
短所
- コールバック地獄-1つのクラスのメソッドの束、または小さなインターフェイスとプレゼンターの束のいずれかですが、いずれにしても、時間の経過とともに同じ結果になります。
- Cocoaでパターンを実装する複雑さ。 これは、コードを初めて見た人に特に感じられました。 実際、新しいウィンドウを作成するには、ビューのC ++クラス、ビューオブザーバのC ++クラス、xib、Objective-C ++インターフェイスと実装、Objective-C ++アダプタ-エンティティの束を作成する必要があります! 通常のQt、Forms and Controlsパターンなどと比較すると、uiとロジックを備えた本格的なウィンドウクラスだけで十分です。 ここでは、何のために何を犠牲にするかを理解するだけの価値があります。 テスト容易性と無料のCLIは私たちにとって大きな利点であるため、このような複雑さに耐えなければなりませんでした。 ただし、原則として、時間の経過とともに、新しいウィンドウの数は増えなくなり、バグを修正して既存のコードを補完する方法が提供されます。
一般的な印象
Freestuff CLIはクールです! 自動テストの実行を開始した場合。 しかし、調整すれば、本当にクールです。
設計者がインターフェイスの大部分を徹底的に再描画することを決めたときなど、選択されたアプローチにより、何度もコードを書き直す必要がなくなりました。 コードの変更は、ほとんどの場合ビュークラスの実装のみに限定され、ほとんどすべてのビジネスロジックは変更されませんでした。 同時に、私の意見によると、パッシブビューは中小規模のアプリケーションに適しています-大規模なアプリケーションでは、柔軟性の利点は、ユーザーインターフェイス自体を拡張するための高コスト/複雑さの不足を上回らないように思われます。
プロジェクトでどのようなアプローチを使用していますか?