review

As the most creative job, coding is indeed one of the most popular jobs in China in recent years. Naturally, design work as a prelude to coding is more creative and uncertain. The author uses the concept of complexity to help us sort out how to simplify the design and build a simple and efficient system. Complexity is also a concept discussed throughout the book, finding ways to reduce the complexity of design by sorting out dependencies and ambiguities in order to simplify design. If someone thinks a piece of code is unintelligible, it is. In order to reduce the complexity as much as possible in the design stage, it is necessary to have strategic thinking and fundamentally change the way of thinking of “completing functions for the purpose”. Carry out system design and module design with the mentality of investment, and bring high returns with the investment in the early stage.

Chapter one: Creative Work

Coding is currently one of the most creative work in the world, and not be limited by the laws of physics, can give full play to their creativity to build some of the virtual world At the same time it also can lead to coding complexity, especially over time, the iteration, will lead to the complexity of the system is more and more high, coding and thus less error-prone

Although there are a lot of very cattle development tools to avoid coding problem, but if you want to build more powerful system, also want to simplify the coding process, we must find some more simple design can help simplify the method of system before the expansion to deal with complexity, we build a more powerful system services The author thinks that there are two ways to solve complexity:

  1. Reduce complexity by writing more concise, easy-to-understand code, such as by eliminating special handling or by using identifiers in a conventional way
  2. Encapsulate it, by modularizing the design, to encapsulate the complexity of the content so that other coders don’t have to care about the details of the module

Because of iteration, software design extends throughout the life of a software system. The complexity of software design is much more complex than other physical design, and the design of a large system can not be fully demonstrated before coding. This book is about how to identify complexity in software design and reduce complexity in the design process

Chapter two: Complexity

Recognizing complexity is a very important skill. It can help you identify a problem before you invest too much energy in it, and it can help you make a better choice between multiple options. If you can identify a system as too complex, you should try different ways or methods to simplify it when designing it. Complexity is anything related to the structure of a software system that makes it hard to understand or modify Understand and modify the system) The author thinks that if a system is simple and easy to understand, the design is simple; otherwise, it is complex. Complexity, of course, can also be evaluated in terms of costs and benefits. In simple systems, large improvements require little effort, whereas in complex systems, complexity is what developers experience at a particular point in time to reach a particular goal, and it is not necessarily related to the size or functionality of the system. If a large system (or one with many features) is easy to develop and maintain, the system is not complex according to the definition of this book; Conversely, if a small system is difficult to maintain and develop, it is also Complexity is determined by the activities that are most common. The complexity formula c=∑ (Cp) * Tp. Complexity is the sum of the complexity of each part p multiplied by the time developers spend working on that part. So isolating complex content in a module that most people won’t touch removes complexity

Complexity is relatively easy to spot for readers than for authors. If a piece of code looks complicated to the reader, it probably is. As a developer, your job is not just to write code, but to write code that others can easily understand and maintain. There are three ways to show complexity

  1. Change amplification: A simple requirement that requires multiple code changes to implement;
  2. Cognitive load: How much a developer needs to know to complete a task. High load also on behalf of the need to spend more time and energy to complete the task in order to learn more knowledge, learning that also means that there will be more risk, in the process of learning may be missing some important knowledge points Through the lines of code to determine whether the system complex is unreasonable, because this way will ignore the complexity of the cognitive load to bring. Is there something you can do with a little bit of code, but it takes a lot of time and effort to learn how to use that code or interface
  3. Unknown unknowns: Not knowing what code needs to be changed to get the job done, or developers not knowing what information they need to know to get the job done.

Of the three manifestations of complexity, unknown unknowns are the worst, compared to change amplification and cognitive load. And good design must be clear, easy to understand; In the event of change, developers can figure out what code needs to be changed with little time cost;

Complexity comes from two sources: dependencies and obscurity.

Dependencies: Dependencies exist when a piece of code cannot be modified independently or easily understood in isolation from other code. Of course, the dependency between modules is one of the components of the system and can not be completely eliminated. Unfortunately, the goal of software design is to reduce the dependencies of code so that the dependencies are as straightforward as possible and ambiguity is defined as ambiguity when some important information is unclear; For example, some variable names that are defined too generically may not contain key information such as units, resulting in increased cognitive load. In many cases, ambiguity is caused by missing documentation. But correspondingly, the authors argue that if a system requires a large amount of documentation, this is also a red line warning dependency leading to change amplification and high cognitive load. Obscurity creates unknowns and cognitive burdens and therefore complexity increases over time as the system iterates

