Clean Code
Chapter 10
Class Organisation
- Class structure:
-
List of variables:
- Public static constants
- Private static variables
- Private instance variables
-
Public functions
-
Private functions placed directly after the public function that utilises it
-
Classes Should Be Small!
-
The size of a class is dependent upon its number of responsibilities
-
The name of the class describes the responsibilities it should fulfill
- If a concise named can't be created, then it's an indicator the class is too large
- Words such as Processor, Manager and Super indicate an aggregation of responsibilities
-
Example of a "God class":
- Consider reducing the class to the small number of highlighted methods, it still has multiple responsibilities
- Tracking version number
- Manages Java Swing components
public class SuperDashboard extends JFrame implements MetaDataUser { public String getCustomizerLanguagePath() public void setSystemConfigPath(String systemConfigPath) public String getSystemConfigDocument() ... // Many, many more methods public Component getLastFocusedComponent() public void setLastFocused(Component lastFocused) public int getMajorVersionNumber() public int getMinorVersionNumber() public int getBuildNumber() }
- Consider reducing the class to the small number of highlighted methods, it still has multiple responsibilities
The Single Responsibility Principle
- SRP states that a class / module should only have one reason to change
- The previous SuperDashboard example had two reasons to change
- Example of methods on SuperDashboard refactored to a single responsibility class:
public class Version { public int getMajorVersionNumber() public int getMinorVersionNumber() public int getBuildNumber() }
- Codebases contain large amounts of logic and complexity
- Organisation, cleanliness and refactoring of code is just as important as getting the code to work
- Despite enforcing SRP and navigating more smaller class files, there is no extended functionality from the original uncoupled classes
- The amount of knowledge to consume is the same
- But the system has a well-defined structure containing smaller components, improving maintainability
Cohesion
- Classes should have a small number of instance variables
- Methods of the class should manipulate at least one of its instance variables
- The cohesiveness of a method to its class is measured by the number of variables manipulated
- Maximum cohesion is unrealistic, but high cohesion is ideal, indicating the methods and variables are closely related logically
- Example of a very cohesive class:
public class Stack { private int topOfStack = 0; List<Integer> elements = new LinkedList<Integer>(); public int size() { return topOfStack; } public void push(int element) { topOfStack++; elements.add(element): } public int pop() throws PoppedWhenEmpty { if (topOfStack === 0) { throw new PoppedWhenEmpty(); } int element = elements.get(--topOfStack); elements.remove(topOfStack); return element; } }
Maintaining Cohesion Results in Many Small Classes
-
Consider extracting a part of a function to a separate function
- The separate function uses variables declared in the original function, they need to access them
- Don't need to pass variables as arguments, just promote them to instance variables of the class
- But adding instance variables to enable few functions reduces cohesion, instead, split functions sharing certain variables into a new class
-
Listing 10-5 example:
- Lots of responsibility taken on by the main function
- Bad variable names, indentation and highly coupled
- Should be split into 2 additional classes, one for calculating primes and one for printing primes
public class PrintPrimes { public static void main(String[] args) { final int M = 1000; final int RR = 50; ... // Lots of undescriptive variables while (K < M) { do { J = J + 2; if (J == SQUARE) { ORD = ORD + 1; SQUARE = P[ORD] * P[ORD]; MULT[ORD - 1] = J; } N = 2; JPRIME = true; while (N < ORD && JPRIME) { while (MULT[N] < J) { MULT[N] = MULT[N] + P[N] + P[N]; } if (MULT[N] == J) { JPRIME = false; } N = N + 1; } } while (!JPRIME); K = K + 1; P[K] = J; } { PAGENUMBER = 1; PAGEOFFSET = 1; while (PAGEOFFSET <= M) { System.out.println("The First " + M + " Prime Numbers --- Page " + PAGENUMBER); System.out.println(""); for (ROWOFFSET = PAGEOFFSET; ROWOFFSET < PAGEOFFSET + RR; ROWOFFSET++) { for (C = 0; C < CC;C++) { if (ROWOFFSET + C * RR <= M) { System.out.format("%10d", P[ROWOFFSET + C * RR]); } } System.out.println(""); } System.out.println("\f"); PAGENUMBER = PAGENUMBER + 1; PAGEOFFSET = PAGEOFFSET + RR * CC; } } } }
Organising for Change
-
Modifications to a class has the potential of breaking other code
-
Organise classes to reduce the risk of change when making modifications
-
Example of a work in-progress class subject to change (opened):
- Violates SRP, class must change when adding a new statement or alter details of a single type of statement
- Multiple private methods applying to different public methods can indicate potential refactoring
public class Sql { public Sql(String table, Column[] columns) public String create() public String insert(Object[] fields) public String selectAll() public String findByKey(String keyColumn, String keyValue) public String select(Column column, String pattern) public String select(Criteria criteria) public String preparedInsert() private String columnList(Column[] columns) private String valuesList(Object[] fields, final Column[] columns) private String selectWithCriteria(String criteria) private String placeholderList(Column[] columns)public Sql(String Table, Column[] columns) }
-
Ideally, new features are implemented by extending the system, not by making modifications to existing code
-
Factor out public interface methods into its own subclass, also moving any necessary private methods along with it
- Classes become much simpler, comprehensible and testable
- Greatly reduced risk of breaking code since modifications are made in a subclass instead
- Supports SRP, small subclasses with one reason to change
- Supports Open-Closed Principle (OCP), the restructured subclass format enables new functionality but keeps other classes closed during this process
-
Example of some refactored Sql subclasses:
- Sql is an abstract class which declares an abstract method to be implemented by its subclasses
- The concrete subclasses each implement generate()
- Each subclass has its own constructor to initialise the table name, columns and other specific parameters
- Private methods specific to different queries are added to their subclass
abstract public class Sql { public Sql(String table, Column[] columns) abstract public String generate(); } public class CreateSql extends Sql { public CreateSql(String table, Column[] columns) @Override public String generate() } public class InsertSql extends Sql { public InsertSql(String table, Column[] columns, Object[] fields) @Override public String generate() private String valuesList(Object[] fields, final Column[] columns) } public class SelectWithCriteriaSql extends Sql { public SelectWithCriteriaSql(String table, Column[] columns, Criteria criteria) @Override public String generate() }
Isolating from Change
-
Dependencies upon concrete details make testing difficult because the expected result is volatile
-
Instead, the class should depend on an interface
- There will be two implementations of the interface, 1 used in real scenarios and 1 used in testing
- We have control over the test implementation, with the ability to fix any value and get an expected result
-
Support Dependency Inversion Principle (DIP) by minimising coupling this way
- Classes should depend upon abstractions, not on concrete details