Tips to embody clean code
Abstruct
Clean code is among significant theme not only for developers but also business itself. It possibly slows the growth of your products if you leave its code messed up. The technical debt gradually stacks like a poison and holds you back behind your competators. Legacy code could become person-dependant. The timer bomb is destine for explosion triggered by the resignation of your key developer with deep insight for it and no one can maintain the code anymore in the worst case. You can't underestimate it for avoiding the risk of X day. The snippets are written by C#. The tequniques below are general, but could include the factor C#-specific in the sample code.
Principals
- High-cohesive, Loose coupling
- Stick to "Single responsiblity" to keep 1.
- Improve codes incrementally.
Basics
Naming
type | words | case | example |
---|---|---|---|
Class | noun, singular | large camel case | Client |
Method | verb | small camel case | addClient() |
Method(boolean) | boolean prefix, state | small camel case | isValidName() |
Variable(primitive) | noun, singular | small camel case | clientName |
Variable(collection) | noun, plural | small camel case | clientNames |
Variable(constant) | noun, singular | large snake case | MAX_CLIENT_NUM |
Variable(boolean) | boolean prefix, state | small camel case | isCanceledClient() |
- boolean prefix: is,has,can,should
- Use unique, detailed and meaningful vocabulary
- Avoid generic words
- method -> verb for operation, class -> noun as purpose of method
- Separate names in diffirent concerns with detail informations
- e.g) Product -> OrderedItem, InventryItem
- Detect it when you call the noun with different adjectives to express.
- Read Terms of Service.
- It indicates proper naming and separation of conserns.
- Be Adherent to its business concerns or purposes.
- Don't stick to technical sections.
Magic numbers
Move literals into constants.
BAD:
for(int i = 0; i < collection.length; i++) {
if(client[i].age >= 60) {
...
}
}
GOOD:
const int DISCOUNT_AGE = 60;
for(int i = 0; i < collection.length; i++) {
if(client[i].age >= DISCOUNT_AGE) {
...
}
}
Not NULL
- Don't initialise / pass / return a NULL value
- Set initialised value object
Dead code
- Remove all dead codes
- DON'T comment it out
- Remove all imports you don't use
Logics
Early return / continue / break
Return earlier as much as possible to reduce nests
BAD:
if(isReserved) {
if(shouldWithGardians) {
if(client[i].price > client[j]) {
...
}
}
}
GOOD:
if(!isReserved) {
return;
}
if(shouldWithGardians) {
return;
}
if(client[i].price > client[j]) {
return;
}
...
- Especially useful for writing guard clauses(validation)
Strategy pattern
Use interface to reduce redundant if-else or switch clauses
BAD:
class Shape {
...
int calcArea() {
readonly int area;
// you'll add case when you create another Shape.
switch(type)
{
case "Rectangle":
area = width * height
case "Triangle":
area = (width * height) / 2;
default:
throw Exception();
}
return area;
}
}
GOOD:
interface Shape {
...
int calcArea();
}
public class Rectangle:Shape {
...
int calcArea() {
return width * height;
}
}
public class Triangle:Shape {
int calcArea(){
return (width * height) / 2;
}
}
static void showArea(Shape shape) {
Console.WriteLine(shape.calcArea());
}
static int main () {
Shape rectangle = new Rectangle();
showArea(rectangle);
Shape triangle = new Triangle();
showArea(triangle);
}
Policy pattern
Super useful to componentise and assemble a set of conditions.
BAD:
// has dupulicate condition
bool isTriangle(Shape shape){
if(hasClosedArea(shape)) {
if(shape.angles.Count() == 3) {
return true;
}
}
return false;
}
bool isRectangle(){
if(hasClosedArea(shape)) {
if(shape.angles.Count() == 4) {
return true;
}
}
return false;
}
GOOD:
// Create components of rules sharing common interface.
interface ShapeRule() {
boolean ok(Shape shape);
}
class ClosedAreaRule : ShapeRule {
boolean ok(Shape shape) {
return hasClosedArea(shape);
}
}
class RectangleAnglesRule : ShapeRule {
boolean ok(Shape shape) {
return shape.angles.Count() == 3;
}
}
class TrianglesAnglesRule: ShapeRule {
boolean ok(Shape shape) {
return shape.angles.Count() == 4;
}
}
class ShapePolicy() {
// Keep set of rules
private readonly rules = new HashSet<ShapeRule>();
void AddRules(ShapeRule rule) {
rules.Add(rule);
}
// Judge if it meets all conditions
boolean MeetsAllConditions(Shape shape) {
rules.All(rule => rule.ok(shape));
}
}
static int main() {
// Create rules and combine them as a policy
ShapeRule closedAreaRule = new ClosedAreaRule();
ShapeRule triangleAnglesRule = new TrianglesAnglesRule();
var trianglePolicy = ShapePolicy();
trianglePolicy.AddRules(closedAreaRule);
trianglePolicy.AddRules(triangleAnglesRule);
Shape triangle = new Triangle();
// Judge by a combined policy
var isTriangle = trianglePolicy.MeetsAllConditions(triangle);
}
High cohesive
Independent class design
- Integrate relevant variables / methods into a class
- Use INSTANCE variables / methods
- Avoid using STATIC variables / methods
- Static methods cannot access instance variable
- Use methods to modify its instance valiables
- Tell, Don't ask
- Avoid using getter / setter.
Block invalid values on instance variables
- Make sure to initialise on constructor.
- Write input validation on the first of method.
- Pass a parameter as a unique class.
- Primitive type doesn't notice when you set a invalid value
- Unique class throws compile error at that time
- Organises the data structure and reduce the arguments
- Set arguments and variables immutable as much as possible
- Reassignment can put a invalid value accidentally.
- Return new value object when you modify the instance
- Avoid using output argument
- It obfuscates where it modified the value
record Price {
// immutable instance variable
private readonly int value;
// immutable argument
Price(in int value) {
// input validation
if(value < 0) {
throw IlligalArgumentException();
}
// initialisation on constructer
this.value = value;
}
Price add(in Price addition) {
// immutable variable
readonly int addedValue = this.value + addition.value;
// return new value object when modification
return new Price(addedValue);
}
}
Factory method
Use with private constractor. Set different initial value by different factory methods. Reduce time to search the class with diffirently initicialised instances.
class MemberShip {
const int STANDERD_FEE = 50;
const int PREMIUM_FEE = 100;
private readonly int monthlyFee;
// private constractor.
private MemeberShip(in int fee) {
this.monthlyFee = fee;
}
// factory methods are static
// you only need to investigate this class to modify either of fee
static createStandardMemberShip() {
return new MemberShip(STANDERD_FEE);
}
static createStandardMemberShip() {
return new MemberShip(PREMIUM_FEE);
}
}
First class collection
Collections tends to be scattered and low cohesive.
- Keep collections as private members
- Operate the collection only via methods
Trolley {
private readonly List<PurchaseItem> items;
Trolley() {
items = new List<purchaseItem>();
}
// Don't let operate the collection directly
AddItem(PurchaseItem item) {
items.Add(item)
}
// Return read only instance when you need to provide reference
readonly List<PurchaseItem> GetItems() {
return items
}
}
Utility class
- Consists of static variables and static methods
- Integrate only something unrelevant to cohesion
- crosscutting concerns e.g) logging, formatting
- Be careful not to pack unrelevant functionalities.
Loose coupling
Proper separation
- Pay attention to instance variables and separate into another classes.
- Loose coupling depends on separation of concerns at all.
Proper consolidation
- Don't believe DRY principle blindly.
- Consider if those steps are in the same single responsibility before consolidating them.
- You'll have complex conditional branches inside after all, if you integrate diffirent concerns.
Composition
- Avoid using Inheritance as much as possible.
- Sub class easily gets unexpected impacts when you modify its super class.
- Use composition instead.
Encupsulation
- Avoid using public blindly.
- it leads tight coupling.
- Consider if you can configure it as package private(internal) or private
- May use protected when you need to use Inheritance.
Refactoring
Reduce nests
- Early return/continue/break
- Strategy pattern
Proper logic cohesion
- Arrange order
- Chunk steps by purposes
- Extract them into methods
Conditional judgements
- Extract it into methods(is/has/should/can...)
- Combine multiple conditions into a combined methods
Consolidate arguments
- Consolidate multiple arguments on methods into a class
- Realise stamp coupling
Error handling
Catch
- Catch only the error you need to handle
- Catch business errors
- Leave System errors
- Avoid catching plain Exception without any reasons
Handling
Throw errors
- Avoid returning error code.
- Consider rethrowing the error when you need to deligate the handling to its caller function.
Logging
- Use logger library
- Avoid logging into only Console without any reasons
- Use detailed error message
- BAD:
"unexpected error occured."
- GOOD:
"the name property is null."
- BAD:
Retry
- Consider when you need to address network delay, DB connection, external service.
- Configure reasonable interval and timeout.
- interval: Exponential backoff and jitter.
- timeout: Usually 3 ~ 5 times.
- Should test if its truly appropriate for your system and adjust it.
Post processing
Release resource
- Don't forget to release opened resource on finally clause.
- e.g.) file, network connection.
Comments
You should only write something you cannot express as code.
Others
Formatting
- 80 characters / line
- Unify tab or space for indent
- Would be nice if you configure auto format on IDE
Tools
It depends on your language, IDE or platform. Those are the example functionalities on Visual studio 2022 or plugins. Let's search if there are any similar features on your environment.
Discussion