One, foreword

With the continuous iteration of the business, the UI controls used to distribute video resources in Youku APP become more and more complex, and there are also many similar codes. Upon closer examination, it turns out that there are many coupling problems:

1) The layout code coupling data model, similar layout components each set of layout code;

2) The inheritance relationship between data model and UIView is too long, so when changing it, it will affect the whole body and have to start its own business for insurance.

3) Dependency import, which introduces sequential dependencies when one component is used under another bundle.

In view of this, we need to find a way to further reduce the access threshold of common capabilities, improve the development efficiency of individual components; To further reduce the coupling between components and pages, establish the framework of the common ability of various components in different pages.

Second, the exploration of plug-in page architecture

Let’s first look at an excerpt of ViewController code. The ViewController implements three features, namely A,B and C, and these slightly complicated features cannot be completed in one step (to be more specific, It is triggered at a certain time, followed by a callback to complete the remaining operations, and finally constitutes a complete feature.

 Copy code

@implementation ViewController - (void)viewDidLoad { [featureA step1]; [featureB step1]; [featureC step1]; } - (void)callback_xxx { [featureA step2]; [featureB step2]; } - (void)callback_yyy { [featureC step2]; } @endCopy the code

This is a basic form of code organization, but faces two pain points:

First, dependency explosion. Every feature will inevitably introduce a batch of dependencies. When the number of features is increased, the import statement alone will be dozens of lines.

Secondly, code dispersion. The related codes of the same feature are scattered in callback, reused to another ViewController or scrapped and removed from the shelf. Developers must be very familiar with every step and even every line of code of this feature. How to solve the above pain points is a breakthrough when we do the architecture blueprint. At this point, we try to change the code organization form around ViewContorller into one around feature code, and then we can get the following 3 code excerpts:

 Copy code

@implementation FeatureA - (void)recvViewDidLoad { [self step1]; } - (void)recvCallback_xxx { [self step2]; } @endCopy the code
 Copy code

@implementation FeatureB - (void)recvViewDidLoad { [self step1]; } - (void)recvCallback_xxx { [self step2]; } @endCopy the code
 Copy code

@implementation FeatureC - (void)recvViewDidLoad { [self step1]; } - (void)recvCallback_yyy { [self step2]; } @endCopy the code

It is not hard to see how the problem of fragmentation has been solved by reorganizing the code. From the perspective of a single feature, multiple dependencies have converged to the inside of the feature, and the number of dependencies has been reduced from N to 1 when the feature is accessed. As long as a proper way is used, the last dependency can also be eliminated.

At this point, we need to give play to our imagination and imagine each feature as an electrical appliance, which is equipped with a unified specification plug. A ViewController is like a plug board, and appliances work no matter which board they’re plugged into. By extension, not only is a ViewController a plugboard, but any class is also a plugboard, and their functional business logic is still organized in feature mode. The tone of the plug-in page architecture is set.

Plug-in is one of the decoupling schemes commonly used in the industry. We have made the transformation of the current architecture in this direction, combined with the actual situation of Youku, and obtained a set of page architecture framework characterized by modularization, plug-in and key-value of data.

1) Modularization — business entities conduct modularization, and modules and modules present a certain organizational form;

2) pluginization — pluginization of functional units, which can be combined, disassembled and replaced;

3) Key-value of data — a minimalist data organization form, eliminating dependencies introduced by the data model.

3. From business modules to architecture overview

Combined with Youku APP business, we divided UI elements into modules from large to small, including pages, drawers, components and pits. Components are made up of several identical pits, and similarly, several components are made up of drawers, and several drawers make up pages.

Modules at different levels have their own functional units, as shown in the following table:

Module level Functional units
The parent page Page card container and buried point statistics (PV)
page Page level network data request Page level data cache buried point statistics (PV)
The drawer List container drawer level layout management (tiled, multi-tab page-turning drawer level network data request
component List container component-level layout management (multi-row, multi-column tiling, waterfall flow, slideshow, wheel casting) component-level network data requests
A pit UI unit (i.e. specific, local UI implementation) Gesture response (click, double click, long press) Routing jump buried point statistics (click, exposure, play)

Large module is composed of a number of small modules. If these large and small modules are connected together with line segments, a huge tree structure can be obtained, and each module is equivalent to a node in the tree. The functional unit is associated with each node here, and a functional unit corresponds to one or more plug-ins. The functional unit code of the module is carried by the plug-in, and the functional units inside and outside the module transmit messages and data through events, together with key-value data storage, so that we can get the embryonic form of this architecture. After comprehensive arrangement, four core managers can be obtained:

1) The ModuleManager is responsible for module lifecycle and relationship management;

2) PluginManager is responsible for the relationship management between modules and plug-ins;

3) EventManager is responsible for message communication between plug-ins inside and outside the module;

4) DataManager is responsible for module data management.

On this basis, we transform the commonly used list container, UI layout logic, buried point statistics logic, network request logic, user interaction gesture logic, route jump logic and other general logic into abstract plug-in, and finally form a 4+N architecture.

4. Module representation and management

How to represent a module is the first problem we solve. In the real world, we identify each person with an ID, and similarly each module should have a uniquely identified ID. The module ID belongs to the core of the core in the whole architecture system, and it is used very frequently, such as data reading, message passing, association and binding between entities. We represent a module with an object of the Context class. The simplest Context class has one and only one ID attribute. Here we specifically define and introduce the ModuleProtocol, so that if other generic classes follow the protocol, we can consider such an instance object to be associated with the module represented by the same module ID.

 Copy code

@protocol SCModuleProtocol <NSObject> // SCModuleProtocol <NSObject> /// Module Id, globally unique @end@interface SCContext: NSObject <SCModuleProtocol> @endCopy the code

We according to the business module page, drawer, components, pit level 4, respectively formulate PageContext/CardContext/ComponentContext/ItemContext, at the same time to make it easier to build a weak reference within the Context class attribute between different modules at the level of use. Context has two main functions: it represents the module itself, and it is a syntactic sugar for module relations.

The ModuleManager is responsible for module lifecycle management and module relationship management, including interfaces for registering modules, deregistering modules, and querying upstream and downstream modules of modules.

 Copy code

@interface SCModuleManager : NSObject + (instancetype)sharedInstance; - (void)registerModule:(NSString *)module supermodule:(NSString *)supermodule; /// register module - (void)unregisterModule:(NSString *)module; - (NSString *)querySupermodule:(NSString *)module; // query parent module - (NSArray<NSString *> *)querySubmodules:(NSString *)module; // query submodule @endCopy the code

5. Key-value data storage

In order to eliminate the dependency introduced by the data model, key-value storage scheme is adopted, using string as Key. And agreed Value using only basic data types (int/double/bool, etc.), string (nsstrings), collection types (NSArray/NSMutableArray/NSDictionary/NSMutableDictionary ) and other system-provided data types (NSValue, etc.), underplaying the use of custom data models (protocols) in the use of data.

 Copy code

/ / write data [[SCDataManager sharedInstance] setdata: propertyValue forKey: propertyKeymoduleId: moduleId]; // Read data [[SCDataManager sharedInstance] dataForKey:propertyKey moduleId:moduleId];Copy the code

Data for each module is stored in the data center. The data center creates an independent space for each module to store data, which ensures that the data of different modules is not crosstalk and that the data in the same module is shared. In the same module, only the field name parameter can read and write data; Under different modules, only one more target module ID parameter can be added to read the data. That is:

In the use of data center, it must be noted that: 563513413, no matter you are big cattle or small white are welcome to enter

1) The purpose of key-value storage is to reduce the dependence of data model, and the use of user-defined types for values should be avoided; otherwise, the Value of key-value storage itself will be lost.

