Book review: A philosophy of software design (From Ch.1 to Ch.4)

Posted by Sungguk's lab on January 18, 2022

My helpful screenshot

Preface

Problem decomposition: How to take a complex problem and divide it up into piece that can be solved indivisually.

I have wondered whether software design can be taught, and I have hypothesized that design skill is what separates great programmers from average ones. (so he made a class, and teach software design)

Students learn best by writing code, making mistakes, and seeting how their mitakes and subsequent fixes related to the principles.

The overall goal is to reduce complexity.

Chapter 1. Introduction (It’s all about complexity)

All programming requires is a create mind and the ability to organize your thoughts. If you can visualize a system, you can probably implement it in a computer program. This means that the greatest limitation in writing software is our ability to understand the systems we are creating. (이해하기 쉽게 만들어라)

Complexity will still increase over time, in spite of our best effrots, but simpler design allow us to build larger and more powerful systems before complexity becomes overwhelming.

Two general approaches:

  • Eliminate complexity by making code simpler and more obvious.

For instance, Complexity can be reduced by eliminating special cases or using identifier in consistent fashion.

  • Encapsulate the complexity = modular design

Relatively indepent of each other. so that a program can work on one module without having to understand the detail of other modules.

Waterfall model rarely works well for software. -> Unlike building a bridge, software systems are intrinsically more complex than physical system. It isn’t possible to visualize the design for a large system. -> Initial design will have many problems. -> Agile development.

As a software developer, you should always be on the lookout for opportunities to improve the design of the system you are working on.

1.1 How to use this book

The best way to use this book is in conjuction with code reviews. It’s easier to see design problem in someone else’s code than your code.

Chapter 2. The Nature of Complexity

The ability to recognize complexity is a crucial design skill. It allows you to identify problem before you invest a lot of effort in them, and it allows you to make good choises among alternatives. (ALTERNATIVES is the point. we don’t always need the right answer.)

2.1 Complexity defined

Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system.

For example, it might be hard to understand how a piece of code works; it takes a lot of effort to implement a small improvement. it might not be clear which parts of the system must be modified to make the improvement. If a software system is hard to understand and modify, then it is complicated; if it is easy to understand and modify, then it is simple.

You can also think of complexity in terms of cost and benefits.

People often use the word ‘complex’ to describe large systems with sophisticated feature, but if such a system is easy to work on, then, it is not complex. It is also possible for a small and unsopphisticated system to be quite complex.

Complexity is determined by the activities that are most common. If a system has a few parts that are very complicated, but those parts almost never need to be touched, then they don’t have much impact on overall complexity of the system.

  • Complexity is more apparent to reader than writers. If you write a piece of code and it seems simple to you, but other people think it is complex, then it is complex.

2.2 Symptoms of complexity

Change amplification(변경확장)

A seemingly simple change requires code modificationis in many different places. One of the goals of good design is to reduce the amount of code that is affected by each design decisioon, so design changes don’t require very many code modifications.

Cognitive load(인지 부하)

How much a developer needs to know in order to complete a task. A higher cognitive load means that developers have to spend more time learning the required information, and there is a greater risk of bugs because they have missed something important. Complexity can not be measured by lines of code. This view ignores the costs associated with cognitive load. Sometimes an approach that requires more lines of code is actually simpler, because it reduces congnitive load.

Unknown unknowns

It is not obvious which pieces of code must be modified to complete a task, or what information a developer must have to carry out the task successfully.

It is the worst. It hides bugs. Unknown unknown means that there is something you need to know, but there is no way for you to find out what it is, or even whether there is an issue. You won’t find out about it until bugs appear after you make a change.

One of the most important goals of good design is for a system to be obvious. This is the opposite of high cognitive load and unknown unknowns. In an obvious system, a developer can quickly understand how the existing code works and what is required to make a change.

2.3 Causes of complexity

Complexity is caused by two things: dependencies and obscurity.

A dependency exists when a given piece of code can not be understood and modified in isolation.

Dependencies are a fundamental part of software and can’t be completely eliminated. One of the goals of software design is to reduce the number of dependencies and to make the deppendencies that remain as simle and obivous as possible.

Obscurity(모호함) occurs when import imformation is not obvious. A simple example is a variable name that is so generic.

Inconsistency is also a major contributor to obscurity.

In many cases, obscurity comes about because of inadequate documentation; However, obscurity is also a design issue. If a system has a clean and obvious design, then it will need less documentation.

The best way to reduce obscurity is by simplifying the system design. Dependencies lead to change amplification and a high cognitive load.

2.4 Complexity is incremental

Complexity comes about because hundreads of thousands of small dependencies and obscrities build up over time.

The incremental nature of complexity make it hard to control. It’s easy to convice yourself that a little bit of complexity introduced by your current change is no big deal. Complexity accumulates rapidly.

Once complexity has accumulated, it’s hard too eliminated. Since fixing a single dependency or obscurity will not by itself. Making a big difference. You must adopt a ‘Zero tolerance’ philosophy.

2.5 Conculusion

Complexity comes from an accumulation of dependencies and obscurities.

Chapter 3. Working Code Isn’t Enough (Strategic vs. Tactical Programming)

