Graduate Program KB

Clean Code

Chapter 6

  • This chapter discusses the principles related to objects and data structures
  • It highlights the importance of managing relationship between objects and data structures and how it affects writing clean and maintainable code

Data Abstraction

  • Implementation should be hidden by adding abstractions, not layers of functions
    • Adding public variables in classes allows it to be directly mutated which exposes its implementation
    • Even if the variables were private and then setters and getters were added, it would still expose implementation
    • Rather, we should expose abstract interfaces which enable the user to indirectly manipulate the data without knowing its implementation
    • In the first example, it's obvious the functions are getters to variables
    • In the second example, the implementation is abstract, the form and details of the data are hidden from the outer scope
    public interface Vehicle {
        double getFuelTankCapacityInGallons();
        double getGallonsOfGasoline();
    }
    
    public interface Vehicle {
        double getPercentFuelRemaining();
    }
    

Data/Object Anti-Symmetry

  • The difference between objects and data structures:

    • Objects hide their data behind abstractions and expose functions that operate on that data
    • Data structures expose their data and have no meaningful functions
    • They are virtually opposite
  • Consider the shape example, which uses a procedural approach

    • The shape classes are simple data structures with no behaviour
    • The Geometry class has all the behaviour
    public class Square {
        public Point topLeft;
        public double side;
    }
    
    public class Circle {
        public Point centre;
        public double radius;
    }
    
    public class Geometry {
        public final double PI = 3.1415;
    
        public double area(Object shape) {
            if (shape instanceof Square) {
                Square s = (Square)shape;
                return s.side * s.side;
            }
            else if (shape instanceof Circle) {
                Circle c = (Circle)shape;
                return PI * c.radius * c.radius;
            }
        }
    }
    
    • If we wanted to add a perimeter function to Geometry, the shape classes would be unaffected
    • On the other hand, if we wanted to add another shape then all the functions in Geometry would change
  • Now consider the same example, but with an object-oriented approach

    • Geometry class is no longer needed
    • area is a polymorphic method, the fields are now hidden
    public class Square implements Shape {
        private Point topLeft;
        private double side;
    
        public double area() {
            return side * side;
        }
    }
    
    public class Circle implement Shape {
        private Point centre;
        private double radius;
    
        public double area() {
            return PI * radius * radius;
        }
    }
    
  • Procedural code (code using data structures) makes it easy to add new functions without changing the existing data structure

    • Complement: Procedural code makes it hard to add new data structures because all the functions must change
  • Object-oriented code makes it easy to add new classes without changing existing functions

    • Complement: Object-oriented code makes it hard to add new functions because all the classes must change

The Law of Demeter

  • The Law of Demeter is a principle that states that a module should not know about the inner workings of the objects it manipulates

    • An object should not expose its internal structure through accessors
  • The law states that a function f of a class C should only call the methods of these:

    • Methods of class C

      • Methods within a class can call other methods in the same class
    • An object created by f

      • If a method creates an object internally, it has control of the creation and behaviour of the object
    • An object passed as an argument to f

      • It allows collaboration with the object by allowing it to call its methods without violating encapsulation
    • An object held in an instance variable of class C

      • If the class holds an object as a member field then methods of that class can invoke functions of the field object
  • The method should not invoke methods on objects that are returned by any of the allowed functions

    • Basically, the metaphor "talk to friends, not to strangers" can be applied here
    • Objects should only communicate with closely related collaborators or objects and not interfere with the internal structure of other objects
  • The following code violates the Law of Demeter

    • Chain of function calls like this is considered a sloppy style and should be avoided
    • getScratchDir is called on the return of getOptions
    • getAbsolutePath is called on the return of getScratchDir
    String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
    
    • Instead, they should be split up
    Options opts = ctxt.getOptions();
    File scratchDir = opts.getScratchDir();
    String outputDir = scratchDir.getAbsolutePath();
    
    • If they are objects, then their internal structure is exposed which violates Law of Demeter
    • If they are data structures with no behaviours, then the Law of Demeter does not apply

Hybrids

  • Hybrid structures are a combination of both object and data structure
    • They can have public variables or private variables with accessors and mutators
    • Tempt external functions to use these variables the way a procedural program would use data structures
    • Hard to add new functions, but also hard to add new data structures
    • Avoid creating them, they inherit the disadvantages of both structures

Hiding Structure

  • Objects are supposed to hide their internal structure
  • Considering the path to scratch directory example, how do we get access to that if we can't navigate through objects, assuming those functions return objects
    • Having a whole function like ctxt.getAbsolutePathOfScratchDirectoryOption() might cause a bunch of function invocations on the ctxt object
    • ctxt.getScratchDirectoryOption().getAbsolutePath() presumes a data structure is returned
    • Assuming ctxt is an object, it should be told to do something
      • The absolute path of the scratch directory is used elsewhere in the code, figure out where it is and why it's used
      • Create a new function on ctxt that handles this behaviour, now the internal structure of the object is hidden

Data Transfer Objects

  • The ideal form of a data structure is a class with public variables and no functions, referred to as a data transfer object (DTO)
  • Often used to convert raw data in a database into objects in the application code
  • Some people try to enforce the style of using private variables and accessors/mutators instead but it usually provides no other benefit

Active Record

  • Special forms of DTOs, they have public variable but typically have navigational methods like save and find
  • Usually, they are direct translations from database tables or other data sources
  • Becareful of turning an Active Record into a hybrid structure, developers tend to add other methods and treat them as objects instead
    • If you want to add other methods, create separate objects for them and treat the Active Record as a data structure