-
Notifications
You must be signed in to change notification settings - Fork 9
Code
Any fool can write code that a computer can understand. Good programmers write code that humans can understand. ~ Martin Fowler
The essence of development is composition. The essence of software design is problem decomposition.
Easier to change principle: A thing is well designed if it adapts to the people who use it. For code, that means it must adapt by changing. [b]
- Every design principle out there is a special case of ETC.
- When it comes to thinking about software, ETC is a guide, helping you choose between paths, just like all other values.
It is a mistake to optimize code to reduce typing. We are optimizing for the wrong thing. Code is a communication tool; we should use it to communicate. We should optimize for thinking, not typing! [c]
Complexity is more apparent to readers 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.
- System should be designed for ease of reading, not ease of writing.
- If I don't understand you, it's your fault (Yegor Bugayenko)
- Complexity is incremental. It's not a particular thing that makes a system complicated, but the accumulation of dozen or hundreds of small things.
Essential complexity is the kind that is inevitable and inherent in the nature of a project domain. Can never be resolved or avoided just by using a particularly good design. The essential complexity of the problem has thus become the essential complexity of the solution.
- Ideally, a solution will only be as complex as the problem.
Accidental complexity can arise from misunderstandings during the analysis of the domain as well as during implementation by the development team.
The problem complexity defines the essential complexity. The solution complexity cannot be lower than the problem complexity. Otherwise, the solution would not solve the problem. Ideally, the solution complexity equals the problem complexity. In that case, we do not have any accidental complexity.
Symptoms of complexity:
- Change amplification: Simple change requires code modifications in many different places.
- Cognitive load: How much a developer needs to know in order to complete a task.
- Unknown unknowns: It is not obvious which pieces of code must be modified to complete a task.
Complexity is caused by dependencies and obscurity.
- Dependencies are a fundamental part of the software and can't be completely eliminated. In fact, we intentionally introduce dependencies as part of the software design process.
- Reduce the number of dependencies and make the dependencies that remain as simple and obvious as possible.
Excessive complexity is extremely unhealthy for people and companies.
Maintainability is the most important quality of any modern software, and it may be measure as "the time required for me to understand your code" before I can make any modifications.
1. Make it work, 2. Make it right, 3. Make it fast, 4. Make it cheap. (Kent Beck)
- System must be obvious.
- Before a solution can be found, the source of the problem has to be identified.
- Always understand one level below your normal abstraction layer.
- Evolve your program toward the problem, not the other way around.
- You Ain’t ’Gonna Need It (YAGNI) - any behavior we add defensively is potentially wasted effort.
- Using design patterns doesn't automatically improve a system; it only does so if the design patterns fit. As with many ideas in software design, the notion that design patterns are good doesn't necessarily mean that more design patterns are better.
- Of all the aspects of a software system, maintenance is the most costly. [a]
- Maintenance is not a discrete activity, but a routine part of the entire development process. [b]
+------------------+-------+---------+--------+----------+
| Complexity Class | Known | Unknown | Knowns | Unknowns |
+------------------+-------+---------+--------+----------+
| Simple | ✓ | x | ✓ | x |
| Complicated | ✓ | x | ✓ | ✓ |
| Complex | x | ✓ | ✓ | x |
| Chaotic | x | ✓ | x | ✓ |
+------------------+-------+---------+--------+----------+
There are known knowns; there are things we know we know. We also know there are known unknowns; that is to say, we know there are some things we do not know. But there are also unknown unknowns — there are things we do not know we don’t know.
Simplicity is the prerequisite of building reliable and robust solutions:
- The more complex a solution becomes, the harder it becomes to understand.
- The harder a solution becomes to understand, the more likely are undetected bugs and other unexpected behavior that reduce reliability and robustness.
Complex vs. Complicated
Being complex is different from being complicated.
Things that are complicated may have many parts, but those parts are joined, one to the next, in relatively simple ways.
Complexity occurs when the number of interactions between components increases dramatically.
The density of interactions means that even a relatively small number of elements can quickly defy prediction.
Because of these dense interactions, complex systems exhibit nonlinear change.
A monolith is complicated, distributed systems are inherently complex.
Coupling is defined by the degree of dependencies.
- High level of coupling leads to
- cascade changes,
- expensive changes,
- difficult separation of work,
- limited reusability.
- Not all coupling can be seen in code. There are
- Data coupling,
- Dynamic coupling,
- Logical coupling,
- Semantic coupling,
- Temporal coupling, etc.
Cohesion is defined by the degree to which the elements inside a module belong together.
- There are no good tools to measure the cohesion degree precisely.
- Plenty of cohesion criteria:
- Distribution, hardware,
- Technology,
- Data, middleware,
- Access,
- Responsibility,
- Geography, etc.
High Cohesion by business responsibility often leads to loose coupling.
Separation of concerns is really a specific take on modularity and cohesion. [c]
Reuse is a dangerous word because it describes components that were designed to be widely used but aren't. (Gregor Hohpe)
Reusability
Reusability means tight coupling.
- If the reused part fails, the reusing part also inevitably fails.
The value of reuse in distributed systems unfolds inside a process boundary, not across service boundaries.
- Inside a process boundary, reusability still leads to very tight coupling, but it does not harm you: if the reused part fails due to a process crash, the reusing part is also dead because it also lives in the just crashed process. Thus, from a logical point of view, it cannot happen that the reused part fails in a non-deterministic way from the reusing part’s perspective.
An important property of reuse is that the reusing part does not work without the reused asset. The reused functionality is an integral part of the solution that (re-)uses it.
You should not aim for reusability in the design of distributed systems like, e.g., going for reusable services. Reusability compromises loose functional coupling which is the prerequisite for robust, highly available and fast service designs.
- Aiming for reusable microservices means deliberately crippling availability.
Maintainability is more important than reusability. [a]
Usability on the other hand is based on collaboration. The usable asset is not part of the solution you create to address a given problem.
If the used asset does not work, your solution can still work – if you designed the collaboration correctly. An important aspect of a usability dependency between two assets is their functional independence. They fulfill independent tasks. It is not that one is needed for the other to fulfill its task. That would be reuse.
E.g.: An accounting service is a usable asset, not a reusable one. Another service, e.g., a checkout service of an e-commerce solution, could complete its task without the accounting service.
- To write clean code, you must first write dirty code and then clean it (LeBlanc’s law: later equals never).
- Most managers want the truth, even when they don’t act like it. Most managers want good code, even when they are obsessing about the schedule. So too it is unprofessional for programmers to bend to the will of managers who don’t understand the risks of making messes.
- You will not make the deadline by making a mess. Indeed, the mess will slow you down instantly and will force you to miss the deadline. The only way to make the deadline—the only way to go fast—is to keep the code as clean as possible at all times.
- Code, without tests, is not clean.
- Don't use the same code (a method) if it doesn't belong to the same use case (it's better to violate DRY here).
- DRY is about the duplication of knowledge, of intent. It's about expressing the same thing in two different places, possibly in two totally different ways. [b]
- Ideally, when you have finished with each change, the system will have the structure it would have had if you had designed it from the start with that change in mind.
- Duplication of logic suggests that there are concepts hidden in the code that are not yet visible because they haven't been isolated and named.
- Abstraction tells you where your code relies upon an idea.
Tactical programming focuses on getting something working, such a new feature or a bug fix. At first glance, this seems totally reasonable, but it's short-sighted.
- Each tactical programming task contributes a few small incremental complexities.
Tactical tornado is a prolific programmer who pumps out code far faster than others but works in a totally tactical fashion. Often treated as a hero by management, however, leaves behind a wake of destruction.
The problem with test-driven development is that it focuses attention on getting specific feature working, rather than finding the best design. This is tactical programming pure and simple, with all of its disadvantages.
Strategic programming does not accept unnecessary complexities in order to finish the current task faster.
- Working code isn't enough.
- Primary goal is to produce a great design, which also happens to work.
- Continuous small improvements to the system design.
- There will inevitably be mistakes in an up-front design (waterfall).
- Ideal design tends to emerge in bits and pieces, as you get experience with the system.
- The name of a variable, function, or class, should tell you why it exists, what it does, and how it is used.
- Name interfaces and methods not after what they do, but after what they mean, what they represent in the context of your domain.
- Classes can be named by concrete implementation.
- Say what you mean. Mean what you say.
- Single-letter names can ONLY be used as local variables inside short methods. The length of a name should correspond to the size of its scope.
- It is a bad idea to prefix every class.
- When constructors are overloaded, use static factory methods with names that describe the arguments.
- When using rather short (like three lines) functions, single-letter variable names are a perfect choice.
- The greater the distance between a name's declaration and its uses, the longer the name should be.
- Using descriptive variable names is a patch, not a cure. They do make code more readable, but they don't solve the root cause of the problem - its high complexity.
- Instead of hiding the complexity issue, we need to make it more visible - we simply need to prohibit long variable names.
- Code must be descriptive enough by itself. Each scope of visibility (method, class, script) must only have as many variables as it can have without making the names longer than single or plural nouns. When it becomes necessary to make names longer, the scope has to be broken down into smaller ones.
- So, don't use any compound names anywhere in the code. If you can't explain your code using just single and plural nouns, refactor the code.
- Names should neither be too general nor too specific. For example,
thing
is too broad, anditOrOne
too narrow.
If it's hard to find a simple name for a variable or method that creates a clear image of the underlying object, that's a hint that the underlying object may not have a clean design.
- Resist mirroring implementation details in a function name
- Resist describing current implementation details in a function name
- Name functions one level of abstraction higher than their implementation
- Choose names from your application lexicon
- Functions should do one thing. Do it well and do it only and completely.
- Functions should have a clean and simple interface.
- Functions should be small.
- Any function with more than one statement is a candidate for refactoring. Ideally, all functions must have just a single statement, and that statement must be
return
. - Statements within the function must be all at the same level of abstraction.
- Stepdown Rule - descending one level of abstraction at a time as the list of functions reads itself down.
- Three arguments (triadic) should be avoided where possible. More than three (polyadic) requires very special justification—and then shouldn’t be used anyway.
- Flag arguments are ugly (it does one thing if the flag is true and another if the flag is false).
- When a function seems to need more than two or three arguments, it is likely that some of those arguments ought to be wrapped into a class of their own.
- Having multiple methods that take the same argument is a code smell, however, it's important to recognize that here "same" means same concept not identical name.
- A method should take as a parameter exactly the same format (datatype) as it uses.
- Command Query Separation - functions should either do something or answer something, but not both.
- Returning error codes from command functions is a subtle violation of command query separation - it is better to extract the bodies of the
try
andcatch
blocks out into functions of their own. Functions should do one thing. Error handing is one thing. - Keeping functions small, then the occasional multiple
return
,break
, orcontinue
statement does no harm and can sometimes even be more expressive than the single-entry, single-exit rule. - Configuration parameters result in an incomplete solution, which adds to system complexity
- Compute reasonable defaults automatically, so uses will only need to provide values under exceptional conditions.
- Ideally, each module should solve a problem completely.
- It's more important for a module to have a simple interface than a simple implementation.
- Procedural code makes it hard to add new data structures because all the functions must change. OO code makes it hard to add new functions because all the classes must change.
- Single Responsibility Principle (SRP) - class or module should have one, and only one, reason to change.
- Open-Closed Principle (OCP) - class should be open for extension but closed for modification.
- Incorporate new features by extending the system, not by making modifications to existing code.
- Don't use generic containers (such as
Pair<T>
) - generic containers result in nonobvious code because the grouped elements have generic names that obscure their meaning.- Generic containers are expedient for the person writing the code, but they create confusion for all the readers.
- Define a new class or structure that is specialized for the particular use.
- Subtyping also known as substitutability.
While subtyping is a very good thing in object-oriented programming, implementation inheritance just makes code messy and difficult to maintain. We must not reuse code by copying it from other places. We must not treat our objects as though they are made of pieces. They are solid, and we can't modify them by inheritance. We cannot create new objects by taking or replacing pieces of existing ones. Our objects must either be alive and solid, or dead and made of pieces. - Don't kill your objects, don't use implementation inheritance. - Use composition via encapsulation instead.
A module should not know about the innards of the objects it manipulates.
A method f of a class C should only call the methods of these:
- C
- An object created by f
- An object passed as an argument to f
- An object held in an instance variable of
The method should not invoke methods on objects that are returned by any of the allowed functions. In other words, talk to friends, not to strangers.
Getters are not methods that create objects as described in the Law. They do exactly the same thing as direct access to object attributes through the dot operator. They don't create objects, they just return them, and so they are against the Law.
- Data structures with no behavior, then they naturally expose their internal structure, and so Demeter does not apply (eg.
String outputDir = ctxt.options.scratchDir.absolutePath;
). - not OOP at all!
- Events (sender doesn't expect anything particular to happen) vs Commands (sender expects something particular to happen) - naming semantic is different here and it's important to be consistent in the system.
- Tell, Don't Ask - as the caller, you should not be making decisions based on the state of the called object that result in you then changing the state of the object. The logic you are implementing is probably the called object’s responsibility, not yours. For you to make decisions outside the object violates its encapsulation.
It's tempting to use exceptions to avoid dealing with difficult situations: rather than figuring out a clean way to handle it, just throw an exception and punt the problem to the caller.
- This just passes the problem to someone else and adds to the system's complexity.
Exceptions thrown by a class are part of its interface; classes with lots of exceptions have complex interfaces, and they are shallower than classes with fewer exceptions.
- Reduce the number of places where exceptions have to be handled.
-
Example: Rather than throwing an exception when
unset
is asked to delete an unknown variable, it should have simply returned without doing anything.
The error-ful approach may catch some bugs, but it also increases complexity, which results in other bugs.
- Define errors out of existence. This simplifies APIs and reduces the amount of code that must be written.
- Overall, the best way to reduce bugs is to make software simpler.
With exceptions, as with many other areas in software design, you must determine what is important and what is not important. Things that are not important should be hidden, and the more of them the better. But when something is important, it must be exposed.
- Use
RuntimeException
only. - Checked exceptions violate the Open/Closed Principle (encapsulation is broken because all functions in the path of a throw must know about details of that low-level exception).
- Create domain-specific exceptions to give a hint to a user of the service.
- Wrap third-party APIs.
- Design of a concurrent algorithm can be remarkably different from the design of a single-threaded system. The decoupling of what from when usually has a huge effect on the structure of the system.
- Keep your concurrency-related code separate from other code.
- Comments should explain why, not what. They can optionally explain how if what’s written is particularly confusing.
- Comments should describe things that aren't obvious from the code, such as low-level details, why code is needed, or why it was implemented in a particular way.
- Don’t comment bad code - rewrite it.
- It is just plain silly to have a rule that says that every function must have a javadoc, or every variable must have a comment.
- Write for your audience (beginners, intermediate and expers) in sections:
- start with a simple overview,
- then get into details,
- and low-level specifics.
- Write to help.
- Be minimalistic.
- Use no more words than necessary.
- Use present tense.
- Prefer shorter words.
- Be direct, use "you".
- Use Use-Cases.
- Review your documentation.
When a child object is a parent object (#1). Parent object methods are either properly overridden or left alone (#2). There is more than one child object that extends parent object and parent object interface is used (#3). The problem is that all these conditions are not met all that often. And that is the reason that you should favor composition over inheritance.
Imperative style technically can't be combined with declarative. Once you start using imperative, you're doomed to stay with it, and eventually your entire codebase will become imperative.
Good code hides the algorithm behind it, while bad code makes it visible. (Some guy)
- Robert C. Martin: Clean Code
- Robert C. Martin: Clean Architecture [a]
- Thomas, Hunt: The Pragmatic Programmer [b]
- David Farley: Modern Software Engineering [c]
- Neal Ford: Functional Thinking
- https://pragprog.com/articles/tell-dont-ask
- Jez Humble: Continuous Delivery
- Yegor Bugayenko: Elegant Objects
- John Ousterhout: A Philosophy of Software Design
- Sandi Metz: 99 Bottles of OOP
- Decomposition of responsibility
- Design Patterns and Anti-Patterns
- Dependency Injection is Loose Coupling
- Wrong Abstraction
- DRY is about knowledge
- Impl classes are evil
- What's an interface
- Fallacy of Reuse
- Where to use Inheritance
- Simple vs. Complicated vs. Complex vs. Chaotic
- Composition and decomposition (Tweet)
- The broken promise of re-use
- Reusability Fallacy
- Falsehoods Programmers Believe About Names
- CUPID properties of code for humans
- https://quickref.dev