⬆️ ⬇️

In search of perfect architecture

image

For 9 years of working with features in robot projects, it became more and more, it became easier to get confused in the code.



When there were more than a dozen developers, another problem appeared - the painful rotation of people between projects. Outsourcing development is famous for tough deadlines, and developers do not have months or weeks to dive in, in particular, a new project, while at the same time working on different projects is necessary for the development of specialists.



The main problem that arises during the long-term development of the application is scalability. It can be solved by switching to a new architecture or refactoring the code base and adding new entities that unload objects with a large number of duties.



Disclaimer



In our understanding, there is no such thing as a “universal architecture”. Everyone makes a choice in favor of the one with which it is more effective and convenient to work throughout the entire project. What is effectively used in our country may become a useless overhead for other teams.

')

Start



It all started with the popular MVC and his buddy network manager.



The main problems of MVC: calling network requests and database requests, implementing business logic and navigation are located in the controller. Because of this, objects are highly interconnected, and the level of reuse and testing is low.



Network manager in serious projects turns into a “divine object” , which becomes impossible to maintain due to its size.



Imagine that we have an application for a chain of stores, in which there are stock screens, profiles and settings. The first screen displays a list of valid promotions, in the profile - the current bonus balance, the name and phone number of the user, in the settings, for example, the ability to enable push notifications about new promotions. To enter the application, you must log in using your login and password.



It turns out that in this case, all requests to the server - authorization, getting the list of shares, getting information on the profile, changing the settings - are in the network manager, including the logic for creating model objects from JSON.



Farewell to NetworkManager



At the layer of interaction with the network, it was decided to adhere to the SOA approach - dividing the service layer into multiple services depending on the type of entity.



image



In our example, the ConcreteService will be AuthService, UserService, DealService, SettingsService . Each service does its own thing - the authorization service works with authorization, the user service works with user data, and so on. A good rule is to split the server side into different path: /auth, /user, /deals, /settings , but not necessarily.



A more detailed description of the service layer is in our previous article .



JSON / XML serialization / deserialization, etc.



We select the serialization / deserialization of objects into separate entities: the parser and the serializer. The operations are reciprocal: the parser converts an object of the data type that it receives from the server into a model object, the serializer converts it from a model object into a data object for transmission over the network. Inside these classes, field binding and error logging are implemented.



Examples of interfaces for working with the “user” entity
 class UserParser: JSONParser<User> { func parseObject(_ data: JSON) -> User? } class UserSerializer: JSONSerializer<User> { func serializeObject(_ object: User) -> Data? } 




image



For each entity we have separate parsers AuthParser, UserParser, DealParser and SettingsParser . With serializers - exactly the same.



Splitting



In our architecture, we adhere to the division into layers, the upper layer knows only about the existence of the lower.



image



Above in order: user interface layer, business logic layer, service layer and data layer.



Data layer



We most often implement this layer through the DAO pattern, abstracting from the implementation and database features on all other layers. We have ready-made solutions for Realm and CoreData, most often we use Realm. An example implementation here .



image



Imagine that in the application we want to cache discounts. Therefore, when using DAO, we will have the following classes:





In the section "Code Generation" I will give examples of the implementation of these classes.



How to deal with the UI-layer?



Here we had several iterations, during which we analyzed the existing solutions: MVVM and VIPER, but without practical application it was difficult to evaluate them objectively. VIPER for our projects seemed redundant: a large number of entities for a single module (in many cases they are only intermediaries in the call chain), a complicated implementation of routing using storyboards, and distancing from UIKit. Of course, testing modules can be attributed to the pluses.



Using MVVM, in our opinion, it was easier to understand with knowledge of MVC, binding solved the problem of explicit calls for updating data, it became possible to write test code. Problems with the use of reactive programming was not - we used it in conjunction with MVC.



Moving to MVVM



This architecture is disassembled in detail, and it is hardly worth trying to do it n + 1 times. What is the advantage over MVC we saw here? In most cases, the information displayed to the user is a conversion of models from the server. Therefore, the logic for converting this information is encapsulated inside the view model, or, if there is a dependency between objects, partly in the view model factory. An example of how a user's phone number is converted for further display on the screen:



image



Go to the presentation model and the router.



After some time, we realized that we could not do with MVVM alone. The view controller class gradually “swelled”, this was especially noticeable if several requests were called up on the screen. The next step identified the appeal to services in a separate entity - the presentation model and the view controller ceased to know about their existence.



Using navigation (with or without segue) on multiple screens also led to the growth of the view controller. I note that a call in itself to show the screen will take you 2-3 lines of code, whereas the configuration and transfer of the necessary data to another screen may take, say, 10 lines. Therefore, the router was allocated to a separate entity (yes, a little more and VIPER). Using the router turned out to be convenient, including when we abandoned stroyboards in favor of xib. Reading the router class is definitely harder than visually grasping a map of transition screens. But even less convenient if your navigation code is scattered everywhere.