Chapter three: Clear way of thinking

The thinking mode of coding is divided into strategic thinking mode and tactical thinking mode. The thinking mode that focuses on function operation and maintenance is called tactical thinking mode. He thinks this kind of thinking is shortsighted and cannot produce good system design. The pursuit of functional tactical coding also leads to a rapid increase in complexity and an interesting concept: tactical tornadoes: A developer who works in a totally tactical fashion coding code faster and more efficiently than any of his or her colleagues. And the consequences of this is like a real tornado, devastating!

The first step to becoming a good software designer is to break away from tactical coding. The most important thing in working code is to build a system structure that can be maintained over the long term. The author believes that most of the system code is completed by extending the base code base, so as a developer, the most important thing should be how to extend the code better, that is, to produce a good design. And that’s strategic planning strategic planning has to have an investment mindset. More time must be spent on system design than on quick completion of functional code; In the short term, this slows progress, but in the long term, it speeds it up. Of course, no matter how much time is invested up front, there will always be mistakes. When these problems are discovered, they need to be fixed. Strategic programming, in fact, is a continuous improvement of the system design, as opposed to tactical coding. Investment, of course, has to be considered. The upfront investment is a huge amount of effort, which is not necessarily effective, such as trying to design a complete system. As you learn more about the system, the ideal design approach tends to be fragmented. So the best way to do this is to invest small amounts of money over time, 10 to 20 percent of your development time at a time

Chapter 4: Dependence

The goal of modular design is to reduce dependencies between modules. To manage module dependencies, each module is defined as two parts: interface and implementation. Interfaces that define what a module does, not how; The implementation consists of The code that carries out The promises made by The interface. Modules can be large or small, and there is no fixed size specified. In object-oriented programming languages, a class can also be considered a module. As long as there is an interface and the corresponding implementation, can be considered a module. Good module design means that the interface is much simpler than the implementation. This has two benefits:

  1. The complexity of other modules that depend on this module is reduced.
  2. Modified the implementation of the module, can not need to modify the module interface, reduce the scope of change *

In modular programming, an interface is an abstract representation of a module’s function. In the process of abstraction, ignore as many unimportant details as possible. Of course, these details can only be ignored in the process of abstraction and may go astray:

  1. Including too many unimportant details can add unnecessary complexity and cognitive load
  2. Missing important details! This can lead to ambiguity

An important part of design abstraction is identifying importance and minimizing the amount of important information in the design process. The author believes that abstraction is not only used in coding to manage complexity, but is everywhere in life. For example, microwave ovens, we don’t care about their specific mechanism, just need a few buttons to heat food, and cars are the same logic. The good modules are of course those that provide simple interfaces but also implement powerful functions. The author introduces Deep to describe such modules; Module Depth can be used to measure cost-benefit ratio. The cost-benefit ratio of deep modules can be very high, because only a small amount of complexity can bring a huge amount of functionality, and the authors argue that we should not break up too many small classes and methods, which can only increase complexity. (The extreme of The “classes should be small” approach is a syndrome I call Classitis.) So module design hides complexity by isolating interfaces from implementation; Of course, module design, the most important is to improve the depth of the module

Chapter five: Information hiding

The most important technique for increasing module depth is to hide information; The basic idea is that each module should encapsulate some knowledge that represents design decisions. This information, hidden in modules, usually consists of implementation details for some mechanism, such as:

  1. How do YOU store data in a B-tree and access it efficiently
  2. How to Implement TCP
  3. How do I parse JSON documents

Information hiding reduces complexity in two ways:

  1. It simplifies the interface of modules
  2. Make the system easier to optimize

When designing a new module, think carefully about what information to hide; If you can hide more information, you should be able to simplify the module interface and make the module deeper.

Information leakage: If the same design decision appears in multiple modules, Information leakage occurs when a design decision is reflected in multiple modules. This leads to dependencies between modules. When the design decision changes, All modules need to be changed. For example, information defined in a module interface is classified as information leakage according to the definition of information leakage (similar to interface input parameters, if the parameters change, other modules related to the interface need to change). That’s why it’s important to design simpler interfaces. At the same time, the author believes that information leakage does not necessarily exist between interfaces, such as two classes dealing with the same file format: one for write and one for read; If the file format changes, then both files need to be changed so leaks are one of the red lines in software design, and you have to be very sensitive to leaks