Many organizations encourage a tactical mindset, focused on getting features working as quickly as possible. However, if you want a good design, you must take a more strategic approach where you invest time produce clean designs and fix problems.

Why strategic approach produces better designs and it actually cheaper than tactical approach over the long run.

3.1 Tactical programming

In tactical programming, your main focus is to get someting working. However, tactical programming makes it nearly impossible to produce good system design.

Tactical programming is ‘short-sighted’. Planning for the future isn’t a priority. You tell yourself that it’s ok to add a bit of complexity or introduce a small kludge or two. This is how system become complicated.

If you program tactically, each programming task will contribute a few of these complexities. Each of them probably seems like a reasonable compromise in order to finish the current task quickly. However, the complexity accumulate rapidly, especially if everyone is programming tactically.

3.2 Strategic programming

Working code isn’t enough. It’s not acceptable to introduce unnecessary complexity in order to finish your current task faster. The most imporotant thing is the long-term structure of the system.

Most of the code in any system is written by extending the existing code, so your most important job as a developer is to facilitate those future extension. THus, you should not think of “working code” as your primary goal. Your primary goal must be to produce a great design which happen to work. This is strategic programming.

Investment mindset.

It will slow you down a bit in the short term. But they will speed you up in the long term.

Some of the investment will be proactive! It’s worth taking a little extra time to find a simple design for the new class rather than implementing the first idea that come to mind.

Other investment will be reactive. No matter how much you invest up front, there will inevitably be mistakes in your design decision. (완벽할순 없다. watterfall은 안된다는걸 증명하지않았나?). Take a little time to fix it.

If you program stragically, you will continually make small improvements to the system design. (tactiical은 작게작게 complexity를 증가시키지만, strategic은 작게작게 system design을 개선한다.)

3.3 How much to invest?

A huge up-front investment won’t be effective. This is the waterfall method. The best approach is to make lots of small investment on a continual basis.

10-20% small enough that itsn’t imppact your schedule. but large enough to produce significat benefits over time.

3.4 Startups and investment

In some environments there are strong forces working against the strategic approach.

Once a code base turns to spaghetti, it is nearly impossible to fix.

another thing to consider is that one of the most important factors for success of a company is the quality of its engineers. The best way to lower development costs is to hire great engineers. If your code base is wreck, they will get out, and this will make it harder for you to recruit. As a result, you are likely to end up with mediocre engineers.

It’s a lot more fun to work in a company that cares about software design and has a clean code base.

3.5 Conclusion

The most effective approach is one where every engineer makes continous small investments in good design.

Chapter 4. Modules Should Be Deep

Developers only need to face a small fraction of the overall complexity at any given time. This approach is called ‘modular design’.

4.1 Modular design

A system is decomposed(분해되다) into a collection of modules that are relatively independent. In an ideal world, each module would be completely independent of the other: a developer could work in any of the modules without knowing anything about any of the other modules.

Unfortunately, this ideal is not achievable. The goal of modular design is to minimize the dependencies between modules.

The interface describes what the module does but not how it does it. It consists of everything that a developer working in a different module must know in order to use the given module.

The best modules are those whose interfaces are much simpler than implementation. First, a simple interface minimizes the complexity that a module imposes(강요하다) on the rest of the system. Second, if a module is modified in a way that does not change its interface, then no other module will be affected by the modification. (인터페이스가 바뀌지않은 상태에서 구현이 바뀐다고 다른 모듈의 변경을 강요해서는 안된다.)

4.2 What’s in an interface

Formal parts: specified explicitly in the code.

Informal elements: not specified in a way in the code. 예를들어, A interface를 호출하기전에 B인터페이스를 먼저 호출해야하는 것. those can be described using comments, and programming language cannot ensure that the description is complete or accurate.

4.3 Abstractions

An abstraction is a simplified view of an entity, which omits unimportant details.

A detail can be omitted from an abstraction if it is unimportant.

An important can go wrong in two ways. First, It can include details that are not really important. It increases ‘cognitive load’. The second error is when an abstract omits details that really are important. This results in ‘obscurity’.

An abstract that omits important details is a ‘false abstraction’.

The key to designing abstraction is to understand what is important.

4.4 Deep modules

The best modules are those that provide powerful functionality yet have simple interfaces. Use the term ‘deep’ to describe such modules.

The best modules are deep: they have a lot of functionality hidden behind a simple interface. A deep module is a good abstraction because only a small fraction of its internal complexity is visible to its users.

Module depth is a way of thinking about cost versus benefit. The benefit provided by a module is its functionality. The cost of a module is its interface.

(인터페이스를 줄여야한다)

4.5 Shallow modules

Shallow module is one whose interface is relatively complex in comparison to the functionality that it provides. They don’t provide help much in managing complexity. Small modules tend to be shallow.

4.6 Classitis

(Fun fact!)

‘classes should be small’ approach is a syndrome called ‘classitis’.

Classitis may result in classes that are individually simple, but it increases the complexity of the overall system.

4.8 Conclusion

By separating the interface of a module from its implementation. we can hide the complexity of the implementation from the rest of the system.

The most important issue in designing classes and other modules is to make them deep, so that they have simple interfaces from the common use cases, yet still provide significant functionality.