Introduction
Arthur J. Riel's Object-Oriented Design Heuristics is one of those older software books that still deserves attention.
It comes from a period when object-oriented design (OOD) was still consolidating as a discipline. Many of the heuristics now feel familiar because they anticipate ideas that would later be framed more formally in SOLID, GRASP, design patterns, MVC-style separations, and broader responsibility-driven design.
The book is like a record of the design instincts from which many modern best practices emerged.
One of the more striking sections, for me, is the discussion of development methodologies.
Looking back, it is hard not to notice how much later revolutions in software practice were really refinements of existing iterative ideas. Agile did not appear out of nowhere. In many ways, it looks like a more disciplined evolution of same-language prototyping, with stronger feedback loops, clearer roles, better tooling, automated testing, and more deliberate attempts to avoid shipping garbage.
What makes Riel's book valuable is not that every heuristic should be followed literally today. Some should not. Rather, its value lies in the questions it teaches the designer to ask:
- is this class hiding its implementation properly?
- is this abstraction coherent?
- is system intelligence distributed sensibly?
- are we using inheritance for the right reason?
The motivation for object-oriented design
Riel begins from a now-familiar observation: OOD allows us to model the world in a more decentralized way. Real systems do not usually succeed because one giant central controller manipulates everything else. They succeed because many cooperating parts interact through well-defined responsibilities. OOD tries to mirror that.
This connects to the distinction between accidental complexity and essential complexity.
Accidental complexity arises from poor tools, mismatched paradigms, bad methodologies, and awkward implementation environments. Essential complexity belongs to the problem itself. Software is often hard because the domain is genuinely hard.
Object orientation can help reduce accidental complexity by giving analysis, design, and implementation a more unified style. But it does not abolish essential complexity. No tool or method does. That is why one of the most practical observations in the chapter is also one of the simplest: if you can buy software instead of building it, do so. If you must build it, build it in a way that encourages reuse.
Classes, objects, and public interfaces
The second chapter focuses on the basic vocabulary of OOD: classes, objects, messages, methods, and interfaces. Much of this material now feels familiar, but it still matters because it grounds the rest of the book's advice.
An object will generally have four characteristics:
- its own identity (often its address in memory)
- the attributes of its class
- the behavior of its class
- the published interface of its class
One of the book's central ideas is that software objects should be used through a well-defined public interface, while implementation details remain hidden.
There are many things that we ordinarily use without knowing their implementation details. 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.
Riel states that objects should be treated as machines that will carry out operations in their public interface for anyone who makes an appropriate request.
That leads directly to one of the strongest heuristics in the book:
Heuristic 2.1:
All data should be hidden within its class.
- users should depend on public interfaces; classes should not depend on their users
- minimise the public protocol of a class
- keep common private implementation details out of the public interface
- do not clutter the interface with operations users do not need
A great deal of later OO advice can be read as a refinement of these points.
YAGNI, ISP, information hiding, and a good part of API design discipline all reflect the same basic idea: make the interface small, meaningful, and stable.
Coupling, cohesion, and the shape of a class
This is one of the strongest sections in the book.
Riel's general principle is simple: strive for tight cohesion within classes and loose coupling between them. That remains as true now as it was then. Several heuristics here are especially durable:
Heuristic 2.8:
A class should capture one and only one abstraction
Heuristic 2.9:
Keep related data and behavior in one place
Heuristic 2.10:
Spin off non-related information into another class
These are essentially warnings against vague, bloated, low-cohesion classes.
When one class represents several abstractions at once, or when its methods operate on disconnected subsets of its data, the design is already drifting toward confusion.
One good observation is the warning against classes that constantly dig data out of other classes and then make decisions on that basis. That is usually a sign that the behavior belongs closer to the data itself. In modern terms, this anticipates the criticism of anemic models and procedural leakage.
The section on coupling is still useful, even if some of the terminology is dated.
The enduring lesson is straightforward: classes should not rely on one another's implementation details. Nil or export coupling is fine; surreptitious coupling is not.
Roles and classes
One of the more subtle heuristics in the chapter concerns the distinction between classes and roles.
Riel asks whether something like Mother or Father should be modelled as a class, or whether those are simply roles played by objects of class Person. The answer, as he rightly notes, depends on the domain. If the role carries genuinely different behavior in the domain being modelled, then a separate class may be justified. If not, the distinction may be purely role-based rather than taxonomic.
This is still a useful question because developers often mistake temporary roles, states, or perspectives for distinct ontological types. The heuristic encourages the designer to ask whether the abstraction is truly a class, or merely a role played within a larger model.
Action-oriented thinking, god classes, and misplaced intelligence
The third chapter is one of the book's most useful. Riel contrasts action-oriented software with OOD and shows how procedural instincts can easily reappear inside nominally OO code.
The central warning is the god class problem.
Developers moving from procedural or action-oriented paradigms often recreate the old centralized controller within an object-oriented system. One large object ends up performing most of the work, while the rest of the classes are trivial helpers or passive data holders.
The heuristics here remain highly relevant:
- distribute system intelligence horizontally
- do not create god classes
- be suspicious of classes named
Manager,System,Driver, orSubsystem - beware of too many accessor methods
- beware of non-communicating behavior inside a class
That last point is particularly sharp. Accessors are not dangerous merely because they expose state. They are dangerous because they often reveal that behavior and data have been separated improperly. If a class mostly hands out data for others to interpret, it may not really own its own responsibilities.
The companion heuristic is also still excellent:
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.
This is one of those places where you can clearly see the same principle expressed in architectural styles such as MVC, MVP, MVVM, and clean separation between domain and presentation.
Class proliferation and the danger of over-modelling
The same chapter also addresses the opposite problem: not too few meaningful abstractions, but too many irrelevant ones. This is a useful corrective, because developers new to OOD often swing between two extremes:
- one giant centralized object
- an explosion of tiny, unjustified classes
Riel's advice here is still sound:
- eliminate irrelevant classes
- eliminate classes outside the system
- do not turn verbs into classes too quickly
- remove agent classes that do not capture a real abstraction
This is not an argument against services or helper abstractions in general. It is an argument against creating classes with no real conceptual weight. A class should earn its existence by capturing something meaningful in the domain or design.
Collaboration between classes
The fourth chapter turns to relationships between classes, and much of it is recognisably about managing collaboration complexity.
The central pattern is once again familiar:
- minimise the number of collaborators
- minimise the number of messages between collaborators
- minimise the variety of messages sent
- keep fanout under control
Much of that can sound mechanical when stated abstractly, but the underlying design goal is good: keep collaboration focused and intelligible.
The discussion of semantic constraints is useful. Constraints should be implemented in a way that preserves the integrity of the domain, but without proliferating unnecessary classes. The modern reader will probably supplement Riel here with factories, builders, specifications, or domain validation strategies.
Inheritance: where the book is strongest, and where it dates
The inheritance chapter is probably the place where the post should be most balanced.
Some of Riel's heuristics remain absolutely right:
- use inheritance only for true specialization
- base classes should not know derived classes
- explicit case analysis on type is usually a design smell
- do not model runtime state using inheritance
- optional containment is not a reason for inheritance
These remain solid. In particular, the warnings against modelling state changes as class changes, or against using inheritance where composition or strategy would do better, still deserve repeating.
But other heuristics here show their age more clearly. The book is much more comfortable with deep inheritance hierarchies than modern practice tends to be. Today, most developers would strongly prefer shallow hierarchies, using interfaces and composition where appropriate rather than relying heavily on implementation inheritance.
Inheritance is a tight form of coupling. Implemented poorly, it results in a hierarchy of brittle abstractions that require workarounds to remain functional in the face of change.
Multiple inheritance, association, and class-specific behavior
The chapters on multiple inheritance, association, and class-specific behavior are shorter, but they still contain useful guidance. The multiple inheritance advice is easy to agree with:
If you think multiple inheritance is necessary, assume you are wrong and prove otherwise.
That remains sensible today, even though some languages handle mixin-style composition more gracefully than others. The key question Riel asks is still the right one: Is this really specialization, or is it containment/composition in disguise?
The preference for containment over association is more debatable if taken too absolutely, but the instinct is understandable. Containment expresses a stronger structural relationship, and where that relationship genuinely exists, it can reduce ambiguity in the design.
The heuristic on class-level data also still translates cleanly into modern language:
- do not use global data/functions for class bookkeeping
- use class-level state or behavior where class-specific responsibility genuinely exists
Logical design versus physical design
One of the more mature points in the book appears in the discussion of physical design.
Riel distinguishes logical design from physical design and warns that implementation concerns should not corrupt the logical structure of the model. This is still excellent advice.
Technology matters. Databases, protocols, deployment environments, tools, and target languages all matter. But they should not be allowed to dictate the conceptual shape of the system prematurely. If the logical model is bent too early around physical concerns, the design loses clarity.
This is one of the places where the book most obviously resonates with later architectural ideas such as clean architecture, hexagonal architecture, and domain-centred design. The modern phrasing may differ, but the instinct is the same: protect the conceptual model from infrastructure-driven distortion.
Heuristics, patterns, and what the book still teaches
The final chapters connect heuristics with patterns and include a case study.
By this point, the book has taught the reader to notice design smells early:
- too much interface
- too much access to internals
- too much centralized intelligence
- too much inheritance for the wrong reasons
- too many trivial or irrelevant classes
- too much dependence on physical implementation detail
Patterns solve recurring structural problems.
Heuristics help the designer notice when a problem exists in the first place.
The two are complementary.
Conclusion
Object-Oriented Design Heuristics is dated, but not obsolete.
Some of its specific recommendations should now be treated cautiously, especially around inheritance depth and older language-era assumptions. But much of its advice remains surprisingly fresh.
Encapsulation, minimal interfaces, cohesion, low coupling, the rejection of god classes, skepticism toward type-switching, and protection of logical design from physical corruption are all still core concerns in good software design.
The book captures a moment when OOD was still being worked out in detail, and in doing so it reveals how many of our current best practices were already present in heuristic form.
It is not the last word on OOD, but it is a valuable one.
Heuristics
Arthur Riel's Design Heuristics (with personal notes in brackets)| Id | Description | Note | |
|---|---|---|---|
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. | |
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. | |
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. (YAGNI, and the ISP in SOLID) | |
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. (Object or TObject in languages like C#, Java and Delphi) | |
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 example, if the 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. Don't expose the container. (Encapsulation) | |
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. | |
2.7 | Classes should only exhibit nil or export coupling with other classes. | Avoid coupling to implementation details. (Temporal coupling and inheritance coupling are often overlooked in modern codebases) | |
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. These classes are often called vague classes and need to be split into two or more classes and abstractions. (SRP in SOLID) | |
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. (An 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) | |
2.10 | Spin off non-related information into another class. | 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. | |
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. | |
3.1 | Distribute system intelligence horizontally as uniformly as possible, that is, the top-level classes in a design should share the work uniformly. | ||
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. | ||
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. | ||
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. (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 don't add a course prerequisites check to the course-offering class, implement the check in a policy class which performs the check) | |
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. | (MVC pattern, or MVP, MVVM) | |
3.6 | Model the real world whenever possible. | (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) | |
3.7 | Eliminate irrelevant classes from your design | ||
3.8 | Eliminate classes that are outside the system | ||
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. | ||
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. | ||
4.1 | Minimize the number of classes with which another class collaborates | ||
4.2 | Minimize the number of message sends between a class and its collaborator | ||
4.3 | Minimize the amount of collaboration between a class and its collaborator, that is the number of different messages sent. | ||
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. | ||
4.5 | If a class contains objects of another class, then the containing class should be sending messages to the contained objects. | ||
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. | |
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. | ||
4.8 | Distribute system intelligence vertically down narrow and deep containment hierarchies. | (Deep hierarchies are typically frowned upon nowadays) | |
4.9 | When implementing semantic constraints, avoid the proliferation of classes. | (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) | |
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. | (Best to avoid using the constructor for this) | |
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. | ||
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. | ||
4.13 | A class must know what it contains, but it should not know who contains it. | ||
4.14 | Objects that share lexical scope - those contained in the same containing class - should not have relationships between them. | ||
5.1 | Inheritance should be used only to model a specialization hierarchy. | ||
5.2 | Derived classes must have knowledge of their base class by definition, but base classes should not know anything about their derived classes. | ||
5.3 | All data in a base class should be private, do not use protected data. | (The author prefers protected accessor methods if you really need to access data in a base class) | |
5.4 | In theory, inheritance hierarchies should be deep - the deeper, the better. | (This didn't age well. Prefer composition over inheritance. Inheritance 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) | |
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. | (Again, don't do this. Prefer composition over inheritance, and shallow hierarchies) | |
5.6 | All abstract classes must be base classes. | ||
5.7 | All base classes should be abstract classes | (This is really about coding to abstractions, and the idea is sound. Today, however, interfaces provide the primary abstraction in many languages) | |
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 - but... avoid deep hierarchies) | |
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. | (For example, a Point class) | |
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. | ||
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. | (In Java or C# use an interface) | |
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. | (Use polymorphism rather than type-checking with specific calls) | |
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. | 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. | |
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. | ||
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. | (If you really need indirection, use an interface in this case, i.e. ITaxCalculator and any implementing classes such as UkTaxCalculator, AusTaxCalculator, etc. See the Strategy Pattern) | |
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. | ||
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. | ||
5.18 | Do not confuse optional containment with the need for inheritance. Modeling optional containment with inheritance will lead to a proliferation of classes. | ||
5.19 | When building an inheritance hierarchy, try to construct reusable frameworks rather than reusable components. | ||
6.1 | If you have an example of multiple inheritance in your design, assume you have made a mistake and prove otherwise. | ||
6.2 | Whenever there is inheritance in an OOD, 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? | ||
6.3 | Whenever you have found a multiple inheritance relationship in an OOD, be sure that no base class is actually a derived class of another base class. | ||
7.1 | When given a choice in an OOD between a containment relationship and an association relationship, choose the containment relationship. | ||
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. | ||
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. | ||
9.2 | Do not change the state of an object without going through its public interface. |