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