During the development of UC browser, I have participated in or led many refactoring and design work. So how do you avoid massive refactoring in the future, and how do you write robust, easy to maintain, easy to expand, and durable programs? Now a few matters needing attention, tread pit record to write down, offer reference.
One-way dependence
The principle of one-way dependence is one that I always emphasize on any team and should never be violated. Start with good habits. These principles need to be etched into your bones without compromising. Circular dependencies are even scarier, and you can’t even detect them if you don’t read the code carefully, and when something goes wrong, it can be fatal. The modification cost is high, not simple optimization can solve, basically is to refactor. See ADP(The Acyclic Dependencies Principle).
I once came across A scenario in A C++ project where A depends on B, B depends on C, C depends on D, and D depends on A. The point is that A uses B in the constructor, and D calls A’s methods. However, A has not completed the construction, its method can not be called casually, so there is A crash. This leads to a chicken-and-egg problem, which can be a waste of time.
Avoid overly abstract library classes
Don’t create Common/General/Utility files or classes, because if you set a precedent, there will be a lot of stuff crammed into them that will become unbearable and impossible to control.
This point of view, I also from other articles, it can be taken doctrine. At that time, when I saw this statement, I felt the same way. I wonder if we often go down this road.
If you don’t believe me, go to your project now and search for the file name: Common/Util/Utils/Tools.
I have experienced project, almost all did such a thing, is usually feel some basic tools, difficult to name the feeling, it is hard to think of an elegant name accurately, and then, this is all you need to use the public tools, then call Common, this name is good, do not break elegant, but also all rivers run into sea rocks! Then there are common.h, utilities.java files in our project. At first, it’s all pretty basic and simple tools and functions, but over time, it becomes a hodgepodge. If you’re working on something that’s hard to categorize at the moment, or that feels like it’s common anyway, put it in there.
The result of this evolution, we are certainly not difficult to imagine, and certainly have seen such cases. We came across the InfoFlowUtils class when we were refactoring streams. Although the name of the InfoFlowUtils class already has the constraint word InfoFlow, this constraint word is too powerful for a stream product, and almost any tool can use it. As a result, there are tools for graphics, colors, strings, dates, Dispatcher operations, MD5 encryption, hexadecimal arithmetic, you name it. When you transplant code and find that your code needs to rely on these, that taste, that sour…
The only way to prevent this from happening again is to stop creating library classes with this name. Even if you like Utility naming, please strictly follow SRP (single responsibility principle) in naming, such as UrlUtil, MathUtils, ColorUtility, etc. That is, put specific constraints in front of file names. See below. Tools need to be small and beautiful.
We are encouraged together.
Low coupling, high cohesion
The definitions of coupling and cohesion are not elaborated here. The higher the coupling, the harder it is to reuse, and the harder it is to migrate code.
Low coupling at the code level, which means you need to rely on as little code or libraries as possible to get the functionality done, and dare to subtract. If you really feel that a requirement is not worth much, because the logic is weird and there is very serious damage to the design and code, you also need to dare to PK with the product.
Low coupling requires low coupling not only at the code level, but also in business logic. For example, in our project, there is a skin change function, which has two built-in skins, daytime mode skin and night mode skin. At the same time, it can technically support a variety of customized skins. In many parts of our business code, will be to test whether the current mode at night, if it is, is to add a mask, or to do color transformation, there are even do image transformation, I don’t know how the efficiency of image transformation, will not affect the interface card, anyway all the image processing, the subconscious mind is low efficiency, power consumption, Unless the transformation happens to use hardware acceleration, but there are so many Android models, can you guarantee hardware acceleration?
So what’s the right thing to do for the specificity of the night mode? First, the processing mode needs to be logically unified and consistent. In other words, the day mode and the night mode should be transparent to the business layer, and the business layer should not care about the current skin mode, only the unified solution, and need to smooth out the differences between the different modes. For example, if the night mode of an image needs to be dimmed, it should not change the pixel color value of the image. It can be done in a different way. For example, regardless of the mode, we add a mask on the image control. The color of the mask is provided by the ResManager, and the color is configured in the skin resource XML file, which is easy to modify and adjust, and does not need the business layer to couple so much skin logic. Simply configure a translucent black color in the night mode XML file and a pure transparent color in the day mode resource.
Careful with inheritance
Abstraction is not a panacea. Do not use inheritance when it is possible to solve a problem without it. Simply say: Do not use inheritance when it is possible to use it.
Although inheritance is justified in many situations and can be a very useful tool, it can be a recipe for disaster if used incorrectly, which is why we often say to be cautious about using inheritance. Regarding the complexity of inheritance and possible pitfalls, please refer to the Object-oriented design principle: Richter’s Substitution Principle (LSP).
In addition, there is an argument for using less inheritance and more composition (aggregation).
Inheritance is intrusive. If you need to write A ClassC, inherited from ClassA, then you C is A invasion, you have to follow the way to design the interface of A relationship with A C, must be set at compile time, is, of course, does not have the flexibility of operation period and then to change, if you need under certain conditions, the one that don’t need to use A feature, It’s going to be more difficult. And, regardless of the dynamics, if one day the product manager says let’s do A different thing, and you discover that the new feature has nothing to do with A, your ClassC code will have to be completely rewritten and will never be portable and reusable.
Multi-purpose composition means that you don’t have to inherit new classes from a class to add new features. Instead, you define a new class, take the class you originally wanted to inherit as your members, and then introduce more classes to assemble your functionality. You can completely design the interface for the new scenario, rather than being constrained by the old class, thus avoiding intrusion and increasing the flexibility for future changes.
I’ve seen too many cases where too much abstraction or abuse of inheritance leads to so much refactoring that you have to rewrite it.
In fact, inheritance is a kind of heavy coupling, and using composition instead of inheritance follows the principle of high cohesion and low coupling, and many design principles are understood.
Avoid centralized management
In my memory, have experienced project had centralized management, the Message string or plastic ID defined, for example, the configuration of the centralized definition, Key JSBridge registered local method of concentration, and so on, the whole project all the business scope of a function of some constants, write to the one definition file, The benefits of this approach are:
- The code is neat and uniform
- Use constant aliases to define specific strings or numbers to avoid clerical errors in use
- It is not easy to have overlapping name conflicts
- Convenient code check and monitoring
But there are drawbacks:
- Centralized definition files are prone to bloat
- Not easy to reuse code
Well, it seems that the disadvantages are not as many as the advantages, which may be the reason why many projects are always happy with it. And even if some people think there is a problem, they are under great pressure to change, and it is almost impossible to achieve the goal due to various historical reasons. Even sometimes centralisation has become the politically correct thing to do, so few people challenge convention. Therefore, only in the new nine Games iOS client project, which started from scratch, did I completely avoid the intrusion of centralized management.
In the case of centralized management, in a real project we would have a dozen or even dozens of business modules, all of which would use messages, all of which would use preferences, all of which would use dynamic configuration, and some of which would use JSBridge. We tend to centrally define things that most modules use in one place, such as message.h, jsconconst. X, configKeydef.java, and so on. Each of these business scenario resources is centrally managed and everyone adds their own constants to it. This brings a problem. One day, when we want to make a certain business unit into an independent business module or AN SDK from the whole project for reuse, we find it extremely painful. The previous code is not reusable at all, and it is difficult to get rid of it easily because it has been seriously coupled. Poor reusability is not a cause of centralized management, but it is. Moreover, there is no more than one centralized management scenario for a project, as mentioned above, so the transformation cost can be imagined.
So how to solve the problem? As long as we want to use the message mechanism, we must define the message resources uniformly, otherwise it will conflict with other modules and lead to program exceptions. In fact, message mechanism is not the only communication scheme, I personally do not like to use message as the communication mechanism between business modules, using routing instead of message mechanism is now more common scheme, not listed. A simple example of how to improve bloated coupling of Message definitions, such as our skolding feature, used to add an ON_THEME_CHANGED Message to message.h, and then the skolding module was coupled to all other modules. In fact, the skin module does not have to deal with this message, can define their own observer interface, all UI modules rely on this skin interface, register the skin change observer, when the skin event occurs, these UI modules can listen to the event. In this way, decoupling between the skin module and other businesses due to messages is removed. Of course, all UI business modules would have to register listeners with the skin module instead of registering one message callback to accept all messages. This is not a reason to use the original solution, some things are done by the business layer itself, there is no need to avoid. In addition, this is not impossible to solve, if you feel that every UI module to register the skin listener trouble, can make a wrapper class in the business layer to register listener, UI business layer Controller inherits this wrapper class. So careful use of abstraction is not to use abstraction, as long as deliberate, reasonable use can.
In addition, I would like to emphasize here that centralized management is a serious violation of the OCP(open and closed principle). The so-called open and closed principle is open to expansion and closed to modification. In layman’s terms when you design a module, someone can add new code on top of you to extend your functionality without having to modify any of your code. By doing this, you are following the open and closed principle. You can check your project code, see how much is done, how much is not done, and what you need to do to do it.
decentralized
I have read a shared article on wechat team reconstruction, in which I mentioned a point of view that core module is easy to centralize. It is a relatively important module, which has intersection with everyone, so everyone adds code to it. Finally, the module will swell and become bloated until serious problems occur. The overall idea of centralized management is roughly the same as the previous one, but the scene is different and not detailed.
The SDK,
At the beginning of the browser, we may think that we will only do a browser project and will not do other products, so we will not care too much about the reuse problem. However, with the expansion of the business, we will find that even if we do not do new products, it is completely possible to reuse internal products. If we can make some business unit, make independent business module, even directly into the SDK, if not physically SDK, but we shall interface standard, rely on the rationalization of the SDK to design these modules, so even in the future need to reengineering, also is very easy, normally you just need to refactor within a module. Because the INTERFACE of SDK must be refined, the scalability and reuse will be very strong, and will not be greatly transformed due to business expansion.
For example, as mentioned above, many of our business units should be completely independent and should not be associated with each other. Then, these independent business units should be designed according to the SDK standards, so as not to generate unreasonable dependence. However, we did manage the messages they need to communicate, THE JS interfaces they need to inject, and many other things in a unified way with the whole project resources. However, those centralized management modules cannot be reused at all, and the cost of separation and transformation is very high. As a result, when the project becomes so burdensome that it needs to be refactored, the cost of refactoring can be extremely high.
Do not have any coupling between sibling modules. If we had designed those modules as separate SDKS from the beginning, or if they were required by the SDK’s standards, it would be easy to understand why messages for different businesses could not be defined centrally.
But anything that can’t go to extremes, even a perfectly good thing, can be a double-edged sword, and it’s hard to have that kind of universal rule. For example, within a business module, and whether it can be defined centrally, I think it can. If there is one thing that can continue to be broken up and separated, then it is best not to deal with the whole business together.
Therefore, as to whether it is absolutely centralized management, can not generalize, need to see the specific scene, need to see whether the granularity is appropriate, can bring what benefits, and may face what problems, finally come to a reasonable plan in line with the current, at the same time with a certain forward-looking. This process is called design.
Zero coupling of widgets
Tools and interfaces need to be small and beautiful.
The things that can be extracted independently are designed as independent modules without dependence on the external environment as far as possible, even if they are temporarily put together for unified development. In other words, unreasonable dependence should be removed at the beginning of design, commonly known as decoupling. Don’t think about refactoring in the future.
For example, in some word processing businesses, we need to do text typesetting and rendering functions. In order to be as efficient as possible, CoreText may be used. However, when we do it, we find that the use of CoreTex API is not so easy. Then write a CoreTextRender class in your project that uses CoreText. However, if you don’t have the idea of low coupling, you will probably introduce project dependencies in CoreTextRender for convenience, such as font size, text color, etc. You might even use another tool in your project like StringUtility in CoreTextRender, and that StringUtility might have complex dependencies of its own, and then your CoreTextRender won’t be reusable at all and you’ll want to use it somewhere else in the future, I found it impossible to reuse. CoreTextRender is, in a sense, a text rendering SDK, but we didn’t make it as an SDK, but in terms of dependencies, it needs to be designed according to SDK standards.
When one day, you find another classmate also use CoreText do something, then you said to him, as I’ve done, you get to use it, as a result, he took your code in the past, discovered that a compiler error, is because you rely on the project in a thing, he then put the things in the past, and then find a N a compiler error. And he said, “No, I’ll rewrite it myself. Isn’t that embarrassing?” Think about whether there are a lot of these phenomena in our projects. When you’ve worked hard to write a standalone module, you suddenly find out who has introduced a project dependency into your code that doesn’t add much value to your module. At this time, do you have the impulse to cut someone down? This is why I mentioned in my previous article that before modifying a module, I should first discuss with the author or the person in charge of the module.
Even if you sometimes have to compromise to achieve zero coupling, try to achieve low coupling and high cohesion.
If we design all of the reusable stuff in the project according to the SDK standards, or if we actually make it into an SDK, then there will be very little refactoring in the future, and even if we do, it will be easy to do without the fear of refactoring. It will be very easy to reassemble these class SDKS into a new product in the future, and the internal code of those class SDKS will need very little modification.
Original article, reproduced please indicate the source