Skip to content

Instantly share code, notes, and snippets.

@Inclushe
Created July 8, 2019 08:16
Show Gist options
  • Save Inclushe/75b1d8181658159cb5d0a18ca74aeffe to your computer and use it in GitHub Desktop.
Save Inclushe/75b1d8181658159cb5d0a18ca74aeffe to your computer and use it in GitHub Desktop.

Clean Code Notes

Intro Notes

  • 5S Principles
    • organization - seiri (整理)
      • Use suitable naming
    • tidiness - seiton (整頓)
      • A place for everything, and everything in its place.
    • cleaning - seisō (清掃)
      • Keep the workplace free of hanging wires, grease, scraps, and waste.
    • standardization - seiketsu (清潔)
      • Consistent coding
    • discipline - shitsuke (躾)
      • Having the discipline to follow the practices and to frequently reflect on one's work and be willing to change.
  • Refactor mercilessly.
  • Build machines that are more maintainable in the first place.
  • Bad code tempts the mess to grow. When others change bad code, they tend to make it worse.
  • One broken window starts the process toward decay.
  • "Clean code is simple and direct. Clean code reads like well-written prose. Clean code never obscures the designer’s intent but rather is full of crisp abstractions and straightforward lines of control." - Grady Booch
  • "It has minimal dependencies, which are explicitly defined, and provides a clear and minimal API." - Dave Thomas
  • If it hath no tests, it be unclean.
  • Beck's rules of simple code
    • Runs all the tests
    • Contains no duplication;
    • Expresses all the design ideas that are in the system;
    • Minimizes the number of entities such as classes, methods, functions, and the like.
  • If an object or method does more than one thing, split it up.
  • Clean code won't surprise you.
  • Making code easy to read makes it easier to write.
  • "Leave the campground cleaner than you found it."

Names

  • The name of a variable, function, or class, should answer all the big questions.
    • It should tell you why it exists, what it does, and how it is used.
    • If a name requires a comment, then the name does not reveal its intent.
  • The length of a name should correspond to the size of its scope
    • Temporary, one-letter variables (ex. for loop) are OK if they are used immediately after declaration.
  • Clarity is king. Write code that others can understand.
    • Don't get smart.
    • Choose clarity over entertainment.
  • Classes and objects should have noun or noun phrase names like Customer, WikiPage, Account, and AddressParser. A class name should not be a verb.
  • Methods should have verb or verb phrase names like postPayment, deletePage, or save. Accessors, mutators, and predicates should be named for their value and prefixed with get, set, and is.
  • Pick one word for one abstract concept and stick with it.
  • Code should be as easy as possible to understand. We want our code to be a quick skim, not an intense study.
  • Use Solution/Problem Domain Names
  • Shorter names are generally better than longer ones.
    • Add no more context to a name than is necessary.

Functions

  • Functions should be small, and that should be smaller than that.
  • Small functions cannot hold nested structures (indent level 1-2 max)
  • "Functions should do one thing. They should do it well. They should do it only."
  • Functions that do one thing cannot be reasonably divided into sections.
  • One level of abstraction per function.
    • Mixing levels of abstraction within a function is always confusing.
  • We want the code to read like a top-down narrative.
    • We want every function to be followed by those at the next level of abstraction so that we can read the program, descending one level of abstraction at a time as we read down the list of functions.
  • Use Descriptive Names
    • The smaller and more focused a function is, the easier it is to choose a descriptive name.
    • Don't be afraid to make a name long.
    • Don't be afraid to spend time choosing a name.
  • The ideal number of arguments for a function is zero.
    • No more than two should be used unless justified.
  • Common forms of monodic functions
    • Ask a question about the argument
    • Operate on an argument, transform it, and return it
    • Events
    • Avoid other forms
  • writeField(name) is easier to read than writeField(outputStream, name)
  • Don't have side effects (do hidden things)
  • Anything that forces you to check the function signature is equivalent to a double-take. It’s a cognitive break and should be avoided.
  • Functions should either do something or answer something, but not both.
  • Extract try-catch and switch blocks into separate functions
    • They confuse the structure of the code
  • Functions should do one thing. Error handing is one thing. Thus, a function that handles errors should do nothing else.
  • Don't repeat yourself.
  • Code is never clean the first time around, refactoring it makes it cleaner.
  • Never forget that your real goal is to tell the story of the system.