2) Not all data needs to be stored in the data center, only public data should be put in the data center, and private data (such as temporary variables, etc.) should not be put in the data center.

In terms of data center capability design, we provide:

1) Provide two storage schemes, strong reference and weak reference, for developers to use as needed;

2) Safe read and write interface, to the data of the conventional error-prone type check, legitimacy check, etc.

Six, functional unit plug-in

Take ViewController for example, in the wild growth iOS development era, the list logic, network request logic, Navigationbar logic and many other functional units are spread out in the ViewController to achieve. The ViewController implements so many different protocols that the ViewController code becomes bloated. Later, for this problem, the boundary of functional units was clearly demarcated and various managers were added. The logic of each functional unit was realized inside the Manager, and the ViewController was only responsible for scheduling back and forth between many managers, thus alleviating the problem of overstaffing.

Under the increasingly rich and complex business logic, it is not enough to solve the bloated code, but also to solve the problem of flexible invocation and code reuse. In practical practice, the following problems are often encountered:

1) The interface design of functional units is deformed, and there are frequent calls between them, resulting in a high coupling of “I am in you, I am in you”, resulting in higher and higher maintenance costs;

2) The personalized customization of functional units leads to the problem of inheritance chain: there are too many subclasses of different businesses, and the parent class leads the whole body, which is not easy to change, but dare not change, and patches are patched up;

3) High cost of functional unit reuse, reuse a small block, dependent on a large block, resulting in low code reuse willingness. Access parties would rather rewrite or Copy&Rename the code.

The goal of functional unit plug-in is to further reduce the coupling between functional units. The idea and principle of plug-in should ensure that the above problems can be effectively solved.

1) Lightweight access. Reduce or eliminate class to class, class to protocol reference dependencies;

2) The plug-in can be combined, disassembled and replaced, so that the upstream and downstream of the business logic can not be perceived;

3) The boundary of the plug-in is clear, and the input and output are clear.

  1. Event mechanism – More flexible communication

Event mechanism adopts the “publish-subscribe” design mode. Functional units drive the flow of information by publishing events, and receive and process information by subscription events. The two parties communicate according to the event name agreed in advance, and the event processing center is responsible for the event distribution, so there is no direct dependence between the two parties. It is important to note that there can be multiple receivers of information in the event mechanism.

EventManager acts as the event processing hub, through which publishers publish events, and EventManger distributes events to subscribers in ascending order of subscription priority. After processing the event, the high-priority subscriber sends the return value (if any) to the EventManager, which distributes the return value (if any) from the previous subscriber to the next subscriber along with the publisher’s entry, and so on until all subscribers are processed. At this point the EventManager outputs the final return value, if any, to the publisher. The illustration is as follows:

Code examples of event publishing and event subscription and processing:

 Copy code

NSString *eventName = @"demoEvent"; NSString *moduleId = ... ; NSDictionary *params = @{... }; NSDictionary *response = [[SCEventManager sharedInstance] fireEvent:eventName module:moduleId params:params]; + (NSArray *)scEventHandlerInfo{return @[@{@" event": @"demoEvent", @"selector": @"receiveDemoEvent:", @"priority": @500}, ]; }{1}- (void)receiveDemoEvent:(SCEvent *)event{ //do something ... event.responseInfo = @{... }; Return value (optional); } {1}Copy the code
  1. Use event mechanisms in plug-ins

We treat the plug-in as a subscriber to the event mechanism and allow a new event to be initiated in an implementation that handles the event. In this way, plug-ins can be connected by events to complete a complete business logic.

In addition to the event mechanism protocol, there is only a dependency on the event name (custom data types are not recommended in event parameters, otherwise explicit dependencies will be reintroduced). The event name itself is a string, which reduces the header dependencies between various functional units caused by calls.

With a plug-in to carry the characteristics of the realization of business logic is very flexible, developers can according to your own judgment to decide the size of the plug-in, plug-in granularity but can small, plug-in internal implementation also can end at any time using the event mechanism and go back to the other general class and class, class and protocol mechanism to achieve specific business logic.

Therefore, we agreed that the boundary of the plug-in must be clear, the principle of single responsibility must be achieved, and the input and output must be clear and simple enough. If the above conditions are not met, it means that the plug-in has the possibility and necessity of disassembly and segmentation.

  1. The combination of plug-ins and modules

The relationship between plug-ins, functional units, and modules has the following four points:

1) A module instance is associated with multiple plug-in instances, but one plug-in instance corresponds to only one module instance;

