Writing high quality maintainable code is not only the basic training of programmers, but also the key factor that can determine the success or failure of the project. This paper tries to summarize the common problems common in problem projects and give corresponding solutions.
1. Programmer destiny?
Programmers career inevitably encountered bad projects, some project is already bad, when you join own some of them are hand made from scratch bad project, some from the inside out, some of them are bright when you drill into found a “tar pits”, some of them are not bad at this time but there have been signs in the way of the decay.
This is basically the case in China, but I don’t know much about the situation abroad, but from the level of complaints of foreign counterparts in the English community and technical media, it should be about the same. Although the overall quality may be higher, but because of the longer information accumulated more problems. After all, such exotic terms as tar pits and Shit_Mountain weren’t invented for nothing.
Any way, that’s probably the fate of our industry — either change careers or live with bad projects and bad code. Like the law of increasing entropy in the universe:
All spontaneous processes of an isolated system tend to make its state more disordered. It is impossible to restore the system to its original ordered state unless work is done on it.
In the face of the fate of the shadow, some people resigned numb, gradually lost enthusiasm for this industry.
Those who do not accept the fate of the choice to fight against it, but there is no road on the ground, the cloud of the software crisis never really cleared, the myth of the man-moon is still a myth, so people made their own different judgments and attempts:
- Flip the table and start over:
- Many people blame bad projects on poor foundation at the beginning of the project, patching along with erratic demand, and the mess left by the architects and programmers in front of them.
- Either they don’t have the confidence to clean up the mess, or they think it’s a thankless task, and they abandon the project in the hope that an opportunity presents itself to start over.
- But they are not sure or thoughtful about how to avoid making another bad project, just blindly optimistic that they are better than their predecessors.
- Radical reformers:
- This faction blames bad projects for not having the right programming language, the latest and most powerful technology stack or tools.
- Some of them also want the opportunity to reinvent the wheel with the hottest technology stacks (Spring Boot, SpringCloud, Redis, NoSQL, Docker, Vue).
- Or even if they don’t reinvent the wheel, they decide that the existing technology stack is too outdated to be tolerated (which it probably isn’t), and that they can’t be tolerated without microservices and distribution, so they aggressively introduce a new technology stack and make radical changes to the project.
- It is very common to blindly follow the trend of immature technologies that have just become popular and not be careful about technology selection. The technology stack that is outdated in their eyes today is actually just a fad that was caught up by another group of people a few years ago.
- I don’t object to technological innovation, but again, the problem here is that they don’t understand and think deeply about the risks and side effects of major surgery, and how to avoid making another bad project with a new technical architecture. They just blindly believe that new technology can bring success.
- Nobody can stop the resume driving technology selection of the impetuous atmosphere, flowers, after all, is the company’s resources, with new things appearing very pursuit, beautification, failed also does not affect the resume resume will only add to a project on the history and several proficient skills, not to mention bad and do a project, fame and fortune is a one-way ticket.
- Conservative reformers:
- And kind of people they don’t want to give up easily this problem but still create benefit programs, because they see the value of the project is still maintained, also saw the difficulty of the stove, all things are difficult before they are easy, in fact project of cold start) there are many external factors, the price of major surgery affect business, the difficulty of system migration and risk.
- At the same time, they tried to improve the project quality gradually in a gentle and gradual way. They adopted a series of engineering practices (mainly including reconstructing hot codes, supplemiting automated tests and documents) to clear up the “technical debt” and eliminate the bottleneck restricting the project development efficiency and delivery quality.
If a problem project is a terminally ill patient, these three approaches are tantamount to giving up treatment, amputation, and conservative treatment respectively.
2. Reflections from a 35+ programmer
When I was young, I was also a desk and radical. New projects and new frameworks opened up in a big way. Along the way, experience value and skill trees grew rapidly, and job hopping and salary increases became very happy.
But in recent years, with the growth of age, on the one hand, I do not learn new things, and on the other hand, I have reflected more on the projects I have experienced.
One of the things that struck me the most was the project I started building from scratch in early 2016 and left at the end of 2018 (just from a code quality perspective) unsatisfied. Only, this time there were no excuses:
- From technical selection to architecture design to code specification, I did it all by myself. The team was not big, but I also built and brought it out by myself.
- The first six months went very well, running with the best technology and tools I had, replacing the rubbish product PURCHASED before the end of the year (yes, having a former business reference was also a big advantage).
- In the process of doing this, I tried my best to build the platform with only 20% of the resources of similar projects of other similar companies.
- If fast and economical is the highest level, then I was able to achieve more, faster and economical — the functions delivered were very rich and close to business needs, the development pace was fast, and the development resources of the company were saved.
- But now, it seems that “good” is far from being achieved. In the middle of the project, the simple and high-priority requirements have been completed, and the new challenge of the company’s business — access to another core system and external platform, the real test comes.
- The transformation project has a large impact on our system, which requires extensive modification. The most troublesome thing is that it means changing from a simple single system to a distributed system. Moreover, the business involves capital transactions and requires high reliability, which is even more difficult.
- Then the problem began to arise: The advantages of my previous architecture — simplicity and directness — were no longer advantages at this time. The simplicity and directness of the architecture could be achieved quickly and cost-effectively when the business environment and technical environment were simple, but not when the business and technical environment became suddenly complicated.
- This is reflected in the fact that structures at both the architectural and code levels are rapidly becoming complex and chaotic — entropy is rapidly increasing;
- The following things spiralled out of control: code changes become more and more difficult, test problems become more and more, production environment failures and problems become more and more, so the energy consumed in troubleshooting test problems production problems and data repair increases dramatically, a vicious cycle…
- At this point, the project is done badly! A failure that I started from scratch with no excuse!
Then I realized a very simple truth: having a blank scroll, a top paintbrush, a professional studio, does not guarantee that you can paint a beautiful scroll. If you’re not good at drawing, it’s all fantasy and fantasy.
Then I became a “conservative reformist”, because I realized that it was irresponsible to flip the table and make radical reforms. To put it in a bad way, it was hiding from one’s ears and avoiding difficulties.
Even if you flip the table and start all over again, you still need to find a way to make the new stove work, because old problems will continue to crop up as the project progresses, and you still need to face reality and not run away from it.
Facing problems will not only help you get your current project right, but it will also help you take advantage of opportunities for new projects in the future.
People at this stage of their career and natural age tend to review and summarize, and become more concerned with projects, products and even the business success of the company than in the past.
As a kind of business activity, the success or failure of software development should be judged on the basis of whether it can continuously deliver products with acceptable cost, predictable time rhythm, stable quality level and functional market needs.
In fact, it is the four elements of project management — cost, schedule, scope and quality. Traditional project management theory believes that these four elements are difficult to be mutually restricted. The art of project management lies in the balance of the four elements.
There are a lot of mature theories and books on software engineering and project management. Here I propose a new viewpoint from the programmer’s perspective — quality cannot be compromised:
- The quality factor is not one that can be sacrificed or compromised — sacrificing quality will hurt all three of the other factors, and in the same way, pursuing quality will benefit you in all three.
- While maintaining a quality level, the three elements of cost, schedule, and scope are really interdependent — typically sacrificing cost (overtime) to speed up the delivery of much-needed features.
- As the famous “broken window effect” suggests: The existence of any kind of unhealthy phenomenon, are all convey a message, the infinite extension of this kind of information will lead to adverse phenomenon, at the same time must be on the alert that appears to be accidental, individual, slight “fault”, if ignored, turn a blind eye to this kind of behavior, slow or correct, will encourage more people to batter down more Windows, It can turn into a “dam of a thousand mile” — bad code is to a project what a broken window is to a building, or an ant’s nest is to a levee.
- The good news is that once you get quality up, the project will be on a healthy track and the other three areas will improve as well. Manage quality, and you will largely be in control of the key factors for the success or failure of the project.
- The bad news is that quality can easily get out of control. There are so many cases of poor quality, bloated and chaotic projects, and so few cases of improving quality that it is regarded as an inevitable law like the law of increasing entropy in physics.
- Of course, there is a degree to everything, and only when the quality falls below a certain level will the other three elements suffer at the same time. On the contrary, when the quality reaches a certain level, the continuous pursuit of quality will not only get no obvious benefits, but also damage the other three elements — the law of diminishing marginal utility.
- This is something you need to evaluate and measure for yourself, and if your current quality level is somewhere in between, focus on improving the project. Of course, in the real world it’s rare to see a project that’s so high in quality that it doesn’t deserve attention.
3. The most common cause of project failure — poor code quality
There are, of course, many reasons for a project’s downfall, such as a person’s deteriorating health — runaway demand, business restructuring, turnover. But as technical people, if we can do our job well — write maintainable code, reduce interest costs on technical debt, and deliver a robust and flexible application architecture — that’s a lot of merit.
Although it is difficult to estimate how many projects can save this, but in my ten years of career, experience and close observation of dozens of projects, do see a lot of the project is due to poor code quality failure and regret, at the same time I also found that actually failed project many of the problems, the problem also can indeed attribution to project the chaos and poor quality of the code, For example, a common vicious cycle of project rot: messy code, more bugs, troubleshooting time, low reuse, overtime, 996, low morale…
The so-called “thousand-mile dike, destroyed by nest”, code problem is nest.
Next, let’s delve from the project management focus to the relatively small area of project code quality. Writing high quality maintainable code is the basic training of programmers. This article tries to find some common problems in failed projects at the code level, and share some design patterns based on my more than ten years of development experience as a prescription.
The topic of code quality can be difficult to explain in an article or even a book, and the many conceptual concerns involved are complex and nuanced.
I recommend chapter 2 of The Beauty of Design Patterns, “What Are the Dimensions of Code Quality? How to Write Quality Code?” This is one of the most brilliant and insightful discussions I’ve seen on the subject of code quality.
4. Review of a failed project
Here are a few code screenshots to see the symptoms and symptoms of this ailing project:
- This is one of the most core, complex, and frequently changed classes in the project, with 4881 lines of code;
- The result is a lengthy list of apis (180 public and private apis) that requires four screens to scroll through.
- Again, the import in the header extends to 139 lines, removing the first line of the package declaration and a few empty lines altogether importing 130 classes!
- There are 40 components declaring Spring dependency injection from line 156 to line 235.
Here is not to analyze this kind of problem, just a preliminary demonstration of the severity of the disease.
I’m sure this isn’t a particularly bad situation, as serious projects can be found everywhere, but it should be enough to expose the problem and dissect the cause.
4.1 Problem 1: Excessively large component granularity and excessive API
The concept of layering has long been deeply rooted in people’s minds, especially the independence of business logic layer, which completely eliminates the confusion of business logic, presentation logic, persistence logic and other problems before (non-layering era).
However, with the complexity and change of business, the complexity of business logic layer also increases sharply, which has become a new bottleneck of development efficiency. The problem lies in the division of business logic components — division of business logic components according to domain model:
- There are no industry standards and best practices on how to design the business logic layer, and most projects (the ones I’ve experienced and the ones I’ve had the opportunity to learn more about) are designed with business domain objects in mind;
- For example, domain entities include Account, Order, Delivery, and Campaign. Therefore, the business logic layer designs AccountService, OrderService, DeliveryService and CampaignService
- This is fine when the project is simple, but in fact when the project is simple you can design it any way you want.
- But as projects get bigger and more complex, there are problems:
- Component bloat: The number of Service components is almost the same as the number of domain entity objects. As a result, individual Service components become very bloat, with many apis and thousands of lines of code.
- Ambiguity of responsibility: Business logic often spans multiple domain entities and is not appropriate in any Service. Similarly, the implementation logic for a function cannot be identified in any Service.
- The dilemma of code duplication or logic entanglement: When confronted with a piece of business logic that is already implemented in another business logic API, you have to call that API if you don’t want to tolerate duplicate implementation and code. But this creates coupling and dependencies between business logic components that can quickly spread — new apis can be relied upon by other business logic, resulting in complex and even circular dependencies.
- While reusing code and reducing duplication is good, complex coupling dependencies are also harmful – driving out a Wolf and attracting a tiger. Two poisoned drinks for you to choose from!
ContractService, the problematic component shown in the previous screenshot, is a good example of a component that is often a bottleneck to hot code and the development efficiency of an overall project.
4.2 Prescription 1: Inverted pyramid structure — business logic components have a single responsibility and forbid intra-layer dependencies
The opposite of the root of the problem is the solution, but it requires a conscious change in habits and a new style of design, rather than intuition:
- The business logic layer should be designed as a very single-functional widget, with small apis and few lines of code.
- Due to the single responsibility, there must be a large number of components, each of which corresponds to a very specific business function point (or several similar ones);
- Reuse (calls, dependencies) should only occur between adjacent layers — the upper layer calls the lower API to reuse the functionality of the lower layer;
- The system architecture naturally takes on an inverted pyramid shape: the closer you get to the top, the more business scenario components there are, and the lower down, the more reusable components there are.
4.3 Crux 2: Low cohesion and high coupling
Classical object-oriented theory tells us that good code structures should be “highly cohesive and low coupled” :
- High cohesion: The component itself should contain as much important information and detail as possible about the functionality it implements so that maintainers don’t have to jump to multiple places to learn the necessary knowledge.
- Low coupling: Components depend on and understand as little as possible so that if one component needs to change, the other components are not affected.
In fact, the two are two sides of the same body. High cohesion basically means low coupling. On the contrary, if the cohesion is very low, there will be a large number of components with high coupling.
I have observed that very low projects have low cohesion and high coupling problems. The root cause is that many programmers, even experienced ones, lack the awareness to understand the concept, understand the hazards, and have no idea how to avoid them.
Many people write programs intuitively from the get-go, and with experience they generally recognize the dangers of repetitive code, have a strong sense of reusability, and fall into the trap of blindly pursuing reuse, which destroys cohesion.
- One of the misconceptions in the industry about “reusability” is that components at any level, including business logic components, should pursue maximum reusability;
- Reuse is good, of course, but only if it doesn’t add complexity to the system.
- What kind of reuse is bad for adding system complexity? As mentioned earlier, the reuse of one business logic API by another business logic API is bad:
- Stability is compromised because business logic itself is tied to real-world business and business changes; When you reuse an API that changes, it’s like building on sand — the foundation is loose;
- Increased complexity: Such dependencies also reduce code readability — the complexity of one complex business logic code can increase dramatically by including calls to another complex business logic, which can be flooded and passed around;
- Cohesion is broken: As the business logic is fragmented within the methods of multiple components, the overall logic and implementation steps cannot be seen in one place — cohesion is broken, which means that there is high coupling between all the components involved in the call chain.
4.4 Prescription 2: The two correct poses for reuse — build your own Lib and framework
There are two things in software architecture for reuse — lib and framework,
- The Lib library is called by your application to implement specific capabilities (such as logging, database drivers, JSON serialization, date calculations, HTTP requests).
- The framework itself is half an application that you can extend. It defines the component partitioning and interaction mechanism, and you need to follow its rules to extend specific implementations and bind them into it to complete an application.
- Lib is the reuse of composition, while the Framework is the reuse of inheritance. The inherited Java keyword is extends, so it is essentially an extension.
- There used to be a saying that “combination is better than inheritance, and problems that can be solved by combination should not be inherited”. I disagree. It’s tempting for beginners to think composition is better than inheritance, but inheritance is the most powerful aspect of object orientation, and of course you can’t mess with anything.
- A typical inheritance mess is to inherit in order to get some API from a parent class. Inheritance must be to extend, not to get a capability directly. To get a capability should call lib.
- You should not inherit classes in lib in order to use lib. Lib is made to be combined and called, framework is made to be inherited and extended.
- To expand: lib can be third-party (log4j, HttpClient, FastJSON) or your own project (e.g. your persistence layer Dao, your utils);
- The framework can be either third-party (SpringMVC, JPA, SpringSecurity) or business-specific (such as report, Excel export, Paging, or any reusable algorithm, process) encapsulated in your project.
- In this sense, there are really only three types of code in a project: custom lib classes, custom framework-related classes, and extension third party or component classes for a custom framework.
- One more extension: Compared to the past, we now have enough third-party libs and frameworks to reuse to help projects save a lot of code, making development seem like a boring, low-tech CRUD. But for very complex business projects, you need people with experience, abstract thinking, and design patterns to design business-oriented frameworks and business-oriented LiBs so that you can deliver maintainable, scalable, and reusable software architectures — high-quality architectures that help the project or product succeed.
4.5 Problem 3: Insufficient abstraction and Logic entanglement – The high-level service logic and low-level implementation logic are intertwined
When we say “business logic contained in code”, what exactly are we talking about? There is no standard in the industry, and CRUD, which is often talked about, is actually a lower-level data access logic.
In my opinion, the business logic in code refers to all the input and output rules, algorithms and behaviors displayed by the code, which can be divided into the following five categories:
- Input validity check:
- Business rule verification: typically, such as checking transaction record status, amount, time limit, permission, etc., usually including database or external interface query as reference;
- Data persistence behavior: any form of data writing to a database, cache, file, log, etc.
- External interface call behavior;
- Output/return value ready.
Of course, a particular component instance may not include all five types of business logic, but there may be multiple types of business logic.
This might not sound too complicated, but in reality each of the five types of business logic usually contains one to more underlying implementation logic, such as CRUD data access logic or calls to third-party apis.
For example, to verify input validity, you need to check whether the corresponding record exists. Before an external interface is invoked, you need to query related records to obtain parameters required for interface invocation. After an interface is invoked, you need to update the status of related records based on the result.
Obviously, there are two levels of logic — High Level logic that corresponds closely to business requirements and Low Level implementation logic.
If the logic of the two levels is not distinguished and confused, code quality is immediately seriously compromised:
- Poor readability: Two dimensions of complexity — business complexity and the technical complexity of the underlying implementation — are mixed together, with complexity 1+1>2 increasing dramatically, placing a great burden on others to read the code;
- Poor maintainability: Maintainability usually refers to the cost of troubleshooting and solving problems. When two levels of logic are entangled, troubleshooting will become more difficult and troubleshooting will be more prone to errors.
- Scalability is out of the question: scalability usually refers to the cost of adding a feature to a system. The higher the cost, the worse the scalability. Similar to troubleshooting and fixing problems, logic entanglement can obviously make it difficult to add new features and inadvertently break existing features.
The following code is a typical example — the High Level logic flow (parameter fetching, deserialization, parameter verification, cache writing, database persistence, updating relevant transaction records) is completely drowned out by the Low Level implementation logic (string comparison, Json deserialization, Redis operation, DAO) Operation and various trivial parameter preparation and return value processing). In the next section, I’ll present a refactoring solution for the code in question.
@Override
public void updateFromMQ(String compress) {
try {
JSONObject object = JSON.parseObject(compress);
if (StringUtils.isBlank(object.getString("type")) || StringUtils.isBlank(object.getString("mobile")) || StringUtils.isBlank(object.getString("data"))) {throw new AppException("MQ return parameter exception");
}
logger.info(object.getString("mobile") +"<<<<<<<<< Obtain authorization data from MQ >>>>>>>>>"+object.getString("type"));
Map map = new HashMap();
map.put("type",CrawlingTaskType.get(object.getInteger("type")));
map.put("mobile", object.getString("mobile"));
List<CrawlingTask> list = baseDAO.find("from crt c where c.phoneNumber=:mobile and c.taskType=:type", map);
redisClientTemplate.set(object.getString("mobile") + "_" + object.getString("type"),CompressUtil.compress( object.getString("data")));
redisClientTemplate.expire(object.getString("mobile") + "_" + object.getString("type"), 2*24*60*60);
// Successfully saved to Redis for 48 hours
CrawlingTask crawlingTask = null;
// providType: (0: Xinyan, 1XX Alipay, 2: ZZ Taobao,3:TT Taobao)
if (CollectionUtils.isNotEmpty(list)){
crawlingTask = list.get(0);
crawlingTask.setJsonStr(object.getString("data"));
}else{
/ / new
crawlingTask = new CrawlingTask(UUID.randomUUID().toString(), object.getString("data"),
object.getString("mobile"), CrawlingTaskType.get(object.getInteger("type")));
crawlingTask.setNeedUpdate(true);
}
baseDAO.saveOrUpdate(crawlingTask);
// Save sesame to xyz
if ("3".equals(object.getString("type"))){
String data = object.getString("data");
Integer zmf = JSON.parseObject(data).getJSONObject("taobao_user_info").getInteger("zm_score");
Map param = new HashMap();
param.put("phoneNumber", object.getString("mobile"));
List<Dperson> list1 = personBaseDaoI.find("from xyz where phoneNumber=:phoneNumber", param);
if(list1 ! =null) {for (Dperson dperson:list1){
dperson.setZmScore(zmf);
personBaseDaoI.saveOrUpdate(dperson);
AppFlowUtil.updateAppUserInfo(dperson.getToken(),null.null,zmf);// Set the values of identity authentication and Taobao authentication to 1}}}}catch (Exception e) {
logger.error("Failed to update my MQ authorization information", e);
throw newAppException(e.getMessage(),e); }}Copy the code
4.6 Prescription 3: Control logic separation — The Pattern of NestedBusinessTemplate
The key to solving “logic entanglement” is to find an isolation mechanism to separate the logic of the two levels — control logic separation, which has many benefits:
- As a rule of thumb, when you set out to maintain a piece of code, you want to understand its overall flow, algorithms, and behavior, rather than dive into its minutiae.
- After the separation of control logic, you only need to read the High Level part to understand the above content, the burden of reading code is greatly reduced, the code readability is significantly enhanced;
- Understanding the code is the premise of all subsequent maintenance and reconstruction, and the number of times a code is read is much higher than the number of times it is modified (one order of magnitude higher), so the readability of the code to human cannot be overemphasized, and the enhancement of readability can greatly improve the maintainability of the system, which is also the main goal of reconstruction.
- Also, in my experience, high-level business logic changes tend to come more frequently than low-level implementation logic changes, since the former are directly related to the business. Of course, different types of projects are different, and they often change at different points in time;
- In this context, the benefits of separation of control logic are even more obvious: each time you maintain or extend system functionality, you only need to change one Levle’s code, leaving the other Level unaffected or minimally affected, which greatly reduces the cost and risk of change.
After summarizing the lessons and experiences of many past projects, I have come up with a best practice or design Pattern — the business template Pattern of NestedBusinessTemplat. It is very simple and effective to separate the two types of logic. First look at the code:
public class XyzService {
abstract class AbsUpdateFromMQ {
public final void doProcess(String jsonStr) {
try {
JSONObject json = doParseAndValidate(jsonStr);
cache2Redis(json);
saveJsonStr2CrawingTask(json);
updateZmScore4Dperson(json);
} catch (Exception e) {
logger.error("Failed to update my MQ authorization information", e);
throw newAppException(e.getMessage(), e); }}protected abstract void updateZmScore4Dperson(JSONObject json);
protected abstract void saveJsonStr2CrawingTask(JSONObject json);
protected abstract void cache2Redis(JSONObject json);
protected abstract JSONObject doParseAndValidate(String json) throws AppException;
}
Copy the code
@SuppressWarnings({ "unchecked"."rawtypes" })
public void processAuthResultDataCallback(String compress) {
new AbsUpdateFromMQ() {
@Override
protected void updateZmScore4Dperson(JSONObject json) {
// Save sesame to xyz
if ("3".equals(json.getString("type"))){
String data = json.getString("data");
Integer zmf = JSON.parseObject(data).getJSONObject("taobao_user_info").getInteger("zm_score");
Map param = new HashMap();
param.put("phoneNumber", json.getString("mobile"));
List<Dperson> list1 = personBaseDaoI.find("from xyz where phoneNumber=:phoneNumber", param);
if(list1 ! =null) {for (Dperson dperson:list1){
dperson.setZmScore(zmf);
personBaseDaoI.saveOrUpdate(dperson);
AppFlowUtil.updateAppUserInfo(dperson.getToken(),null.null,zmf); }}}}@Override
protected void saveJsonStr2CrawingTask(JSONObject json) {
Map map = new HashMap();
map.put("type",CrawlingTaskType.get(json.getInteger("type")));
map.put("mobile", json.getString("mobile"));
List<CrawlingTask> list = baseDAO.find("from crt c where c.phoneNumber=:mobile and c.taskType=:type", map);
CrawlingTask crawlingTask = null;
// providType: (0: xx, 1YY Alipay, 2: ZZ Taobao,3: TT Taobao)
if (CollectionUtils.isNotEmpty(list)){
crawlingTask = list.get(0);
crawlingTask.setJsonStr(json.getString("data"));
}else{
/ / new
crawlingTask = new CrawlingTask(UUID.randomUUID().toString(), json.getString("data"),
json.getString("mobile"), CrawlingTaskType.get(json.getInteger("type")));
crawlingTask.setNeedUpdate(true);
}
baseDAO.saveOrUpdate(crawlingTask);
}
@Override
protected void cache2Redis(JSONObject json) {
redisClientTemplate.set(json.getString("mobile") + "_" + json.getString("type"),CompressUtil.compress( json.getString("data")));
redisClientTemplate.expire(json.getString("mobile") + "_" + json.getString("type"), 2*24*60*60);
}
@Override
protected JSONObject doParseAndValidate(String json) throws AppException {
JSONObject object = JSON.parseObject(json);
if (StringUtils.isBlank(object.getString("type")) || StringUtils.isBlank(object.getString("mobile")) || StringUtils.isBlank(object.getString("data"))) {throw new AppException("MQ return parameter exception");
}
logger.info(object.getString("mobile") +"<<<<<<<<< Obtain authorization data from MQ >>>>>>>>>"+object.getString("type"));
return object;
}
}.doProcess(compress);
}
Copy the code
If you are familiar with the classic GOF23 design patterns, you can easily see that the above code example is an application of the Template Method design pattern, nothing new.
It is true that I did not propose or create anything new in this scheme. I just stumbled upon the Template Method design pattern in practice, which is really suitable for solving a wide range of logical entanglement problems. Moreover, I also found that few programmers can actively apply this design pattern. Part of the reason may be that not many people are aware of the problem of logic entanglement, and not many people are familiar with the design pattern and comfortable with it. Whatever the cause, the result is that the problem is widespread.
I see a segment of programmers who are obsessed with code quality and their solution is through “structured programming” and “modular programming” :
- Extract the Low Level logic into private function, which is directly called by the function where the High Level code resides.
- Problem 1 The hard connection is not flexible. First, although the hard connection has a certain isolation effect, the two levels are static and hard associated with each other. The Low level cannot be simply replaced.
- Problem 2 Visibility within components causes confusion: The extracted private functions are globally visible within the current component — and also visible to other unrelated High Level functions — and there is still logical entanglement between modules. This is common in the hot code of many projects, and the problem is also prominent: imagine a component containing dozens of apis, each API function has one or two associated private functions, then the internal chaos of this component and maintenance difficulty are unbearable.
- Extract the Low Level logic into a new component, which is dependent on and invoked by the component where the High Level code resides. More experienced programmers might add a layer of interfaces and resort to Spring dependency injection;
- Problem 1 API flooding: Extracting new components seems to avoid the limitations of “structured programming”, but it introduces a new problem — API flooding: because components can only be called through public methods, the API doesn’t have much reuse and doesn’t need the highest visibility of public.
- Problem 2 Out-of-control component dependency of the same layer: The overflow of components and apis inevitably leads to the normal interdependence of components, which gradually becomes out of control and eventually leads to all components relying on most other components, or even cyclic dependency. Like ContractService, which has 130 imports and 40 Spring-dependent components.
Here is a brief introduction to the application of Template Method design mode, which can be summarized as follows:
- The High Level logic is encapsulated ina final function of the abstract parent class AbsUpdateFromMQ, forming a template of business logic.
- Final functions ensure that the logic can’t be tampered with by subclasses, either intentionally or unintentionally, so it must encapsulate what is relatively fixed in the business logic. For those mutable parts and temporarily uncertain parts, reserve extension points in the form of abstract protected function;
- Subclasses (an anonymous inner class) are filled in like “fill in the blank”, templates implement Low Level logic — implement those protected function extension points; Because the extension point is abstract in the parent class, the compiler reminds the subclass programmer what to extend.
So how does it avoid the four limitations of the two schemes above:
- When a Low Level needs to be modified or replaced, it simply extends a new subclass from the parent class, and the parent class does not know anything about it.
- Function is not visible to the XyzService component of the outer layer, whether it is a parent class or a subclass. Public function is not visible to the XyzService component of the parent class, because only the instance object that holds the class can access the function.
- No matter the parent class or the subclass, they are as the internal class of XyzService, will not add new Java class files will not add a lot of meaningless API (API is only meaningful in the project reuse or published for external use, only the unique caller API is not necessary);
- The problem of runaway component dependencies is certainly gone.
In fact, Template Method design pattern has been widely used in open source projects such as SpringFramework, which should have brought inspiration and demonstration to us application developers, but unfortunately the industry has not noticed and given full play to its value.
The NestedBusinessTemplat pattern is fully and actively applied. The two correct poses for reuse mentioned in the previous section — building your own Lib and framework, NestedBusinessTemplat is the framework of the project itself.
4.7 Crux 4: Ubiquitous if else psoriasis
No matter what your initial programming language is, one of the first logical control statements to learn is if else, but unfortunately it can become a bad habit once you start doing real programming.
If else flooding is a common problem in almost every project, but it is not considered serious enough and is considered normal by many programmers.
First, let me explain why this seemingly innocuous if else is harmful and should be strictly controlled:
- if else if … Else and similar switch control statements are essentially hard coding behavior. If you agree that “magic number” is a wrong coding habit, then if else is also wrong hard coding style.
- The problem of hard coding is that when the requirements change, it needs to be modified everywhere, so it is easy to miss and make mistakes.
- Take a piece of code as an example for specific analysis:
if ("3".equals(object.getString("type"))){
String data = object.getString("data");
Integer zmf = JSON.parseObject(data).getJSONObject("taobao_user_info").getInteger("zm_score");
Map param = new HashMap();
param.put("phoneNumber", object.getString("mobile"));
List<Dperson> list1 = personBaseDaoI.find("from xyz where phoneNumber=:phoneNumber", param);
if(list1 ! =null) {for (Dperson dperson:list1){
dperson.setZmScore(zmf);
personBaseDaoI.saveOrUpdate(dperson);
AppFlowUtil.updateAppUserInfo(dperson.getToken(),null.null,zmf); }}}Copy the code
- if (“3”.equals(object.getString(“type”)))
- “3” is a magic number. No one knows what “3” is.
- But simply recomposing “3” into the constant ABC_XYZ doesn’t improve much, because if (abc_xyz.equals (object.getString(“type”))) is still procedural-oriented and cannot be extended;
- The constant ABC_XYZ referenced everywhere is not much better than the magic number hard coding everywhere, it just has meaning;
- Upgrading constants to Enum Enum types is not much better, you still need to change all over the place when the type to judge is added or the rules to judge change.
- Not all if else is bad, such as if (list1! =null) {is harmless, there is no need to eliminate it, there is no feasibility to eliminate it. The basis for judging whether it is harmful:
- It doesn’t matter if there are only two possibilities for an if to determine the state of a variable (such as Boolean or null).
- Conversely, if the variable judged by if has multiple states, and new states may be added in the future, then this is a problem;
- Switch statements are definitely harmful because there are many states where switches are used.
4.8 Prescription 4: Enumeration Type of congestion — Rich Enum Type
As the previous analysis shows, simply reconstructing the values being compared into constants or enum enum types doesn’t make much of an improvement on the widespread if condition of state and type in code — consumers still rely directly on concrete enumerated values or constants, rather than on an abstraction.
A solution naturally emerges: encapsulate the enum enum type further and get what’s called a “congested” enum type. The code says:
- Realize a variety of system notification mechanisms, traditional practices:
enum NOTIFY_TYPE { email,sms,wechat; } // Define an enum -- an enumeration type that defines only "anaemic" values without any behavior
if(type==NOTIFY_TYPE.email){ //if determines whether the type invokes different notification mechanisms. }else if(type = NOTIFY_TYPE. SMS) {... }else{... }Copy the code
- Implementation of a variety of system notification, congestion enumeration Type — Rich Enum Type mode:
enum NOTIFY_TYPE { //1. Define a "congested" enumeration type that contains the notification implementation mechanism
email("Email",NotifyMechanismInterface.byEmail()),
sms("Text",NotifyMechanismInterface.bySms()),
wechat("WeChat",NotifyMechanismInterface.byWechat());
String memo;
NotifyMechanismInterface notifyMechanism;
private NOTIFY_TYPE(String memo,NotifyMechanismInterface notifyMechanism){//2, private constructor, used to initialize enumerated values
this.memo=memo;
this.notifyMechanism=notifyMechanism;
}
//getters ...
}
public interface NotifyMechanismInterface{ //3. Define the interface or abstract parent of the notification mechanism
public boolean doNotify(String msg);
public static NotifyMechanismInterface byEmail(a){//3.1 Returns an implementation of a policy that defines the mail notification mechanism -- an anonymous inner class instance
return new NotifyMechanismInterface(){
public boolean doNotify(String msg){... }}; }public static NotifyMechanismInterface bySms(a){//3.2 Define an IMPLEMENTATION policy for the SMS notification mechanism
return new NotifyMechanismInterface(){
public boolean doNotify(String msg){... }}; }public static NotifyMechanismInterface byWechat(a){//3.3 Define the implementation strategy of wechat notification mechanism
return new NotifyMechanismInterface(){
public boolean doNotify(String msg){... }}; }}//4
NOTIFY_TYPE.valueof(type).getNotifyMechanism().doNotify(msg);
Copy the code
- Rich Enum Type:
- It is not difficult to find that this is actually a clever combination of enum enum type and Strategy Pattern.
- When you need to add a new notification mode, you only need to add a value to the enumeration class NOTIFY_TYPE and a by method to the policy interface NotifyMechanismInterface to return the corresponding policy implementation.
- If you need to modify the implementation details of a notification mechanism, you only need to modify the corresponding policy implementation in NotifyMechanismInterface.
- Notify_type.valueof (type).getNotifyMechanism().donotify (MSG);
- Comparative advantage with traditional Strategy Pattern: Common Strategy Pattern can also eliminate if else judgment, but it is more difficult to implement, requiring more class and code development:
- Each policy implementation needs to be defined as a separate class;
- You also need a Context class to initialize — Map the type to the corresponding policy implementation;
- Get specific policies from Context when used;
- Rich Enum Type of further hyperemia:
- The enumerated types in the above example contain behaviors, so they already count as congestion models, but they can be further congested;
- For example, in some scenarios, there is no need to abstract the computation logic to a NotifyMechanismInterface when simply calculating the enumeration value to obtain a flag.
- You can add static functions to enumerated types to encapsulate simple calculation logic;
- Further abstraction of policy implementation:
- When there is a common part and repeated logic in each policy implementation (byEmail bySms byWechat), it can be extracted into an abstract parent class.
- Then implement elegant logical separation and reuse between subclasses as in the previous section, the Pattern of NestedBusinessTemplate.
5. Fire scout before refactoring: Compile a CODEX directory/index for your project
These are the four most common code quality issues I’ve identified and their solutions:
- Business logic layer components with single responsibility, small granularity, high cohesion and low coupling — inverted pyramid structure;
- Build your project’s own Lib layer and framework — the right reuse posture;
- Pattern of NestedBusinessTemplate — Control logic separation;
- Rich Enum Type — Eliminates hard-coded if else criteria;
The next step is to start refactoring for these four aspects, but it’s not that simple.
Above all the content from the practical experience, but to apply to your specific project, also need a step – fire scout – to figure out what you want to refactor the logical context and algorithm of the module so that the implementation details, or to begin, it is easy to risks missing key details and reconstruction efficiency, more difficult to guarantee the embarrassment of a dilemma.
I worked on three projects with very bad code in 2019, and the biggest takeaway was that I came up with a best practice for sorting out bad code — CODEX:
- As you read through the code, add structured comments in key places, such as: //CODEX ProjectA 1 Checkup Appointment Process 1 Appointment Service API entry
- The so-called structured annotation is to reflect the corresponding project, module, process step and other information in the annotation content through the standard named number prefix, delimiter, etc., similar to the title 1, 2, 3 in text editing.
- The IDE tool is then set up to recognize this particular annotation for structured display. Eclipse Tasks display something like the following image;
- This structured view is essentially an index and directory of the code base. Unlike javadoc documents, CODEX has a clearer logical hierarchy and easier code lookup. Click on Eclipse Tasks to jump to the corresponding line of code.
- These structured comments are shared by the team as they are submitted along with the code;
- Such an accurate, shared, and living index of source code will undoubtedly be a great help to the development and maintenance of the entire team.
- Furthermore, if the Markdown keyword is added to CODEX, the exported CODEX can even be simply processed into a Sequence diagram of business logic, as shown below.
6. Summary — Live up to the best of times for programmers
There is no doubt that this is the best time for programmers, the Internet wave has swept every corner of the world, and all industries are relying more and more on IT. In the past, only software companies, Internet companies and the banking industry hired programmers. With the popularity of cloud computing, the rise of industrial Internet and Internet plus, more and more traditional enterprises have begun to hire programmers to build IT systems to support business operations.
The exuberant demand for IT driven by capital makes programmers scarce talents. The number of positions and salary level of programmers have long been among the best on various recruitment platforms.
But what about the overall performance of our group? Honestly, I find it hard to be satisfied. Few of the projects I’ve experienced or seen up close can be called successful. Success here is not commercial success, but is limited to whether a software project and engineering can be delivered consistently over a long period of time at an acceptable cost and quality.
The short-term success of a business is often not necessarily related to the success of the project. A commercially successful project may not do well in the project, but only achieve temporary success through huge investment of capital resources.
At the end of the day, we programmers as a community are responsible for our reputation, and in the long run have our reputation to gain or lose.
In my opinion, the greatest reputation and the most important professional quality of programmers is to write high-quality code to do a good job in each project and product, to help the team, the company and the organization to create value and increase the chances of success.
I hope the experience and methods shared in this article can help!
This article is a technical director of my good friend: the right elder brother spent half a month to write out the conscience of the article, strongly recommended to you, the article is very long very hard very valuable, we can collect to see several times. I hope you forward after reading, point to see, a good article to let more people see.