image



Router in this scheme is a separate property on the view controller that is created in the viewDidLoad method. On some projects we created it directly at the time of the navigation.



Here it is important to understand that we do not require compliance with the division on the presentation model and the router for relatively simple screens, say, where everything fits in 200 lines.



For example, the router in our application will have methods





Since the settings and the user in the application exist in a single copy, there is no need to transfer it to the router to configure the new screen. On the contrary, having many discounts, the presentation model of the details screen of the discount should be created with the discount entity parameter (or the view model if it is sufficient).



And what about the table and the collection?



Initially, we created the data source and delegate implementation as a separate class that was stored on the view controller. This class, in turn, took the data (view model) from the presentation model.



image



The cell mapper in this scheme is a closure that aligns the cell class with the view model class. This is done in order not to manually register the classes of cells on each screen.



Thus, we have allocated most of the data source and delegate code into a separate entity.

We tried, it turned out that delegation in a separate class is inconvenient, and when selecting one data source, the gain is not so significant.



Therefore, the next iteration turned to the use of the table presentation model as a data source, the view controller became the delegate.



The scheme is simplified, the unnecessary data source and cell mapper entities are gone. Easier - better.







The scheme is simplified, the unnecessary data source and cell mapper entities are gone. Easier - better.



Revised routing



The implementation of the routing, which was described above, is bad in that all transitions are hard-coded in the view controller. To implement a weak connection between navigation and the internal device of a separate view controller, we do the following:



  1. On the specific implementation of the presentation model, we set up the desired closure - handler (or several, if navigation leads to several places) as optional variables.
  2. When creating the presentation model in the router, we install this handler. For example, when you call which should go to another screen.
  3. From the view controller, at the right time, call the handler for the presentation model.


Overall, the view controller has ceased to have knowledge of the router.



Code Generation



We should also mention another feature of the development in Redmadrobot - the use of code generation. Based on model entities using the console utility, parsers and translators for DAO are generated.



Consider this on the example of working with the essence of a discount.
 /*  @model */ class Deal: Entity { /*  @json */ let title: String /*  @json */ let subtitle: String? /*   @json end_date */ let endDateString: String init(title: String, subtitle: String?, endDateString: String) { self.title = title self.subtitle = subtitle self.endDateString = endDateString super.init() } } 




We have personally written Deal discount class. Based on its and auxiliary annotations ( @model, @json ), the code generation utility creates the DealParser parser class, the DealParser database DBDeal class, and the DealTranslator translator DealTranslator .



Class of parser, DB entity and translator
 class DealParser: JSONParser<Deal> { override func parseObject(_ data: JSON) -> Deal? { guard let title: String = data["title"]?.string, let endDateString: String = data["end_date"]?.string else { return nil } let subtitle: String? = data["subtitle"]?.string let object = Deal( title: title, subtitle: subtitle, endDateString: endDateString ) return object } } 




 class DBDeal: RLMEntry { @objc dynamic var title = "" @objc dynamic var subtitle: String? = nil @objc dynamic var endDateString = "" } 


 class DealTranslator: RealmTranslator<Deal, DBDeal> { override func fill(_ entity: Deal, fromEntry: DBDeal) { entity.entityId = fromEntry.entryId entity.title = fromEntry.title entity.subtitle = fromEntry.subtitle entity.endDateString = fromEntry.endDateString } override func fill(_ entry: DBDeal, fromEntity: Deal) { if entry.entryId != fromEntity.entityId { entry.entryId = fromEntity.entityId } entry.title = fromEntity.title entry.subtitle = fromEntity.subtitle entry.endDateString = fromEntity.endDateString } } 




Recently, we have learned how to generate service on the basis of a documented protocol (it’s worth writing a separate article about it). Until the moment when they started using zeplin, they generated color and font styles based on a text file with their description.



We use our Model Compiler library for writing utilities for generation, but Sourcery may well be suitable for this task.



Conclusion



Developing the architecture, we first of all thought about the possibility of expanding our projects, the obvious segregation of duties and low entry threshold for new developers. Of course, we also encounter complex scenarios, where some of the elements of our architecture “subside”, and we figure out how to get out of this situation, how to spread responsibility to auxiliary entities and make the code more understandable. Obviously, no architecture solves absolutely all problems. On several projects that we have been developing for more than a year, our approaches have proven convenient, and rarely any problems arise with this.



We do not preach MVC, MVVM, VIPER, Riblets and other architectures. We are constantly trying something new at the expense of efficiency. At the same time we try not to reinvent the wheel. Then we check how comfortable it is to work with this or that approach, how new developers can quickly grab these changes.

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



All Articles