2) When the module is initialized, all the plug-ins are mounted, and the life cycle of the plug-in is basically synchronized with that of the module. It is not allowed to mount or uninstall a plug-in at a certain point in the process;

3) A business function in a single module, that is, a functional unit, composed of one or more plug-ins;

4) A cross-module business function, that is, a cross-module function unit, jointly carried by multiple plug-ins belonging to multiple modules.

Plug-in and the connection between the module through a configuration file, each module is initialized, through a configuration file, the initialized associated with plug-ins and binding, plug-in subscribe to specific events and operational mechanism, until the module is cancelled, plug-in unsubscribe all events and the end of the life cycle.

7. Architectural practice

This section uses diagrams to illustrate how to write a button function using plug-ins. A page has a single button and supports clicking to jump to.

We treat this functionality as a unit and simply implement it with a plug-in:

1) Register the module at ViewController initialization and initialize ButtonPlugin through a series of managers;

2) All button-related logic is converged in ButtonPlugin, and no button-related code directly appears in ViewController;

3) The ViewController sends the ViewDIDLoad event to drive other plug-ins to work;

4) ButtonPlugin receives the ViewDIDLoad event, initializes and adds to the ViewController, etc. When the user clicks the screen, the Tap operation is processed by ButtonPlugin.

Clicking a button involves both statistics and jump logic, so ButtonPlugin can actually be split into two other plug-ins to implement their logic separately.

We can see that when the click behavior is split into jump and statistics plug-ins, the responsibilities of plug-ins are more simple and the reusability is greatly improved. If the product presents new click requirements, for example, you must check whether the login status is correct before switching to another user. If the user does not log in, he/she must log in first and continue subsequent operations. We only need to add a LoginCheckPlugin to handle the logic and do not need to modify the plugin code, which is one of the advantages of plug-ins.

Conclusion;

Only the right architecture, there is no best architecture. Plug-in page architecture has both advantages and disadvantages. It subverts the MVC architecture development experience, increases the learning cost of developers, and the compiler does not help developers with compile-time (event name mismatch, etc.) verification. Therefore, we give full play to its aspect programming ability. In the development process, we add debugging classes and monitoring class logic in the form of plug-ins to alleviate the shortcomings of the architecture. On the other hand, we establish a standardized plug-in management platform to systematically manage all plug-ins. At the same time, the development of standardized events makes unified logic closure, which greatly facilitates the construction of tools such as code debugging and online problem location.

The main scenes of Youku APP have been connected to the plug-in page architecture, including the home page, hot spots, members, personal center, search, play page and other six sections. Precipitation of CollectionView, network request, gesture processing, route jump, buried point statistics and other series of systematic plug-ins.

When building a new page, the above series of plug-ins can be quickly accessed and realized in the form of configuration and callback. Also thanks to the increasingly sophisticated list layout plugins, developing complex layout components such as sliders, waterfall streams, and carouches is as effective as developing tiled components. According to rough calculations, component development efficiency has increased by more than 30%. At the same time, the unified configuration format enables the client to have the ability to put components across pages and across plates, breaking the dependency boundary between frameworks. The plug-in page architecture is a good place to start, and we will continue to refine and exploit its capabilities to make it more stable and efficient to support business growth.