Graduate Program KB

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()
    }
    

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