01 Ddd Magic Weapon for Stopping Software Degeneration

01 DDD Magic Weapon for Stopping Software Degeneration #

In 2004, Eric Evans, a master of software, published his immortal work “Domain-Driven Design: Tackling Complexity in the Heart of Software”. As the title suggests, it is a methodology book that deals with the increasing complexity of software systems. However, at that time, China’s software industry was just starting out, and software systems were not yet as complex. Even if the software degraded and became difficult to maintain after a few years, it could simply be rebuilt from scratch. Therefore, in the many years that followed, there were not many teams that truly applied Domain-Driven Design (DDD). This excellent methodology has been lukewarm due to practical reasons.

However, in recent years, with the rapid development of China’s software industry, software systems have become larger in scale and have longer lifecycles. The risk of rebuilding from scratch is becoming greater. Software teams are in urgent need of maintaining a system for many years at a lower cost. However, the reality is the opposite. As time goes by, the code becomes more and more messy, and the cost of maintenance becomes higher. Software decay has become a nightmare for numerous software teams.

At this point, microservices architecture has become the solution for scalable software. However, microservices impose high requirements on design, emphasizing “small and focused with high cohesion”. Otherwise, the advantages of microservices cannot be fully realized and may even worsen the problems.

Therefore, the design and decomposition of microservices need the guidance of Domain-Driven Design. So, why can Domain-Driven Design solve the problem of scaling software? Let’s start with the root cause of the problem, which is software decay.

The Root Cause of Software Decay #

In the past decade, the development of the Internet, from e-commerce to mobile internet, and then to the “Internet Plus” transformation of traditional industries, has been a painful transformation process. In recent years, the development of artificial intelligence and 5G technology has driven the industry towards the development of big data and the Internet of Things, marking the beginning of another round of technological transformation.

In this process, on the one hand, it brings many challenges, but on the other hand, it also brings endless opportunities. It will bring more emerging markets, emerging industries, and brand new businesses, providing us with new development opportunities.

However, when facing new businesses and new growth opportunities, can we seize these opportunities? We hope to, but every time we return to reality, back to the system we are maintaining, it is frustrating. Our software always goes through this cycle— the highest quality of software design is the first version, but as soon as the first version is launched, various requirements change, often disrupting the original design.

Therefore, with each requirement change, the software is modified, and with each software modification, the quality decreases. No matter how high the quality of the initial design is, after a few rounds of changes, the software enters a low-quality and difficult-to-maintain state. As a result, the team has to continue maintaining it at a high cost for many years.

In this situation, it is already very difficult to maintain the existing business. How can we expect more new businesses in the future? For example, consider the design of the payment function of an e-commerce website. The quality of the initial design was good:

Drawing 0.png

After the first version is launched, the first change quickly arrives - adding a discount function for products. This discount function needs to be divided into time-limited discounts, limited quantity discounts, discounts for certain categories of products, and discounts for specific products. How do we implement this requirement? It’s simple, add an if statement for time-limited discounts, another for limited quantity discounts, and so on… the code starts to expand.

Next, the second change requires the addition of VIP membership. In addition to various discounts for gold and silver card members, various benefits need to be provided to members to enjoy various privileges. To implement these requirements, we need to add more code to the payoff() method.

The third change is the addition of payment methods. In addition to Alipay, there is also WeChat Pay, various bank card payments, and payments through various platforms. At this point, a lot of code needs to be inserted. After these three changes, can you imagine what the payoff() method looks like now? Can we end the changes? Actually, we can’t. There are more things to add, such as flash sales, pre-orders, group-buying, crowdfunding, and various coupon rewards. The program becomes more and more chaotic and difficult to read, and each change becomes more and more challenging.

图片3

Here’s the problem: why does software decay and why does the quality decrease with changes? To solve this problem, we must find the root cause. To explore the root cause of software degradation, we must start by understanding the nature and laws of software. The essence of software is the simulation of the real world, and each software can find its shadow in the real world. Therefore, the only criterion for whether the business logic in software is correct is whether it is consistent with the real world. If it is consistent, then the software is OK; if not, users will report bugs and provide new requirements.