Comments

  • Don't comment bad code--rewrite it.
  • Comments are almost always failures.
  • Inaccurate comments are far worse than no comments at all.
  • Explain yourself in code.
    • In many cases, it's simply a matter of creating a function that says the same thing as the comment you want to write.
  • Valid reason to write comments
    • Explain intent behind a decision
    • Warn other programmers about certain consequences.
    • To do comments
    • Amplify the importance of something
  • In documentation, it's silly to give every variable a description
    • Small functions probably don't need documentation if the name is descriptive.
  • Don't use a comment when you can use a function or variable.
  • Comments should be near the code it describes.
  • Don't put irrelevant or not obvious information in a comment.

Formatting

  • Small files are usually easier to understand than large files are.
  • We would like a source file to be like a newspaper article.
    • The name should be simple but explanatory.
    • The topmost parts of the source file should provide the high-level concepts and algorithms.
    • Detail should increase as we move downward, until at the end we find the lowest level functions and details in the source file.
  • Groups of lines that represent thoughts should be separated by blank lines.
  • Lines of code that are tightly related should appear vertically dense.
  • Variables should be declared as close to their usage as possible.
  • Instance variables should be declared at the top of the class.
  • If one function calls another, they should be vertically close, and the caller should be above the callee, if at all possible. This gives the program a natural flow.
  • Bits of code with stronger conceptual affinity should be vertically close.

Objects and Data Structures

  • Express data in abstract terms.
  • Objects expose behavior and hide data, making it hard to add new behaviors to existing objects.
  • Data structures expose data and have no significant behavior, making it hard to add new data structures to existing functions.
  • Avoid hybrids.

Error Handling

  • Error handling is important, but if it obscures logic, it's wrong.
  • Use exceptions rather than return codes.
  • Try starting with a try-catch-finally statement when writing code that could throw exceptions.
  • Try to write tests that force exceptions, and then add behavior to your handler to satisfy your tests.
  • Create informative error messages and pass them along with your exceptions. Mention the operation that failed and the type of failure.
  • Try wrapping third-party APIs in a wrapper, minimizing dependencies upon it.
  • Create special case patterns that handles special cases/exceptional behavior.
  • Don't return or pass null.

Boundaries

  • Write learning tests to explore our understanding of third-party code.
  • When we use code that is out of our control, special care must be taken to protect our investment and make sure future change is not too costly.

Unit Tests

  • Write unit tests first before you write production code.
  • The Three Laws of TDD
    1. You may not write production code until you have written a failing unit test.
    2. You may not write more of a unit test than is sufficient to fail, and not compiling is failing.
    3. You may not write more production code than is sufficient to pass the currently failing test.
  • The tests and the production code are written together, with the tests just a few seconds ahead of the production code.
  • Having dirty tests is equivalent to, if not worse than, have no tests.
  • Test code is just as important as production code.
  • The higher your test coverage, the less your fear.
  • Readability is even more important in unit tests.
  • Use the Build-Operate-Check pattern to write tests.
  • Make one assert per test.
  • Test a single concept per test.
  • FIRST
    • Fast
      • Tests should be fast. They should run quickly.
    • Independent
      • Tests should not depend on each other. One test should not set up the condition for the next test.
    • Repeatable
      • Tests should be repeatable in any environment.
    • Self-Validating
      • The tests should have a boolean output. Either they pass or fail.
    • Timely
      • The tests need to be written in a timely fashion. Unit tests should be written just before the production code that makes them pass.

Classes

  • Loosening encapsulation is always a last resort.
  • Classes should be small!
  • Functions are measured by lines, classes are measured by responsibilities.
    • Ambiguous class names are a sign of too many responsibilities.
  • The Single Responsibility Principle states that a class or module should have one, and only one, reason to change.
    • Maintaining a separation of concerns is important in our activities and programs.
    • Do you want your tools organized into toolboxes with many small drawers each containing well-defined and well-labeled components? Or do you want a few drawers that you just toss everything into?
    • We want our systems to be composed of many small classes, not a few large ones. Each small class encapsulates a single responsibility, has a single reason to change, and collaborates with a few others to achieve the desired system behaviors.
  • Classes should have a small number of instance variables.
    • The more variables a method manipulates, the more cohesive that method is to its class.
    • We would like cohesion to be high.
      • When cohesion is high, it means that the methods and variables of the class are co-dependent and hang together as a logical whole.
      • Try to separate the variables and methods into two or more classes such that the new classes are more cohesive.
        • When classes lose cohesion, split them.
  • Isolate from change.
  • Decoupled systems are more flexible, testable, and reusable.
  • Classes should depend upon abstractions, not on concrete details.

