Preface Before I knew it, I have been coding for more than a year. As the amount of coding increases rapidly, how to organize the code efficiently and simply often makes me think. As a methodology and practitioner (this definition is made up by me), I always hope to find some simple and effective ways to solve problems. Thus, I started a practical experience of building code. What I want to share this time is my understanding of the MVVM framework and my own workflow after practicing the MVVM structure for a long time. There may be some places that are not well handled, and I hope everyone can communicate with each other. Foreplay The concept of ViewModel is based on the MVVM structure. The full name should be Model-View-ViewModel. Structurally, it should be Model-ViewModel-ViewController-View. In simple terms, it is based on the MVC structure, and the data-related functions in the ViewController are separated to form a separate structural level. For a detailed definition of ViewModel, please refer to this MVVM introduction. In addition, in the workflow, the author has referred to the code construction ideas of BDD to a certain extent. Although the test code is not really built according to the behavior, the writing process is indeed similar to BDD. For more information about BDD, you can refer to this behavior-driven test. The demo written for this article has been uploaded to Github: Portal~ Okay, let's begin. ViewModel and ViewController Base Class Well, here, we need to use the classic OOP pattern - inheritance. We don't intend to build ViewModel's functionality too heavy, so it only needs a pointer to its own ViewController and a factory method that assigns the ViewController. Just like the following code:
ViewModel only needs a weak viewController pointer to point to its own viewController, while viewModel is held by viewController using a strong pointer to avoid circular references. That's enough. Principal and Agent In order to make the relationship between ViewModel and ViewController clearer and to be able to mass-produce ViewModel, the next thing to be defined is the structural characteristics of ViewModel and ViewController. After analyzing the reasons for the hierarchical division of ViewModel and its main functions, we can roughly summarize the following characteristics: ViewModel and ViewController have a one-to-one correspondence The functionality implemented by ViewModel is separated from ViewController ViewModel is a subsidiary object of ViewController Based on the above features, the most easily thought of inter-class relationship should be the agency/delegation relationship. It may be criticized to describe a relationship that can be seen at a glance as complicated, but it will more or less play a decisive role in the following discussion. For example, although the agency and the entrustment are determined, who is the agent and who is the entrustor? In other words, who is the drafter of the agreement and who is the implementer? The author gives two bases here to confirm this. The protocol method is a passive calling method, that is, a reverse call. Based on this, the implementer of the protocol should also be the responder of the event, using the event to drive the forward call, which in turn triggers the reverse call. The methods implemented by the implementers of the protocol are universal and abstract. Conversely, the protocol makers need to implement methods that are more difficult to abstract or more specific. This basis can also be understood from another level, that is, the implementers of the protocol should be more interchangeable. The first basis is relatively unquestionable. After all, ViewController is the holder and manager of View, and it is the only channel for View and ViewModel to interact with each other. It is somewhat unreasonable to let ViewModel drive UIViewController as the responder of View events. The second conclusion is drawn from practice. In actual development, from the outside to the inside, the frequency of view modification is often greater than the data. Therefore, the probability of refactoring ViewController is also greater than the probability of refactoring ViewModel. However, this inductive conclusion cannot be summed up in one word. Instead, it is recommended that you should make more flexible adjustments and optimizations to the structure according to these development requirements during the actual development process. In this practice, ViewModel will be used as the protocol developer to build the code. Make the agreement lighter In OC, there is a series of @protocol-related syntaxes specifically used to declare all functions related to the implementation of the protocol. However, considering that the mutual calls between specific ViewModels and ViewControllers are different, if we declare a protocol for each set of ViewModels and ViewControllers, and let them implement and call each other, the surge in code volume is basically inevitable. In order to make the entire protocol structure lighter, the @protocol related syntax is not used here, but the following code is used:
This code does several things: By using classification, the callback method declarations related to ViewModel are expanded for UIViewController. The function is similar to the parent class declaring an abstract interface and leaving it to the subclass to implement. The interface supports parameter passing. Specific classes no longer need to define protocol methods, but only require protocol parameters. By declaring this category in the base class of the ViewModel, you can ensure that all UIViewControllers visible to the ViewModel implement the protocol method, eliminating the need to write the @protocol paragraph. In the specific ViewModel and ViewController subclasses, you only need to design the callback parameters according to specific needs and build a corresponding enumeration. The main reason for making the entire protocol structure lightweight is that the content of the protocol changes frequently. Using enumeration instead of protocol can reduce the scope of change, reduce the amount of code, and facilitate customization. I have also tried to define two-way abstract methods, that is, to create some abstract methods for ViewModel so that both parties can work only according to the protocol agreed upon by the base class. However, in practice, ViewModel methods are not easy to abstract because its public methods often directly reflect the data requirements of ViewController. If abstract methods are forcibly formulated, it will cause inductive confusion when constructing specific classes. The worst result is to give up complying with the protocol, and the entire code will become difficult to maintain. Transforming needs into actions In the development process, the most common development flow is demand-driven development flow. To put it bluntly, it is to give you a schematic diagram, sometimes with good luck, there is an interactive prototype or something (if you are unlucky, it is someone else's app = =), and then leave it to you to write and draw as you please. At this time, it is recommended to properly plan the development process. The main considerations are: Development levels and sequence; Care about as few things as possible per unit of time; Easy to build and debug; Reasonably simplify repetitive work. In fact, to put it simply, it is to make the entire workflow regular and orderly to ensure that the development is reasonable and controllable. In addition, it can also effectively reduce the frequency and severity of errors. Here, I shamelessly share my simple workflow. The whole process is not complicated. In fact, you just need to open the ViewController interface first. When you encounter a place where data is needed, declare a method in the ViewModel and then pretend to call it. The code is probably like this:
This development exploits a Runtime trick, that is, Nil can respond to any message. Therefore, although we only declared the method and did not implement it, the above code can be run at any time. In other words, you can run it at any time to debug the interface without worrying about the implementation of ViewModel. It is relatively troublesome to test the callback method. My own suggestion is that after writing the callback method, call your own callback directly after the corresponding ViewModel in ViewController is called forward. If you encounter possible network requests or callbacks that require delayed processing, you can also consider writing a test macro based on dispatch_after to test the callback. Generally speaking, the development of the view interface layer is always what you see is what you get, so the test standard is the page requirements themselves. When all the requirements visible to the naked eye are met, our interface writing comes to an end. Of course, the code at this time is still fragile, because we have only done a positive implementation and have not done boundary case testing, so we don’t know whether anything strange will happen under abnormal circumstances. Fortunately, we have successfully isolated the data-related part of ViewController. In future tests, if we find any data-related bugs, we can be sure that they have nothing to do with ViewController. In addition, as I said, the requirements themselves are the test standards for the page. In other words, when you implement the requirements, your view layer has passed the test. Yes, I am going to start applying the TDD way of thinking. We have used the requirements as test cases and passed them one by one. When we have developed the ViewController, we have also declared all the public methods for the ViewModel and called them in the corresponding locations. The key point of BDD is the behavior assertion of It...when...should. In this environment, It is the ViewModel, when is each call in the ViewController, and should corresponds to the changes derived from all the data interfaces of the ViewModel. In other words, we may not be able to see all the changes caused by the behavior from the interface, but we have built a testable environment before the ViewModel is implemented. If there is enough time, the first thing at this time should be to write strong enough test code for each public method according to the specific calling environment to avoid data errors. By the way, let me say a few words about the romantic world. When building a program, it is better to be interface-oriented than implementation-oriented, because in any system, the transmission of information determines whether the system itself is powerful rather than the generation of information. When writing code, first concretize the abstract function method, and then gradually abstract the data, going through a shuttle-like process, which can more perfectly meet the goal of "high cohesion and low coupling". Fat Model If we only talk about ViewModel practice, the above content has been explained almost enough. However, since the author made a whole demo, I will explain the design of several other places. First, let's talk about the design of fat models. I only recently came across the concept of fat and thin models from the article iOS application architecture talks about the organization and calling scheme of the view layer. Before that, I just intuitively discussed with my friends that there should be a distinction between models. The fatness or thinness of models is determined by business relevance. Therefore, I sometimes directly refer to fat models as business-layer models to distinguish them from thin models. In the sample code, CalculatorBrain should be considered a relatively standard business-layer model. If a single ViewModel (or Controller in MVC) cannot solve a problem, the entire business needs to be transferred to a relatively independent Model to solve the problem. The upper layer only holds the interface opened by the Model, and the business layer Model promoted by this has obvious business traces. To put it bluntly, it is not easy to reuse. However, my own development point of view is that modules with weak business relevance should be highly reusable, that is, functions should be as unitized as possible. Modules with strong business relevance should have better refactoring and replacement performance, that is, as much functional cohesion as possible. To put it simply, for example, if this Demo is no longer a calculator, but needs to be turned into a counter or something else, the only class that needs to be refactored is the CalculatorBrain class. (Of course, this is just based on assumptions. I can't imagine the need for the underlying data to change drastically while the interface remains unchanged...) From another perspective, in the entire MVVM framework, each individual ViewModel can also be viewed as a pipeline. Two-way abstraction is done in the entire business chain, which improves the replaceability of each part of the entire business chain. I personally tend to interpret it as balancing the complexity of the upper and lower levels by designing the middle layer. Lighter ViewController The first article in the first issue of objccn.io is about lighter View Controllers. The article mentioned that ViewController can be slimmed down by moving the implementation of each protocol outside of ViewController. I was one of the practitioners of this suggestion, and even thought that this was the main function of ViewModel. However, as the development time lengthened, I had to re-examine this issue. First of all, this method will generate a lot of extra interfaces. Let's still use UITableView as an example. Suppose we let ViewModel implement UITableViewDelegate and UITableViewDataSource protocols. At this time, what if another control of ViewController wants to respond according to the scroll position of tableView? Since ViewModel is the delegate of tableView, we need to declare additional public methods for ViewController for ViewModel to call in callback methods. It is not difficult to find that the Delegate protocols of almost all view controls involve the response of the view itself. As long as it involves the interaction of different control elements in the same interface, the participation of ViewController is inevitable. I have also tried to implement UITableViewDelegate in ViewController and delegate UITableViewDataSource to ViewModel. The annoying thing happened when implementing dynamic height Cell, we input data to tableView:cellForRowAtIndexPath in ViewModel, but we also need to provide the same data to tableView:heightForRowAtIndexPath: to calculate the height. The author finally summarized the reason. It is because the View layer and the ViewController layer themselves are in a dependent relationship of holder and held. Therefore, any class that implements the protocol callback as an instance within the ViewController class is actually a cross-layer call. Therefore, it is destined to come at the cost of an additional interface. In other words, the cohesion of the ViewController has deteriorated. The other reason is about testing. The reason why we say ViewController is difficult to test is that in most cases, it does not have many decent public methods, and a large part of the private methods are parameter passing callbacks. If we implement these protocols in another class, it will not improve their testability. A more effective way should be to isolate the implementation of the protocol from the data interface, and let the implementer fill in the data through the interface instead of itself. In the demo, TopViewModel has opened data interfaces such as operationCount and operationTextAtIndex: for filling the cell content. I believe that constructing a test environment for such a data interface is much simpler than constructing a test environment for a method such as tableView:cellForRowAtIndexPath. From a side point of view, such an interface is more suitable for test coverage. Based on the above two reasons, in the subsequent development, I began to put more and more protocols back into ViewController. And because of the existence of ViewModel, I prefer to build ViewController into an independent implementation class that is only responsible for implementing interface layout and logic, so that one class can do less but do it better. postscript The functions implemented in the demos in this article are not complicated, and some of them are even too crude to be seen by others. I blame the author for his lack of imagination. In the spirit of focusing on practical demonstration, it is just a reference. The author claims to be a practitioner of methodology and agrees with the view that "the method of building code is more valuable than the code itself". Writing one or two amazing lines of code may be luck, but mastering the method to build code itself is the combat effectiveness. Try to make every line of code have a reason and basis, rather than just writing it arbitrarily, which also makes me feel more responsible, at least I can give an explanation when writing it. The above summary is based on limited knowledge and may have omissions in many places. I hope to communicate with you readers. If you can point out the omissions or even wrong views, I would be very grateful. In addition, let me say something that is not too embarrassing to say. As of the time I finished writing this blog post, although I have sought various indirect verifications and inquiries about the concepts related to "design patterns", I have not yet systematically studied the relevant concepts. To be honest, sometimes I spend a lot of effort to figure out and think clearly about the answer, and suddenly I find that a book or an article has already explained it clearly in a few sentences. In fact, it is quite frustrating. After so many times, I even resist the unknown knowledge and use it to comfort myself that I am awesome. This is probably the reason why I specifically stated that I have not systematically studied it. However, the road to development is long. In fact, we all know that we are just bricklayers climbing on the shoulders of giants. Looking back at the road under our feet, every brick is enough to make us feel ashamed. Self-deception is nothing but impetuousness and shame. So, halfway through writing this article, I have already purchased the book "Design Patterns: Elements of Reusable Object-Oriented Software", hoping to systematically learn some code construction techniques (so that I can continue to brag about it later = =). Postscript to the Postscript I am currently looking for a new job, and my intention is still iOS development, and my location is still Shenzhen. If you are lucky enough to see this article and hope that I can be your comrade-in-arms, or have a good place to recommend, I hope you can contact me. Thank you in advance~ |
<<: To make your circle of friends explode, you need to know these 10 article illustration styles!
>>: Alipay challenges real-name social networking, what is the success rate?
Source code introduction: Based on Baidu data, a ...
Despite macroeconomic headwinds, U.S. electric ve...
Compared with the three-dimensional world, the tw...
Ô´Âë¼ò½é ¿ ÉÒÔʵÏÖÈ«¾ÖÅú×¢£¬ÔÚÈκνçÃ涼¿ ÉÒÔ½«Åú...
In the fragmented Internet era, the rich media in...
What is mouth breathing? Our breathing can be div...
The PC Internet era is gradually fading away, and...
Are vertical lines and white spots on the nails a...
The world's largest deserts, such as the 7.77...
What rules does the optimization adjustment of se...
In the past 10 years, China's space industry ...
In recent years, the emergence of a large number ...
The promotion cost is almost zero, how to recruit...
The concept of "organizing an event" ma...
According to foreign media macrumors, comparing t...