Temporal decomposition: The authors call this design style, which leads to information leakage, Temporal decomposition. In the design process, if you only design according to the time proposed by the function, without overall consideration, it is very likely to be called into the trap of timing decomposition. For example, to design a file system, we need to read, write and modify related interfaces. According to the function or priority was proposed to realize, may be divided into multiple classes, which leads to information leakage, because every class needs to deal with the format of the file The author thinks in the temporal decomposition, execution order will reflect on the code structure: different time of operation invocation will be different methods or different classes; These operations, may use the same knowledge (knowledge), which requires the same code in a number of different places, which can lead to information leakage of information hiding can be applied to different system level, for example, in the same class can also code for encapsulation of high complexity, the hidden information; It is also important to minimize the number of references to the same instance variable. Similarly, hiding information only makes sense if the information is not dependent outside the module. As a designer, the goal is to minimize the amount of information required for appearance

Chapter 6: Generic or exclusive

In the design process, we always have to face a very common problem: designing a more general system (or module) or designing a more personalized module. The general module may be applicable to other problems in the future, saving some development time. The result of a generic design may be that some features are never used, resulting in waste. The author’s view is that partial generic design should be implemented: the implementation should focus on current features, but the interface should be a common pattern. In this way, it can also avoid over-design and make the interface design more general, and it can also better hide information. One of the most important elements of software design is determining who needs to know what and when. When details are important, it’s best to make them explicit and as obvious as possible. The author also lists some questions to ask themselves to help design a more generic and concise class

  1. What is the simplest interface that meets my current needs? If you can remove some interface parameters without affecting interface functionality, you are working on a more generic design
  2. In what scenarios will this interface be invoked? For a particular interface, consider whether you can replace it with a more generic approach
  3. Is the API easy to understand and use for the current invocation? If the caller needs to write a lot of code to call the corresponding interface, it is also a red line alarm

Chapter 7: Different abstractions for different layers

If there are abstractions with similar meanings in two adjacent layers of the system, this indicates that the system has not done a good job of stratification. The e often manifests itself in the form of pass-through (methods.) Pass-through methods refer to those methods that have almost no processing logic except calling other methods. The interface signature and calling method are also very similar. The complexity of the pass-through method is increased and it also shows that there is ambiguity in the division of responsibilities between classes. The author considers the pass-through method to be very useless and does not add any new functionality. Of course, not all equally signed methods are redundant, and as long as they implement different functions, it doesn’t matter that the decorator pattern is a common design pattern that encourages copying of the API; Extend the functionality of a class by inheriting it, such as Window and ScroolWindow. The original intention of decorators is to separate personalized features from a common core through inheritance. Another form of API duplication is pass-through parameters, where a parameter is passed through a long call chain to the final method. This approach is bound to increase complexity because the upper layer is forced to know the parameter, increasing the cognitive load and eliminating parameter pass-through through shared objects or global variables, or encapsulation through context objects. However, when using the context approach, be careful not to blindly expand the number of variables in the context

Chapter 8: Reducing Complexity

As a developer, it is instinctive to leave complex content to others to deal with in order to ensure the simplicity of relevant modules. Therefore, the author believes that developers should provide concise interfaces, increase the depth of modules or methods, improve functionality, and reduce the processing logic of callers when calling relevant APIS. For example, try to avoid arbitrary exceptions and other processing methods to avoid increasing complexity

Chapter 9: Separate or Merge

In the design process, the choice of the caution is separated or merged, split, or the decision to join the module should be based on complexity But whether to merge or apart, remember that first goal is always to reduce the complexity, and optimize the modularity The author thinks that break up a large number of small systems might be for a single system complexity is low, but the whole, the complexity and does not decrease Mainly due to:

  1. Too many can make tracking difficult, users can’t find what they rely on in a large number of modules, and add a large number of interfaces, increasing complexity
  2. Refinement requires additional code to manage related components; Furthermore, refinement creates isolation and makes calls more difficult, such as introducing distributed transactions
  3. Subdivision leads to duplication

The general principle is: if they are closely related, it is more profitable to code together; If each part is irrelevant, separate it as follows:

  1. If there is information to share, gather it together
  2. If merging together makes interfaces simpler, merge
  3. Duplication can be eliminated by merging
  4. Separate general-purpose code from special-purpose code. I feel that its core is to eliminate the repeated code, the repeated code after the abstract package call

