Clean Code, Refactoring and Test-Driven Development
I would like to summarize some of the basic principles and habits of coding that I learned while working at ThoughtWorks. These are some 'metaphysical' guidelines, and later I plan to use Kotlin to write some design patterns to practice the 'physical' aspects.
In a context where GPT can help us write most of the code, these principles become even more important.
私はThoughtWorksでの勤務中に学んだコーディングの基本原則と習慣をまとめたいと思います。これらはいくつかの「形而上」のガイドラインで、後でKotlinを使用してデザインパターンを書き、その「形而下」の部分を実践する予定です。
GPTがほとんどのコードを書く手助けをする状況では、これらの原則はさらに重要になります。
Clean Code, Refactoring and Test-Driven Development
Reference: - Refactoring: Improving the Design of Existing Code - Clean Code: A Handbook of Agile Software Craftsmanship
## Clean Code
The problem that Clean Code want to solve is raised
Any fool can write code that a machine can understand. GPT is better at this field that you. Good programmers write code that humans can understand.
Code is primarily written for people to read, and only incidentally for machines to execute.
The time spent reading code far exceeds the time spent writing code by a factor of 10x.
Good design is obvious with no issues, while bad design has no obvious issues.
So the things that we values should be - Readability! - Maintainability!
Code should be a Problem Solver, not a *Trouble Maker** .
A Clean Code should be a code that achieve the beforehand mentioned functions and values.
My basic rules of Clean Code are
- 1-10-50 Rule (exceptions allowed in rare cases)
- Each method should not have more than one level of indentation.
- Exceptions for try-catch and JavaScript callbacks.
- Each method should not exceed 10 lines.
- Excluding braces and the name itself.
- Exceptions for try-catch and fetching APIs.
- Do not force multiple lines into a single line.
- Each class should not exceed 50 lines.
- Import statements do not count.
- Each method should not have more than one level of indentation.
- Reasonable naming: variables, constants, methods, classes, enum values, files, etc.
- Formatting
- Variable
- Variable declarations should be as close as possible to their point of use
- Local variables should appear at the top of the function
- Variable declarations within loops should always occur inside the loop
- Entity variables should be declared at the top of the class.
- Method order: if one function calls another, should be placed together, and the caller should be placed above the callee
- Variable
- "No" Comments.
- "No" Else.
- Favor the Return Early pattern.
Refactoring
Refactoring is the way to achieve clean code. ### Code Smell Reference :Code Smells
- Duplicate Code
- Long Method
- Large Class
- Long Parameter List
- Primitive Obsession
- Data Clumps
- Switch Statements
- Feature Envy
- Comments
example for 5-9 using Kotlin
1 | // Primitive Obsession |
1 | // Data Clumps |
1 | // Switch Statements |
1 | // Feature Envy |
1 | // Bad practice with comments |
Refactoring Techniques
Reference : Refactoring Techniques 1. Extract Variable 2. Inline Temp 3. Extract Method 4. Inline Method
When is refactoring needed?
- Code Review: Detect bad smells during code reviews and politely suggest improvements.
- Every Commit: Each commit you make should leave the code cleaner than it was before.
- When Taking Over a Difficult-to-Read Project: Convince the project team to treat refactoring as a necessary task.
- When Iteration Efficiency Is Below Expectations: Treat refactoring as a specific task, and if necessary, pause to iterate on requirements.
Rules of Refactoring
Use keyboard shortcuts.
Refactoring should not break the functionality of the code; it should always be able to compile and run.
Avoid starting to write new code while in the middle of refactoring.
Test-Driven-Development
To ensure that refactoring does not break the functionality of the code, we require a reliable method to verify its integrity. This is where Test-Driven Development (TDD) proves invaluable. By integrating TDD, we establish a safety net of tests that confirm the code continues to perform as expected throughout the refactoring process.
The 3 Laws of TDD
- You are not allowed to write any production code unless it is to make a failed pass
- You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures
- You are not allowed to write any more production code than is sufficient to pass on failing unit test .
Test Double and Test Structure
1 | interface CreditCardProcessor { |
1 | import io.mockk.every |
Stub: In the test above,
mockProcessor
is used as a stub. It is configured to return a specific response (true
in this case) when itschargeCard
method is called. Stubs are used to provide predetermined responses to method calls during tests.Mock:
mockTransactionLog
is used as a mock. While it could also be seen as a stub because it is providing predefined behavior (due torelaxed = true
), the key aspect here is that we're verifying its behavior post-factum. We are checking whether thelogTransaction
method was called with the correct argument ("Success"). This is typical mocking behavior, where the emphasis is on verifying that certain methods are called correctly.More detailed ( Reference: Mocks Aren't Stubs)
Dummy objects are passed around but never actually used. Usually they are just used to fill parameter lists.
Fake objects actually have working implementations, but usually take some shortcut which makes them not suitable for production (an in memory database is a good example).
Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.
Spies are stubs that also record some information based on how they were called. One form of this might be an email service that records how many messages it was sent.
Mocks are what we are talking about here: objects pre-programmed with expectations which form a specification of the calls they are expected to receive. ### Test Pyramid