📜 ⬆️ ⬇️

How we designed and made True Image for Mac

Hello. One day we learned that we have to do a True Image for Mac OS. As is usually the case, you need to do it quickly and efficiently, yeah. At once a reasonable question arose, why not just compile a true image for Windows under Mac, because most of the code is already cross-platform, including the interface written in Qt? But we were immediately marked frame:

The interface was decided to make a completely new one, many times simpler than that of a big brother. Also, as a GUI framework, experienced in Poppy cases, the guys from Parallels advised to use native ocoa instead of Qt, and people from another well-known company confirmed the correctness of this decision. We decided not to question their experience.

As a result, it was decided to try to write frontend on Cocoa to the existing code. We did release the product and already wrote about it on Habré , and today I want to share the architectural and technical details of this process.



Passive view


The basis for the new architecture was to put the Passive View pattern, the original description of which can be read by Fowler .
The pattern itself is ugly simple. Like the classic MVC / MVP triad, there is a view, a model and a presenter (in another terminology, the controller). The difference with other similar patterns is that the view, as the name implies, is “passive” or, simply, “stupid” - it knows nothing about the model, and the presenter is responsible for the coordination of the model and the view.
')


Why this approach?
  1. Testability is the biggest plus of this pattern. The view and model are isolated, they know nothing about the outside world, except for the reviewers who subscribe to their changes. The presenter, in turn, receives almost all of his knowledge from the outside through a dependency injection. You can write tests for implementations of the form, you can write tests for the implementation of the model, you can write tests for the correct behavior of logic in the presenter;
  2. Understandability - all the logic of a particular piece is concentrated in one place - in the presenter, and not spread over the types;
  3. Reusability and composability - the presenter works with the view and the model through the interfaces, so you can use the same logic, designed into the presenter, in different places of the program.

The components interact as follows: the presenter adjusts the view, subscribes to the events of the view and model, shows the view and processes the events of the model and view:



This pattern by itself does not claim to be the most-most, certain things are still at times more convenient to do, using, say, the MVC-approach, when the data itself pulls the data. For example, this was the way the file browser was made in the restore dialog. Passive View is good where there is no large data flow in the view.


Code!


Types and presenters we organized in a hierarchy. The presenter of the main window generates other presenters in the event handlers, and they do their part of the work. Conceptually, it all looks like this:

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(); } 

Since the deadlines were quite tough, all these injections were very useful to us from the very beginning and allowed us to parallelize the work: we changed the models for stubs and did a full test of the interface behavior while the full-fledged models were being implemented. The caps themselves can be reused for unit tests.

At one point, the abstractness of the models can be said to have saved the project’s end dates when the (correct) decision was made not to reinvent the wheel for several subsystems, but to use all the low- and middle-level logic from True Image for Windows. As a result, models were implemented with varying degrees of thickness by the facades or adapters to the existing logic layer, and both True Image versions received all bonuses relying on it, including in the form of fixing ancient bugs that only came out on the Mac (for example, incorrect or insufficient synchronization is better manifested it is on GCC than on MSVC).


We screw Cocoa


It is worth mentioning how we screwed the native Cocoa into this structure, maybe someone will come in handy. We used Objective-C ++ and ARC, we drew windows in Interface Builder. The process is as follows:

  1. We make a xib window and its obj-c ++ controller, in most cases we use bindings to control the window state

     @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 

  2. Making an obj-c ++ adapter that can already be injected into the presenter

     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]; } } 


Bonus Command Line Interface


Abstraction and passivity of the form made it possible to make an alternative CLI-interface, which is actively used for our automated tests for Mac. It is very easy to maintain it, because for each species it is enough to implement only one class without any business logic!

 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" } 


Multithreading


From the very beginning we made one essential assumption - to consider all kinds of thread-safe. This made it possible to significantly simplify the code of presenters. The point is that almost all GUI frameworks have the ability to perform an operation asynchronously in the main thread, and used this:


Prosperity




disadvantages




General impressions


Free CLI is cool! If you have run auto tests. But if it is adjusted, then it is really cool.

Several times the chosen approach saved from rewriting a lot of code, for example, when designers decided to thoroughly redraw most of the interface. For the most part, changes in the code were limited to only the implementation of the class of the form, and almost all business logic remained intact. With all this, according to my feelings, Passive View is more suitable for small or medium-sized applications - for large applications it seems to me that the advantages of flexibility will not outweigh the lack of expensiveness / complexity of expanding the user interface itself.

And what approaches do you use in your projects?

Source: https://habr.com/ru/post/232543/


All Articles