Systems

  • Cities have evolved appropriate levels of abstraction and modularity.
  • Construction is a very different process from use.
  • Having construction and runtime processing mixed together violates SRP.
  • Modularize them separately.
  • Try moving construction to main or main modules, and design assuming it has been constructed.
  • Inversion of Control moves secondary responsibilities from an object to other objects that are dedicated to the purpose, thereby supporting the Single Responsibility Principle.
  • It is a myth that we can get systems right the first time. Instead, we should implement only today's stories, then refactor and expand the system to implement new stories tomorrow.
  • It is not necessary to do a Big Design Up Front, it may inhibit adapting to change.
  • Software projects can be started "naively simple" by nicely decoupling architecture, delivering work stories quickly, then adding more infrastructure as we scale up.
  • Efficient and robust systems use minimally coupled designs that are simple at each level of abstraction and scope.
  • An optimal system architecture consists of modularized domains of concern.
  • No one person can make all the decisions.
  • Invasive architectures overwhelm the domain logic and impacts agility.
  • At all levels of abstraction, the intent should be clear.
  • Never forget to use the simplest thing that can possibly work.

Emergence

  • A design is simple if it (in order of importance)
    • Runs all the tests
    • Contains no duplication
    • Expresses the intent of the programmer
    • Minimizes the number of classes and methods.
  • Making our systems testable pushes us toward a design where our classes are small and single purpose.
  • Tight coupling makes it difficult to write tests.
  • The fact that we have tests eliminates the fear that cleaning up the code will break it.
  • Understanding how to achieve reuse in the small is essential to achieving reuse in the large.
  • Code should clearly express the intent of its author.
  • The clearer the author can make the code, the less time others will have to spend understanding it.
  • Care is a precious resource.

Concurrency

  • Keep your concurrency-related code separate from other code.
  • Restrict the number of critical sections.
  • Severely limit the access of any data that may be shared.
  • Avoid sharing data if at all possible.
  • Consider writing your threaded code such that each thread exists in its own world, sharing no data with any other thread.
  • Review the classes available to you.
  • Learn appropriate algorithms.
  • Avoid using more than one method on a shared object.
  • Think about shut-down early and get it working early. It's going to take longer than you expect.
  • Write tests that have the potential to expose problems and then run them frequently.
  • Do not ignore system failures.
  • Get your nonthreaded code working first.
  • Encourage task swapping by running with more threads than processors or cores.
  • Multithreaded code behaves differently in different environments.
  • Instrument your code to try and force failures.
  • Use jiggling strategies to ferret out errors. (calls to wait, sleep, yield, and priority)
  • Concurrent code is difficult to get right.
  • Thread-aware code should be small and focused

Concluding Notes

  • The author doesn't write clean code from beginning to end in one sweep.
  • The author doesn't expect you to do it either.
  • Write dirty code and then make it clean.
  • When refactoring, you are not allowed to make a change to the system that breaks the tests.
  • Much of good software design is simply about partitioning--creating appropriate places to put different kinds of code.
  • It is not enough for code to work.
  • Code that works is often badly broken.
  • Programmers who satisfy themselves with merely working code are behaving unprofessionally.
  • Nothing has a more profound and long-term degrading effect upon a development project than bad code.
  • Continuously keep your code as clean and simple as it can be. Never let the rot get started.
  • Critiquing other's code is professional, not feeding a superiority complex.
  • You should be able to run all the unit tests with just one command.
  • Raw numbers should instead be well-named constants unless unjustified.
  • Avoid negative conditionals.
  • Separating levels of abstraction is one of the most important functions of refactoring, and it's one the hardest to do well.
  • Names are too important to treat carelessly.
    • Choose names that reflect the level of abstraction of the class.
  • A test suite should test everything that could possibly break.
  • Use a coverage tool to report gaps in your testing strategy.
  • Give boundary conditions special care.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment