Clean Code V2
Clean Code V2
Code is clean if it can be understood easily by everyone on the team. With understandability comes readability, changeability, extensibility and maintainability. All the things needed to keep a project going over a long time without accumulating up a large amount of technical debt. optimal Responsiveness
Smells Rigidity
The software is difficult to change. A small change causes a cascade of subsequent changes.
Fragility
The software breaks in many places due to a single change.
Over Configurability
Prevent configuration just for the sake of it or because nobody can decide how it should be. Otherwise, this will result in overly complex, unstable systems.
high
high
Immobility
You cannot reuse parts of the code in other projects because of involved risks and high effort.
Responsiveness to change
Micro Layers
Do not add functionality on top, but simplify overall.
actual
Responsiveness
CoC
Viscosity of Design
Taking a shortcut and introducing technical debt requires less effort than doing it right.
Viscosity of Environment
Building, testing and other tasks take a long time. Therefore, these activities are not executed properly by everyone and technical debt is introduced.
Technical Debt
Needless Complexity
The design contains elements that are currently not useful. The added complexity makes the code harder to comprehend. Therefore, extending and changing the code results in higher effort than necessary.
low
low
Needless Repetition
Code contains lots of code duplication: exact code duplications or design duplicates (doing the same thing in a different way). Making a change to a duplicated piece of code is more expensive and more error-prone because the change has to be made in several places with the risk that one place is not changed accordingly.
Writing clean code from the start in a project is an investment in keeping the cost of change as constant as possible throughout the lifecycle of a software product. Therefore, the initial cost of change is a bit higher when writing clean code (grey line) than quick and dirty programming (black line), but is paid back quite soon. Especially if you keep in mind that most of the cost has to be paid during maintenance of the software. Unclean code results in technical debt that increases over time if not refactored into clean code. There are other reasons leading to Technical Debt such as bad processes and lack of documentation, but unclean code is a major driver. As a result, your ability to respond to changes is reduced (red line).
Continuous Integration
Assure integrity with Continuous Integration
Feature Envy
The methods of a class should be interested in the variables and functions of the class they belong to, and not the variables and functions of other classes. When a method uses accessors and mutators of some other object to manipulate the data within that object, then it envies the scope of the class of that other object. It wishes that it were inside that other class so that it could have direct access to the variables it is manipulating.
Overridden Safeties
Do not override warnings, errors, exception handling they will catch you.
Opacity
The code is hard to understand. Therefore, any change takes additional time to first reengineer the code and is more likely to result in defects due to not understanding the side effects.
Artificial Coupling
Things that dont depend upon each other should not be artificially coupled.
Most software defects are introduced when changing existing code. The reason behind this is that the developer changing the code cannot fully grasp the effects of the changes made. Clean code minimises the risk of introducing defects by making the code as easy to understand as possible.
Transitive Navigation
Aka Law of Demeter, writing shy code. A module should know only its direct dependencies.
Dont Be Arbitrary
Have a reason for the way you structure your code, and make sure that reason is communicated by the structure of the code. If a structure appears arbitrary, others will feel empowered to change it.
Be Precise
When you make a decision in your code, make sure you make it precisely. Know why you have made it and how you will deal with any exceptions.
High Cohesion
Cohesion is the degree to which elements of a whole belong together. Methods and fields in a single class and classes of a component should have high cohesion. High cohesion in classes and components results in simpler, more easily understandable code structure and design.
Change is Local
When a software system has to be maintained, extended and changed for a long time, keeping change local reduces involved costs and risks. Keeping change local means that there are boundaries in the design which changes do not cross.
It is Easy to Remove
We normally build software by adding, extending or changing features. However, removing elements is important so that the overall design can be kept as simple as possible. When a block gets too complicated, it has to be removed and replaced with one or more simpler blocks.
Symmetry / Analogy
Favour symmetric designs (e.g. Load Save) and designs that follow analogies (e.g. same design as found in .NET framework).
Misplaced Responsibility
Something put in the wrong place.
Encodings in Names
No prefixes, no type/scope information
Understandability Consistency
If you do something a certain way, do all similar things in the same way: same variable name for same concepts, same naming pattern for corresponding concepts.
Positive Conditionals
Positive conditionals are easier to read than negative conditionals.
1) Identify Features
Identify the existing features in your code and prioritise them according to how relevant they are for future development (likelihood and risk of change).
Commit Reviews
A developer walks a peer developer through all code changes prior to committing (or pushing) the changes to the version control system. The peer developer checks the code against clean code guidelines and design guidelines.
Clutter
Code that is not dead but does not add any functionality
Inappropriate Information
Comment holding information better held in a different kind of system: product backlog, source control. Use code comments for technical notes only.
Coding Dojo
In a Coding Dojo, a group of developers come together to exercise their skills. Two developers solve a problem (kata) in pair programming. The rest observe. After 10 minutes, the group rotates to build a new pair. The observers may critique the current solution, but only when all tests are green.
4) Identify Components
Within a feature, identify the components used to provide the feature. Prioritise components according to relevance for future development (likelihood and risk of change).
Obscured Intent
Too dense algorithms that lose all expressiveness.
Bibliography
Clean Code: A Handbook of Agile Software Craftsmanship by Robert Martin
Fail Fast
Exceptions should be thrown as early as possible after detecting an exceptional case. This helps to pinpoint the exact location of the problem by looking at the stack trace of the exception.
Isolate Change
First, isolate the code to be refactored from the rest. Then refactor. Finally, undo isolation.
Migrate Data
Move from one representation to another by temporary duplication of data structures.
Inappropriate Static
Static method that should be an instance method
Swallowing Exceptions
Exceptions can be swallowed only if the exceptional case is completely resolved after leaving the catch block. Otherwise, the system is left in an inconsistent state.
Nesting
Nested code should be more specific or handle less probable scenarios than unnested code.
Legend:
DO DONT
Faking Framework
Use a dynamic fake framework for fakes that show different behaviour in different test scenarios (little behaviour reuse).
Obsolete Test
A test that checks something no longer required in the system. May even prevent clean-up of production code because it is still referenced.
Partial Test
Write a test that does not fully check the required behaviour, but brings you a step closer to it. Then use Extend Test below.
Extend Test
Extend an existing test to better match real-world scenarios.
Bloated Construction
The construction of dependencies and arguments used in calls to testee makes test hardly readable. Extract to helper methods that can be reused.
Another Test
If you think of new tests, then write them on the TO DO list and dont lose focus on current test.
Learning Test
Write tests against external components to make sure they behave as expected.
Constructor Lifetime
Pass dependencies and configuration/parameters into the constructor that have a lifetime equal to or longer than the created object. For other values use methods or properties.
Erratic Test
Sometimes passes, sometimes fails due to left overs or environment.
Obvious Implementation
If the implementation is obvious then just implement it and see if test runs. If not, then step back and just get test running and refactor then.
Isolated
Clear where the failure happened. No dependency between tests (random order).
Repeatable
No assumed initial state, nothing left behind, no dependency on external services that might be unavailable (databases, file system ).
Tiny Steps
Make tiny little steps. Add only a little code in test before writing the required production code. Then repeat. Add only one Assert per step.
Acceptance Test Driven Development Use Acceptance Tests to Drive Your TDD tests
Acceptance tests check for the required functionality. Let them guide your TDD.
Test Namespace
Put the tests in the same namespace as their associated testee.
Self-Validating
No manual test interpretation or intervention. Red or green!
Timely
Tests are written at the right time (TDD, DDT, POUTing)
Automated ATDD
Use automated Acceptance Test Driven Development for regression testing and executable specifications.
Resource Files
Test and resource are together: FooTest.cs, FooTest.resx
Checking Internals
A test that accesses internals (private/protected members) of the testee directly (Reflection). This is a refactoring killer.
Anonymous Variables
Always use the same name for variables holding uninteresting arguments to tested methods (e.g. anonymousText).
Irrelevant Information
Test contains information that is not relevant to understand it.
Chatty Test
A test that fills the console with text probably used once to manually check for something.
Post-Commit Check
Run all unit and acceptance tests on every commit to the version control system on the continuous integration server.
You need to build up knowledge to implement the acceptance test. Make an initial design
Build Staging
Split the complete continuous integration workflow into individual stages to reduce feedback time.
Implement a Spike to gather enough knowledge so you can design a possible solution.
Roughly design how you want to implement the new functionality, especially the interface for your acceptance test (how to call and verify functionality).
Refactor Refactor existing code to simplify introduction of new functionality. Run all tests to keep code working. Succeeded, not all acceptance tests implemented yet Write an acceptance test Add arrange, act and assert parts to the acceptance test skeleton (Given, When, Then or Establish, Because, It ). Run acceptance test Failed Make error reason obvious The failing test should state what went wrong so you dont have to debug the code.
Continuous Deployment
Install the system to a test environment on every commit or manual request. Deployment to production environment is automated, too.
Test Pyramid
ATDD
You have no class design idea You have a class design idea Spike a solution Implement a Spike to get the acceptance test running so that you get an initial design. Make initial or update class design Design how you want to implement the new functionality. Do per class Succeeded, code clean, TO DO list empty
Bibliography
Test Driven Development: By Example by Kent Beck ATDD by Example: A Practical Guide to Acceptance Test-Driven Development by Markus Grtner The Art of Unit testing by Roy Osherove xUnit Test Patterns: Refactoring Test Code by Gerard Meszaros
TO DO list Add missing test when you think of one Remove test when written We write the TO DO list into the same file as the unit test with // TODO: Pick test: 1) Prove that the code is making a hard coded assumption. 2) Prove that something is wrong. 3) Prove that something is missing. Write a test
Add a minimal test or make a minimal change to an existing test (< 10 minutes).
Run test
TDD
Make error reason obvious The failing test should state what went wrong so you dont have to debug the code. Write code
Failed
Clean up code Succeeded, code not clean Apply clean code guidelines. Redesign classes as needed. (< 10 minutes).
Legend:
DO DONT