Here, we have discovered a very important clue, which is that “what kind of software should be developed is not determined by us or the users, but by the objective world”. The reason why users always change their requirements is because they are also uncertain about the rules of the objective world, and they can only think of them when they encounter problems. Therefore, for us, it is more advantageous to proactively analyze the software based on our understanding of the business, rather than meekly following the user’s requirements, as the latter will help us reduce the cost of changes.

So, we develop software in the same way as the real world, right? Actually, it’s not that simple, because the real world is very complex, and it requires a process to deeply understand the business logic in the real world. Therefore, initially, we can only understand those simple, clear, and easy-to-understand business logic in the real world and incorporate them into our software, which means that the requirements for the first version of each software are always so clear, concise, and easy to design.

However, when we deliver the first version of the software for users to use, they will find that many of the complex, unclear, and hard-to-understand business logic have not been incorporated into the software. This is inconvenient in the process of using the software and is inconsistent with the real business, so users will report bugs and provide new requirements.

“In the process of continuously fixing bugs and implementing new requirements, the business logic of the software will become more and more similar to the real world, making our software more professional and user-friendly. However, as the software gets closer to the real world, the business logic becomes more complex and the software scale becomes larger.”

You must have this understanding: simple software has simple design, and complex software has complex design.

For example, the current requirement is to calculate the amount payable for user orders according to the formula “unit price × quantity”. In this case, we can simply add a method payoff() in a PaymentBus class, and there will be no problem with this design. However, if the current requirement is to calculate various discounts, various promotions, and various coupons during the payment process, then we will inevitably create a complex program structure.

Lark20201116-102936.png

However, the reality is not like this. In reality, we initially only receive the simple requirements, and then design and develop based on these simple requirements. But as the software undergoes continuous changes, the business logic becomes more and more complex, and the software scale expands, gradually transforming from a simple software to a complex software.

At this point, in order to maintain the quality of software design and prevent degradation, it is necessary to gradually adjust the program structure of the software, gradually transforming it from a simple program structure to a complex one. If we always do this, we can always maintain high-quality software design. However, unfortunately, we have not done this in the process of maintaining software in the past. Instead, we have constantly added code to the original simple program structure, which will inevitably lead to software degradation.

In other words, “the root cause of software degradation is not software changes, software changes are only a trigger”. If we timely decouple and expand functionality during each software change, and then implement new functions, we can maintain high-quality software design. But if we do not adjust the program structure during each software change and keep adding code to the original structure, the software will degrade. This is the law of software development and the root cause of software degradation.

Preventing Software Degradation: Two Hats #

As mentioned earlier, in order to maintain the quality of software design and prevent degradation, it is necessary to make appropriate adjustments to the existing program structure when there is a requirement change. So how should these adjustments be made? Let’s go back to the example of the payment function of an e-commerce website and see how the design should be made for each requirement change.

Based on the delivery of the first version, the first requirement change quickly arrives. The content of the first requirement change is as follows:

Add product discount functionality, which includes the following types:

  • Time-limited discounts
  • Limited quantity discounts
  • Discounts on specific product categories
  • Apply a discount to a certain product
  • No discount

In the past, when we received this requirement, we would start modifying the code without much thought and made the following code changes:

Drawing 3.png

We added an if statement, which is not a good way to make the change. If we make changes like this every time, the software will inevitably deteriorate and become difficult to maintain. Why is this change not good? Because it violates the “Open-Closed Principle”.

The Open-Closed Principle (OCP) consists of two parts: the Open part and the Closed part.

  • Open : The software system we develop should be open for extension, which means that when the system requirements change, we can extend the software functionality to meet the new user needs.
  • Closed : The modification of software code should be closed for modification, which means that when modifying the software, the original functionality should not be affected. Therefore, new functionality should be implemented without modifying the existing code. This means that new and old code should be isolated and not in the same class or method.

The previous design violated the “Open-Closed Principle” because the new code was added to the same class and method as the old code. So how can we meet the “Open-Closed Principle” and implement new functionality? You realize that you can’t do anything with the existing code! Does it mean that the “Open-Closed Principle” is wrong?

The key to the problem lies in using a “Two Hats” approach when implementing new requirements. This approach requires splitting the modification process into two steps.

Two Hats:

  • Refactor the code without adding new functionality to adjust the original program structure to accommodate the new functionality.
  • Implement the new functionality.

Taking the above example, to implement the new functionality, we adjusted the original program structure without adding new functionality by extracting the Strategy interface and implementing the “No discount” class. Did the original program change? No. But the program structure has changed by adding this interface, which is called an “extension point”. Based on this extension point, various discounts can be implemented to satisfy the “Open-Closed Principle” and ensure program quality, as well as meet the new requirements. In the future, when new changes occur, we can modify the corresponding implementation class for the discount type, and when adding a new discount type, we can add a new implementation class, reducing the maintenance cost.

Drawing 4.png The significance of the design approach of “Two Hats” is substantial. In the past, whenever we designed software, we were always concerned about future changes, which led us to design many so-called “flexible designs” in an anxious manner. However, each “flexible design” can only address one type of change, and since we are not prophets, we don’t know what changes will occur in the future. As a result, either the expected changes do not happen and the designs become ornaments that do not serve any purpose and increase program complexity, or the unexpected changes do occur and the existing program still cannot meet the new requirements, resulting in the program reverting to its original state. Therefore, such designs cannot truly address future changes and are referred to as “excessive designs.”

“With ‘Two Hats,’ we no longer need to worry, nor do we need excessive designs.” The correct approach is to “live in the present and do today’s work,” which means designing for the current requirements to just meet them. The so-called “high-quality software design” is about finding a balance: on one hand, meeting current requirements, and on the other hand, making the design just sufficient to meet the requirements, resulting in the most simplified design with the least amount of code. By doing so, not only does the quality of software design improve, but also the difficulty of design is greatly reduced.

In short, “maintaining non-degrading software design” relies on designing for each requirement change. Only by ensuring that correct designs are made for each requirement change can software be continuously maintained in a virtuous cycle. This correct design approach is “Two Hats.”

However, the most challenging part in practicing “Two Hats” is the first step. Without adding new functionality, it is difficult to refactor the code and adjust the original program structure to accommodate new functionality. Many times, the first, second, and third changes can still be understood, but after experiencing the tenth, twentieth, and thirtieth changes, it becomes difficult to comprehend, and the design starts to lose its direction.

So, is there a method that allows us to still find the correct design during the tenth, twentieth, and thirtieth changes? Yes, and that is “Domain-Driven Design.”

Maintaining Software Quality: Domain-Driven Design (DDD) #

As mentioned earlier, software is essentially a simulation of the real world. Therefore, we can consider whether we can align software design with the real world. If we design the software world based on how the real world is, then every time there are requirement changes, we can revert back to the real world, see how it is, and make changes accordingly. In this way, no matter how many rounds of changes or what changes occur in the future, if we continually design based on this method, we will never lose our direction and the quality of design can be guaranteed. This is the idea behind “Domain-Driven Design.”

So, how can we align the real world with the software world? This alignment includes the following three aspects:

  • Whatever entities exist in the real world, there should be corresponding objects in the software world;
  • Whatever behaviors these entities have in the real world, there should be corresponding methods for these objects in the software world;
  • Whatever relationships exist between these entities in the real world, there should be corresponding associations between these objects in the software world.

Image 5

Correspondence between the real world and the software world

In Domain-Driven Design, these three correspondences are first transformed into a domain model, which then guides the program design. During each requirement change, the requirements are first analyzed within the domain model, followed by making changes based on the real world behind the domain model, and then making corresponding changes in the software based on the changes in the domain model. This way, the quality of the design can be improved.

Summary #

In summary, the law of software development is gradually evolving from simple software to complex software. Simple software has its own design, and complex software has its own design. Therefore, when software evolves from simple to complex, it is necessary to timely adjust the program structure with the help of “Two Hats” to implement new requirements. Only then can software degradation be prevented. However, when making changes, how can we adjust the code to accommodate new requirements?

DDD provides us with a solution: during each change, we go back to the domain model and make changes based on the business rules. Then, using the changes in the domain model as guidance, we make corresponding changes in the program. This way, no matter how many requirement changes we go through, we can always maintain the quality of the design. This kind of design ensures that the system can be continuously maintained at a low cost.