I took the following notes whilst studying Arthur J. Riel’s book on OOD best practices, I hope they may be useful to you. This is a good book, definitely worth a place on any developer’s bookshelf. It is a bit dated, but much of the advice is solid, and you can see the seeds of many modern day best practices. One of the really interesting sections for me was the one discussing development methodologies. It strikes me that Agile was not so revolutionary. It is essentially an evolution of the iterative same-language prototyping approach circa early 90’s; with the addition of formalized roles, tools, and added processes to try limit the shipping of garbage (a real problem with the prototyping approach) and identifying technical debt, refactoring, unit testing etc.
Chapter 1 – The Motivation for OOP
One of the biggest reasons for moving to the OO paradigm is that is allows us to more closely model the real world. Successful real-word entities interact in a decentralized manner. Following this approach, loosely coupled classes can be updated without affecting the entire system.
There are two types of complexity in software: accidental complexity and essential complexity.
Accidental complexity occurs due to a mismatch of paradigms, methodologies, and/or tools. This type of complexity can be eliminated given sufficient resources to build or buy various tools. OOP helps to reduce accidental complexity by providing a consistent paradigm for software development that encompasses analysis, design, and implementation.
The real culprit of the software crisis is essential complexity. Software is intrinsically complex, and no methodology or tool is going to eliminate this complexity.
One way to control essential complexity is to avoid the development of software. Instead of building software, why not buy it? If you have to build it, write it in a way that it can be reused.
Chapter 2 – Classes and Objects: The Building Blocks of the Object-Oriented Paradigm
The object-oriented paradigm uses the concepts of class and object as basic building blocks in the formation of a consistent model for the analysis, design, and implementation of applications.
There are many things in the real world that we are capable of using without knowledge of anything about their implementation. They are designed to be used via a well-defined interface. The interface hides the complexity from the user. For example, the ignition system of a car.
One of the basic ideas in the object-oriented paradigm is exactly this philosophy. All implementation constructs in your system should be hidden from their users behind a well defined, consistent public interface. This allows the implementor to change the implementation whenever he or she desires, so long as the public interface remains the same.
An object will always have four facts:
- its own identity (often its address in memory)
- the attributes of its class
- the behavior of its class
- the published interface of its class
Messages and Methods
Objects should be treated as machines that will carry out operations in their public interface for anyone who makes an appropriate request.
Heuristic 2.1: All data should be hidden within its class.
The violation of this heuristic effectively throws all maintenance out the window. Information hiding reduces coupling, and localizes changes.
Heuristic 2.2: Users of a class must be dependent on its public interface, but a class should not be dependent on its users.
The rationale behind this heuristic is reusability. Such dependencies are undesirable.
Heuristic 2.3: Minimize the number of messages in the protocol of a class.
By keeping the interface minimal, we make the system easier to understand, the component easier to use, and we only develop what we need.
(Note: see YAGNI, and the ISP in SOLID)
Heuristic 2.4: Implement a minimal public interface that all classes understand.
This minimal public interface consists of functionality that can be reasonably expected from each and every class.
(Note: see Object or TObject in languages like C# and Delphi)
Heuristic 2.5: Do not put implementation details such as common-code private functions into the public interface of a class.
Keep implementation details out of the public interface, for obvious reasons, as well as the fact that the user does not need to see such details. For example, if you user needs to insert and remove items from a container, but never needs to find an item, then put the find code which is common to the insert and remove into a private method.
Heuristic 2.6: Do not clutter the public interface of a class with items that users of that class are not able to use or not interested in using.
A generalization of the previous heuristic.
Class Coupling and Cohesion
We strive for tight cohesion within classes and loose coupling between classes.
There are five basic forms of coupling between classes:
- nil coupling
- export coupling (client and server)
- overt coupling (see friend class in C++)
- covert coupling (same as overt, but no permission granted)
- surreptitious coupling (class X knows class Y’s implementation details)
(Note: see also temporal coupling, and inheritance coupling)
Heuristic 2.7: Classes should only exhibit nil or export coupling with other classes.
Avoid coupling to implementation details.
Heuristic 2.8: A class should capture one and only one abstraction.
A key abstraction is defined as a main entity within a domain model. Key abstractions often show up as nouns within requirements specifications. Each key abstraction should map to only one class. If it maps to more than one class, then the designer is probably capturing each function as a class. If more than one key abstraction maps to the same class, then the designer is probably creating a centralized system. There classes are often called vague classes and need to be split into two or more classes and abstractions.
(Note: see SRP in SOLID)
Heuristic 2.9: Keep related data and behavior in one place.
The two areas are actually of the same key abstraction and therefore should have been captured in the same class. The designer should watch out for objects that dig data out of other objects via some “get” operation.
(Note: for example, request a status directly from an object, don’t analyze its attributes to determine the status, put that logic in the object with the attributes)
Heuristic 2.10: Spin off non-related information into another class.
The developer should look for classes with a subset of methods that operate on a proper subset of the data members. The extreme case is a class where half of the methods work on half of the data members and the other half of the methods work on the other half of the data members.
(Note: dynamic semantics is essentially the state of an object)
(Note: Essentially interfaces in older languages, with optional default behavior and attributes. Can’t be instantiated directly.)
Roles Versus Classes
Heuristic 2.11: Be sure the abstractions that you model are classes and not simply the roles objects play.
Is Mother or Father a class, or are they roles that certain Person objects play? The answer depends on the domain that a designer is modeling. If, in the given domain, Mother and Father have different behavior, then they should probably be modeled as classes. If they have the same behavior, then they are different roles that objects of the Person class play.
Chapter 3 – Topologies of Action-Oriented vs Object-Oriented Applications
(Note: action-oriented programming is procedural, imperative programming)
While action-oriented software development is involved with functional decomposition through a very centralized control mechanism, the object-oriented paradigm focuses more on the decomposition of data with its corresponding functionality in a very decentralized setting.
The God Class Problem (Behavioral Form)
The behavioral form of the god class problem is caused by a common error among action-oriented developers in the process of moving to the object-oriented paradigm. These developers attempt to capture the central control mechanism so prevalent in the action-oriented paradigm within their object-oriented design. The result is the creation of a god object that performs most of the work, leaving minor details to a collection of trivial classes.
Heuristic 3.1: Distribute system intelligence horizontally as uniformly as possible, that is, the top-level classes in a design should share the work uniformly.
Heuristic 3.2: Do not create god classes/objects in your system. Be very suspicious of a class whose name contains Driver, Manager, System, or Subsystem.
Heuristic 3.3: Beware of classes that have many accessor methods defined in their public interface. Having many implies that related data and behavior are not being kept in one place.
Heuristic 3.4: Beware of classes that have too much non-communicating behavior, that is, methods that operate on a proper subset of the data members of a class. God classes often exhibit much non-communicating behavior.
Too many accessor methods are not dangerous because they give away implementation details – they are dangerous because they often indicate poor encapsulation of related data and behavior.
(Note: return a Point object rather than creating get_x, get_y, set_x and set_y methods on your class – let the Point object do the work. A better example is provided, see the image above. Don’t add the course prerequisites check to the course-offering class, implement the check in a policy class which performs the check.)
Heuristic 3.5: In applications that consist of an object-oriented model interacting with a user interface, the model should never be dependent on the interface. The interface should be dependent upon the model.
(Note: see the MVC pattern, or MVP, MVVM, etc.)
Heuristic 3.6: Model the real world whenever possible.
(Note: This helps improve clarity, especially when discussing the system with users. This heuristic is often violated for reasons of system intelligence distribution, avoidance of god classes, and the keeping of related data and behavior in one place.)
The Proliferation of Classes Problem
Heuristic 3.7: Eliminate irrelevant classes from your design.
Heuristic 3.8: Eliminate classes that are outside the system.
Heuristic 3.9: Do not turn an operation into a class. Be suspicious of any class whose name is a verb or is derived from a verb, especially those that have only one piece of meaningful behavior. Ask if that behavior needs to be migrated to some existing or undiscovered class.
The Role of Agent Classes
Keeping agent classes in a design is okay if they capture a useful abstraction. Keeping irrelevant agents in a system overly complicates the design, with no benefit to the designer.
Heuristic 3.10: Agent classes are often placed in the analysis model of an application. During design time, many agents are found to be irrelevant and should be removed.
Chapter 4 – The Relationships Between Classes and Objects
Heuristic 4.1: Minimize the number of classes with which another class collaborates.
Heuristic 4.2: Minimize the number of message sends between a class and its collaborator.
Heuristic 4.3: Minimize the amount of collaboration between a class and its collaborator, that is the number of different messages sent.
Heuristic 4.4: Minimize fanout in a class, that is, the product of the number of messages defined by the class and the messages they send.
Heuristic 4.5: If a class contains objects of another class, then the containing class should be sending messages to the contained objects.
Heuristic 4.6: Most of the methods defined on a class should be using most of the data members most of the time.
A class should capture only one meaningful abstraction within a domain.
Heuristic 4.7: Classes should not contain more objects than a developer can fit in his or her short-term memory. A favorite value for this number is six.
Heuristic 4.8: Distribute system intelligence vertically down narrow and deep containment hierarchies.
(Note: deep hierarchies are typically frowned upon nowadays)
Semantic Constraints Between Classes
It is very common for containment hierarchies to have semantic constraints imposed upon them. In the case of the meal class, we wanted the appetizer to come first, the entrée second, the desert third. We captured this information in the definition of the class. This is the preferable method for capturing semantic constraints because it makes it impossible for a user of the class to build objects that violate the semantic constraint.
(Note: for more complex constraints, use factory methods or factory/builder objects. It is also possible to allow the construction of the object but use a validation method to enforce constraints)
Heuristic 4.9: When implementing semantic constraints, avoid the proliferation of classes.
Heuristic 4.10: When implementing semantic constraints in the constructor of a class, place the constraint test as far down the containment hierarchy as the domain allows.
(Note: best to avoid using the constructor for this.)
Heuristic 4.11: The semantic information on which a constraint is based is best placed in a central, third-party object when that information is volatile.
Heuristic 4.12: The semantic information on which a constraint is based is best decentralized among the classes involved in the constraint when that information is stable.
Heuristic 4.13: A class must know what it contains, but it should not know who contains it.
Heuristic 4.14: Objects that share lexical scope – those contained in the same containing class – should not have relationships between them.
Chapter 5 – The Inheritance Relationship
Heuristic 5.1: Inheritance should be used only to model a specialization hierarchy.
Heuristic 5.2: Derived class must have knowledge of their base class by definition, but base classes should not know anything about their derived classes.
Heuristic 5.3: All data in a base class should be private, do not use protected data.
(Note: the author prefers protected accessor methods if you really need to access to data in a base class.)
Heuristic 5.4: In theory, inheritance hierarchies should be deep – the deeper, the better.
(Note: this is a big no. Prefer composition over inheritance. Inheritances is the tightest form of coupling, it is often difficult to get the abstractions correct, and a dangerous way to introduce brittle code hard to modify.)
Heuristic 5.5: In practice, inheritance hierarchies should be no deeper than an average person can keep in his or her short-term memory. A popular value for this is six.
(Note: again, don’t do this. Prefer composition over inheritance, and shallow hierarchies.)
Heuristic 5.6: All abstract classes must be base classes.
Heuristic 5.7: All base classes should be abstract classes.
(Note: this is really about coding to interfaces, and is generally a good idea. In some cases, however, this is not necessary – see Object or TObject in C# and Delphi.)
Heuristic 5.8: Factor the commonality of data, behavior, and/or interface as high as possible in the inheritance hierarchy.
This is to maximize code reuse.
(Note: avoid deep hierarchies.)
Heuristic 5.9: If two or more classes share only common data (no common behavior), then that common data should be placed in a class that will be contained by each sharing class.
(Note: for example, a Point class)
Heuristic 5.10: If two or more classes have common data and behavior then those classes should each inherit from a common base class that captures those data and methods.
Heuristic 5.11: If two or more classes share only a common interface (messages, not methods), then they should inherit from a common base class only if they will be used polymorphically.
(Note: in Java or C# use an interface.)
Heuristic 5.12: Explicit case analysis on the type of an object is usually an error. The designer should use polymorphism in most of these cases.
(Note: use polymorphism rather than type-checking with specific calls.)
Heuristic 5.13: Explicit case analysis on the value of an attribute is often an error. The class should be decomposed into an inheritance hierarchy, where each value of the attribute is transformed into a derived class.
(Note: example provided – rather than a Ball class with a Color attribute, create a Ball class with subclasses, i.e. RedBall, GreenBall, BlueBall. This is very domain specific.)
Heuristic 5.14: Do not model the dynamic semantics (state) of a class through the use of the inheritance relationship. An attempt to model dynamic semantics with a static semantic relationship will lead to a toggling of types at runtime.
Heuristic 5.15: Do not turn objects of a class into derived classes of the class. Be very suspicious of any derived classes for which there is only one instance.
(Note: if you really need indirection, use an interface in this case, i.e. ITaxCalculator and any implementing classes such as UkTaxCalculator, AusTaxCalulator, etc. See the Strategy Pattern.)
Heuristic 5.16: If you think you need to create new classes at runtime, take a step back and realize that what you are trying to create are objects. Now generalize these objects into a class.
Heuristic 5.17: It should be illegal for a derived class to override a base class method with a NOP method, that is, a method that does nothing.
Heuristic 5.18: Do not confuse optional containment with the need for inheritance. Modeling optional containment with inheritance will lead to a proliferation of classes.
Heuristic 5.19: When building an inheritance hierarchy, try to construct reusable frameworks rather than reusable components.
Chapter 6 – Multiple Inheritance
Heuristic 6.1: If you have an example of multiple inheritance in your design, assume you have made a mistake and prove otherwise.
Heuristic 6.2: Whenever there is inheritance in an object-oriented design, ask yourself two questions – 1) Am I a special type of the thing from which I’m inheriting? 2) Is the thing from which I’m inheriting part of me?
Heuristic 6.3: Whenever you have found a multiple inheritance relationship in an object-oriented design, be sure that no base class is actually a derived class of another base class.
Chapter 7 – The Association Relationship
Heuristic 7.1 When given a choice in an object-oriented design between a containment relationship and an association relationship, choose the containment relationship.
(Note: for value types like Point, this is possible in languages like C# and Java, but class objects dictate an association relationship. In C++ you have more flexibility.)
Chapter 8 – Class-Specific Data and Behavior
Heuristic 8.1: Do not use global data or functions to perform bookkeeping information on the objects of a class. Class variables or methods should be used instead.
Chapter 9 – Physical Object-Oriented Design
Physical design involves the techniques used to map abstract constructs onto given software and hardware platforms. They are implementation details based on the target language(s), tools, networks and their protocols, databases, or hardware would be included in the physical design.
Heuristic 9.1: Object-oriented designers should not allow physical design criteria to corrupt their logical designs. However, physical design criteria are often used in the decision-making process at logical design time.
(Note: the author discusses creating object-oriented classes which call into a legacy library which can’t be rewritten. I’d probably create a façade – see Big Ball of Mud Pattern – which takes care of interfacing with the legacy library, then add a service layer with services grouped by functionality/abstraction which call into the façade. But it’s hard to say without looking at the problem domain.)
Heuristic 9.2: Do not change the state of an object without going through its public interface.
Chapter 10 – The Relationship Between Heuristics and Patterns
(Note: the author explores the relationship between heuristics and design patterns)
Chapter 11 – The Use of Heuristics in Object-Oriented Design
(Note: a case study where the author applies the heuristics.)