Split merging also works at the method level, but when splitting a method, be careful to keep the depth of the method; The premise of method splitting must be that after splitting, you can still quickly understand how to make method calls, reduce the cognitive load of splitting method must be more abstract to make sense

Chapter 10: Defining nonexistent Errors

The author considers exceptions as one of the most serious sources of complexity in software systems. Because developers often don’t consider when an exception of the upper, induce dependency treatment this abnormal situation than the normal situation is more complicated large distributed system, also need to introduce a special exception handling middleware or through a lot of code to implement the abnormal processing, in order to ensure the correctness of the service. Especially when dealing with distributed transactions, improper processing can easily lead to data inconsistency and new exceptions may be added when dealing with exceptions, such as closing IO streams. For example, in a two-phase commit, if the request fails due to network latency, but the request is eventually sent to the relevant node, it is also very complicated to handle. Therefore, the author thinks that supporting exceptions will lead to clumsy and redundant code. When reading files through try-with-resourses, you may need to handle multiple checking exceptions, making your code unreadable. Do not throw an exception, but return it as a result, so that you can directly know the information of the exception, without many branches of catach like Java.) Adding too many exceptions will only increase the complexity of handling. (For most business exceptions, it is a good idea to define an exception and distinguish it by an error business code.) The author believes that throwing exceptions is irresponsible and that leaving the problem to the caller does not really reduce the overall system complexity. Exceptions become part of the interface, and adding too many exceptions to an interface can also make the class implementation shallow. At the same time, in multi-level invocation, exceptions will not only affect the direct caller, the exception stack may affect the higher level of the business or module, and the way to eliminate exceptions is very simple, just do not define the exception. On Windows and Unix, when you delete a file that has been opened by another process, Windows will report an error and the user will have to spend a lot of time trying to figure out who is using the file. Unix is very elegant, first mark the file as deleted, new process can not open, and wait for the previous process to stop before deleting the actual exception another way to eliminate exceptions is to mask the exception, the underlying layer to handle the related exception, and react to the related exception, masking the perception of the upper layer; For example, the sliding window of TCP can shield the impact of packet loss on the upper layer. Another way is to aggregate exceptions together to reduce the number of exceptions. The most extreme case is to crash directly when encountering exceptions, such as OOM exceptions or StackOverFlow exceptions

Chapter 11: Design twice

Software design is a very challenging job, just thinking about how to design a module or system may not make a good answer, designed two (twice) design it would be a good way The so-called design two people understand or try to make a different plan, even if they know might be only one method is feasible, Also try to think out another method, that is to say, want to try more different methods Perhaps a solution is not possible, but he may be able to find another solution weakness, so that you can promote each other by listing the advantages and disadvantages of all schemes, can be very easy to choose, choose one of the best solution; And even through this kind of comparison, it might be possible to regroup, to redesign something that is better than all the alternatives and the same design principle applies to all the layers. If this principle is often used in design, the author believes that it can improve the design skills: by frequently comparing multiple solutions, it will be easier to find some core elements that affect the design

Chapter 12: Write notes

Code comments help developers understand systems more efficiently, work more efficiently, and more. Documents also play an important role in abstraction. The author believes that good documentation can hide the complexity of modules and improve system design. Conversely, a design without documentation is less valuable. And the lack of documentation can be an unnecessary drag on development.

The author thinks that good code can reduce the required number of comments, but want to fully achieve comments since the code or a more idealistic idea good annotation can reduce the cognitive load, reduce the unknown unknowns, and it can eliminate the ambiguity Another important reason is the same a design idea, in a place to write relevant comments, to avoid repeated comments, Reduce annotation maintenance

Chapter 13: How to Write notes well

The purpose of comments is to make it easier for users to use, so the principle of comments is: One of the main reasons for writing comments is abstraction. Comments can reflect a lot of detail in code, such as in the JDK. But comments can provide a much simpler, more advanced view of things. So comments, don’t comment some very simple code, it doesn’t help much; Annotations, can consider to include using a hierarchy of information, so that we can to have a very fine complement the current level Comments should be clear, such as for some into the parameter, to indicate the unit, for some typical of query, should show that open and close range Comments should be as far as possible from the high-level current level, in this way, You can have a more clear introduction to the whole Want to be a good designer, besides can consider more details and good processing, but also to take a step back, from a higher level system, so that you can think what is more important in the system, able to think from the system the most basic features The so-called abstract, It’s a simple way of thinking about complex things and the author thinks that if you want to elaborate on the abstraction the only way to do that is to add comments

What: Let the reader (or user) understand what the code is doing. Why: Explain why it is doing something that is difficult to understand in the code

Chapter 14: Choosing a Good name

Self-commenting code is never out of date. Good naming is when naming comments, ensuring consistency and accuracy; The author believes that if a variable is difficult to be named with precise, intuitive and concise words, it indicates that there may be problems with the definition of the variable, and its function may not be so clear

Chapter 15: Writing comments first may be a good idea

The author believes that a big reason for the poor quality of comment writing is that it is often done after code and unit tests have been completed, and that writing comments after the entire development process is complete is overly delayed. If you keep putting off writing comments, you’ll end up with delayed work piling up until writing comments becomes a time-consuming task. Also, writing comments first reflects the design process and helps clarify your thoughts. Writing reviews early can also help you spot flaws in your design and make changes in time

Chapter 16: Modifying old Code

Software development is an iterative process. A large system becomes a large system through a series of iterations, which means that with each iteration we add some new functionality and change some old code. When you modify old code, you have to be careful not to increase the complexity of it. You have to think strategically, you have to modify old code, you don’t have to think about the least of the changes to meet the requirements or fix bugs, you don’t have to worry about the risk of introducing changes. Consider whether there is a better way to redesign this section based on this change. Constant refactoring of code is necessary. Also, when modifying old code, be sure to maintain comments as necessary; It is not enough to describe the changes in the commit log

Chapter 17: Consistency

System consistency refers to: Similar things are done in similar ways. And dissimilar things should be done in different ways. Consistency involves everything

  1. Naming: The same name must represent the same or similar meaning
  2. Coding style
  3. Interface, where multiple implementations of an interface end up rendering the same content
  4. Design patterns
  5. Immutability, a default property of a variable or data structure; For example, the end of each line is always represented by a newline character. In my opinion, it is the convention of some things, such as the request returns OK status on success, failure returns an ERROR status

System design should also consider the agreed good things, can not casually go to change it to avoid unnecessary trouble

Chapter 18: Code Should Be Obvious

Ambiguity is one of the two main causes of complexity, and one of the solutions is to write code in a way that’s easier to understand which is to make code readable, after all code is written for people to read by naming it well and keeping the system consistent, Good code style (including the formatting of comments) makes it easier for the main reader to understand the code

for(int pass=1; pass>=0&&! empty; pass--) { for (int pass = 1; pass >= 0 && ! empty; pass--) {Copy the code

For example, the above two lines of code, the next line is obviously more pleasant to read. In short, the code should be more readable, not easier to write

Chapter 19: Trends in system design

The idea of object-oriented programming is a good encapsulation of complexity inheritance is one of the key elements of object-oriented, including two forms:

  1. By way of interface implementation
  2. Through class inheritance

Although the author believes that class inheritance can reduce duplicate code and change amplification, it also believes that it causes the dependency between the parent class and the child class and leads to information leakage. When developers modify related code, they need to expand their cognition to the upper parent class. Or modify the parent class, will affect multiple subclasses So the author thinks that is caused by using class inheritance of class stratification will increase the complexity of agile development mode, each iteration will have a design, development, testing, and the author’s ideas are consistent, through continuous iteration to optimize system design But the author also thinks that Agile development tactics of programming error easily, because too much emphasis on agile development focus on the characteristics of development and design patterns is very popular now, but the author thinks that abuse can cause certain fuzzy design patterns, hard set of design patterns for any question, do not improve the design of system For audit system design, want to learn from complexity as the starting point to look at the problem

Chapter 20: Consider Performance

When designing, keep the system simple, but also keep performance in mind and optimize performance around the critical path: specify the minimum code that satisfies the function, which may be contrary to reality, but still provides a good idea. Another is to remove special processing from the critical path as much as possible

Chapter 21: Conclusion

The book is all about one thing: complexity.

Software design is a constant struggle against complexity. The book addresses two fundamental causes of complexity: dependency and ambiguity. It also shows how to avoid unnecessary complexity: information leakage, unnecessary error handling, and naming systems too loosely. Always keep an investment mentality when designing systems. How to solve the problem in the simplest way after all, spending time designing the system well is more interesting and attractive than spending time fixing bugs