Object Oriented Programming: Designing objects

❝Relevant concerns in designing an object.❞

This is a much discussed topic. This article is not intended as “the solution”, nor does it pretend to know better. This is my attempt to define a number of strict rules that naturally follow from OOP principles, instead of the numerous attempts to define soft rules that mostly cause interpretation problems and result only in more discussions.

This article relies on a previous article: Implementation and usage. Furthermore, we will strive for the quite commonly accepted goal of having only a “single responsibility” for an object.

The goal for this article is to pose a number of strict rules that will clearly define what to put in an object, and what to leave out. I aim to leave as little as possible open to interpretation.

The core of the object

The core of an object is its state. Every object is represented by the data it holds. Reducing the amount of state to the bare minimum is also the mechanism for ensuring an object to have only a single responsibility.

Implementation logic only

An object represents a concept. It exists for the purpose of representing this concept in an application. The concept is represented by the state and the behavior of the object. Actually, the concept is mostly represented by its state. The object’s behavior is there to support and ensure consistent state transitions.

Rule 1: Model a concept through the internal state necessary to express this concept.
Internal state must consist of fields that together represent the concept of the object. Nothing more, nothing less.

We already discussed, in the article on implementation and usage, that we can distinguish two types of logic: implementation logic and usage logic. Implementation logic manages the internal state of an object. Implementation logic has the privilege to access internal state. Usage logic, on the other hand, can only use the publicly provided API.

In terms of designing an object, where do we draw the line between functionality that should be implemented on the object itself and functionality that should be implemented elsewhere and use this object as part of its logic? Methods that are implemented on the object itself, are more conveniently accessible, that is obvious. The downside is that an object can grow infinitely large because of this. Looking at this from a design perspective, we can see that with every addition of a “convenient” method, the “single responsibility” goal will become a little more distant. Additional methods could even grow the object’s state, if a sufficient number of “extra” methods are added that we are required to add state (new fields) for managing their interaction.

Rule 2: Only allow for methods that require privileged access to internal state.
Methods (logic) that only depend on public API must not be added to the object itself.

By not applying a strict boundary for adding methods, we create a sliding scale. Instead, I propose to define a strict rule: Given an object that contains just enough internal state to represent its concept, we must only accept methods that manage that state. We must only allow for methods that contain logic that cannot be expressed through usage logic alone. Any method (even private methods that are defined in support of implementation logic) that only contains usage logic is an indication that this logic should move to another location, e.g. utility methods.

This rule also protects the developer from making the mistake to overuse fields. Because we cannot introduce new fields ad-hoc, we are restricted to using fields that are already there. So, developers that would (incorrectly) resort to fields for storing data that could have been stored in the local scope, are constrained by this rule.

Rule 3: Only allow for methods that manage the existing internal state.

Alternatively, added functionality that brings with it a requirement for new state, i.e. requires new fields to be added to the existing internal state, should be implemented as decorators/adapters. The reasoning behind this is that this is new functionality that is not strictly necessary for the original concept to be represented. As a consequence, this new functionality will only use the original object. However, it will require new fields to be available.

For example, let’s look at an in-memory cache for a reference that provides (relatively) expensive operations, such as accessing a networked service. By addressing these operations through the in-memory cache wrapper (which could be implemented using the decorator pattern), we augment the original reference for accessing the network service. We need not bother with inheriting from this object in order to influence implementation logic. It suffices to keep track of the most recent requests and keep a record of those results in memory. The wrapper needs only to verify if an answer is available before passing on the call to the networked service. Consequently, the wrapper is only dependent on public interface of the object.

The rules mentioned above enable us to construct minimal objects. Rule 1 ensures that we do not keep data in the internal state that are not part of the original concept the object should represent. Rule 2 ensures that you cannot add new methods that do not depend on privileged access to the internal state. Rule 3 ensures that you cannot introduce new data to the internal state merely in support of a (new) method of the object, instead of having methods in support of the internal state. These rules let you keep a pair of internal state + methods with a singular common goal, a minimal object with a single responsibility: the concept that the object represents.

Assuming that we follow these rules, there is one very typical use case that we prohibit: we cannot alternatingly add new methods for existing state and add new state for methods. It is easy to see that by not avoiding this situation, an object can go into a never-ending cycle, resulting in unbounded growth. Now we all know that we should not let an object grow infinitely big, but so far we have only seen “soft” rules often supported by arbitrary numbers, subject to mood and interpretation.

The article on utility objects discusses how we can use objects to augment behavior of other objects.

Reduced size, complexity and growth

In the introduction of this section we discussed the rules for minimizing the amount of state and corresponding implementation logic. The advantages of this are that of a reduced size and therefore reduced complexity. All methods are closely related, because they have a common goal: managing the internal state. The number of methods is small, so any planned changes to the object become more manageable and it is easier to have an overview.

Having smaller and more manageable objects is also advantageous in the evolution of your code. Often times, a concept expressed as code starts its life as a simple, concrete type. At some point, abstraction is required. If the amount of methods is reduced to the minimal amount necessary that consequently is closely related to the main concern of the object, then it becomes easier and more transparent to define an interface based on the original concrete type, reflects the concept.

Improved readability

With the strict rules for adding new methods a question might arise: Where do we put all the logic that is written in support of an implementation method, but that is only considered usage logic, since we do not access internal state?

As noted in Implementation and usage, even for implementation logic, there is a necessary amount of logic for supporting the connection from input parameters and return type, to the internal state. This logic is not inherently implementation or usage logic, as it depends on the nature of the logic. Logic that references objects not from the internal state, form a usage pattern.

Some types of objects tend to gather quite significant amounts of such supporting logic. When such methods get sufficiently large, subsets of the logic get extracted into a different (private) method. In case such methods do not directly reference the internal state of an object, then this is an indication that you are working with a usage pattern. This usage pattern can best be extracted as a utility method and called upon in the code. This reduces the size of the code and it makes such usage patterns more easily accessible to other logic. More on the use of utility methods in the article on Utilities.

By reducing the amount of (usage) logic inside methods, the remaining logic will perform more elaborate operations. A reduced number of lines of logic will have a bigger impact. This directly impacts readability because it becomes easier to comprehend the implementation of an object, as you do not get buried in implementation details. At the same time, the amount of logic in the object is reduced.

Flexible methods without exposing too much state

We have discussed how we should reduce the number of methods on an object by setting the constraint that a method must always access internal state. Even so, we can still define an innumerable many methods.

There is a strict rule that an object should be in complete control of the object’s state. Therefore, it is out of the question to, for example, provide access (possibly through accessor methods) to fields if that means that the user can interfere with the object’s state consistency. However, there is no upper bound of the amount of methods one can add to an object.

Typically we choose to expose methods that provide no or a very thin “layer” on top of the bare necessary implementation logic. As soon as all necessary behavior is provided, we can solve more elaborate use cases through the use of the object, i.e. through usage logic. When combining this reasoning with conveniently chosen types for parameters and return types, we can ensure that the methods that we do provide are maximally applicable. See Concrete types in implementation logic, section “Methods” for a reasoning on how applying types for maximal convenience.

“Short-cut” methods that use internal state

In a previous section we described how logic that only relies on public API should not be added to the object. There is a gray area where one can accomplish a certain goal using only public APIs, but there is a faster implementation possible that does rely on internal state.

According to the second rule, we only allow methods that actually leverage the privileged position and use the internal state access they are given. A more efficient logic that leverages this privileged access is thus allowed. Given that the internal state is fully related to the original concept, any (sensible, useful) method that uses this internal state is acceptable. So, given these rules, we allow implementing methods that exposes a more efficient logic, a “short-cut”, to (part of) a usage pattern.

Refer to the article on utilities on how we can leverage these “short-cut” implementations to make utility methods more intelligent by using the “short-cuts” that are available.

Improved reusability

… reducing the need to resort to inheritance

Let’s take a different view and look at an object’s methods from a reusability perspective. In Implementation and usage - Generalized code we noted that implementation logic is tied to a fixed internal state, a single implementation. Any additional method that is (unnecessarily) implemented, will not be reusable unless we inherit from this object. Most object-oriented programming languages (at least those based on the fundamental notion of a class hierarchy) do not support multiple inheritance. That means that if you, at some point, decide to inherit from a different class in order to reuse earlier methods that are already provided by the object, you are closing the door to any future inheritance option.

Direct field access

The internal state of objects contain fields. These fields can be accessed directly or they can be accessed through so-called accessor methods. Depending on the required consistency guarantee, fields can be made publicly accessible. (This is not customary in Java, however it is more prevalent in Go.) Or in case we need to pose restrictions in order to preserve consistency, we can use accessor methods to provide a controlled partial access. We discuss this topic in greater detail in the article on accessor methods.

Another part of fields is their access from implementation logic. Implementation logic is already part of the object and as such has privileges to access internal state. Implementation logic will typically access fields directly. For implementation logic to access fields through the use of methods only makes sense for convenience. It should never be the case that a “convention” exists that says that you “shouldalways access certain fields through such an (accessor) method for the sake of preserving some kind of consistency. This is a clear warning sign, as this leverages convention over the type system to “enforce” constraints. The type system should always be leveraged (if possible) to strictly enforce these constraints.

Field consistency

A significant part of designing an object, is to ensure consistency. In the article on accessor methods we go into various types of dependencies and how they influence the amount of restrictions we need to pose on field access. The restrictions are necessary to preserve consistency.

The consistency of individual fields should be preserved by the object itself. By definition of an object, we should assume that the object preserves its own consistency. However, we can still have stricter requirements than the requirements imposed by the object. We can make use of decorators to further restrict the input and output values that we allow to reach the object. We can do all this without writing new methods on the encompassing object.

If the object itself has a stake in the way the object is used, for example to prevent certain activities, then we do not expose the field. Exposing could mean either as public field, or through an accessor method. Instead, we provide the necessary methods such that the decorators can verify method calls before passing them on to the field.

Reasons for an object to rely on field state:

Nested, implicit consistency constraints

In case of “conventions” for working with fields, this is a clear indicator that there exists (although currently only implicitly) a subset of 1 or more fields that should represent its own object. This subset already represents a separate concept, as is indicated by “convention” on how fields should “cooperate”:

Such implicit constraints only refer to a subset of fields in the internal state. These indicators signal the existence of another concept, one that should be extracted into its own object. This newly extracted object can then be referenced from the internal state. With the definition of the new type, we can enforce previously implicit, “by convention” constraints, transparently apply interpretation constraints and enforce usage requirements such as in what state an object should be in.

We relieve the internal state of a hidden “secondary concern” of managing this implicit object, and we improve reusability as this object can be reused.

A nice guideline that I heard about in a talk - I don’t remember which talk it was exactly, but I think it was by Kevlin Henney - says that: “fields in an object should change at the same time”. A reason to change one field, should also be a reason to change other fields in the same object. This demonstrates the high cohesion that we strive for in a single object.

What if extending functionality requires privileged access?

The rules at the beginning of this chapter state that we should not add internal state in order to implement new methods. The internal state should be such that it sufficiently expresses the concept that is being represented and implementation logic should only use existing internal state in order to provide the public API. We can modify or extend on an object using well known patterns such as decorators, adapters, bridges, etc. Multiple objects can be combined into a larger more elaborate object by composition. Note that composition, unlike inheritance, is not limited in anyway. It can be applied repeatedly. Many object can be composed into one larger object, as opposed to maximally 1 parent class as is often the case of inheritance.

The exception to this rule is when we require access to internal state (i.e. not accessible through the public API) in order to extend an object. There are few cases where we clearly influence the original implementation:

  1. Completing an abstract class.
    We providing a first implementation for methods that are defined as abstract.

  2. Modifying an existing method by overriding a previously implemented method.
    This method may be called from within implementation logic in the original class thus influencing the original behavior through the newly provided overriding implementation logic. A secondary influence is possible too. Through a subsequent return to the original implementation logic, the influenced/altered internal state may result in different behavior in the original implementation logic as well.

  3. Protected methods provided for use by implementation logic only.
    These methods are intended for use by inheriting classes. They provide a guided access into the internal state that is not intended for usage logic. As noted in Curiosity of access modifiers, these methods are also accessible by usage logic as long as it resides within the same package. This may not always be the intention, but is implied by using the access modifier protected.

The 1st and 2nd example influence internal state by proxy. The 3rd is provided privileged access through a restricted feature. These are the clearest cases for inheritance where we depend on internal state access, even if indirectly. Other examples are already significantly less convincing. Most of these examples are easily solved using composition. By far, most extensions to an object can be done without the need for privileged access.

Reduced use of statics

The rules defined in the previous chapter state that we cannot allow for methods that do not access internal state. We can also apply this rule to static methods. We have to clarify though, since static methods are also used for utility methods. In this section, I am solely referring to static methods for the object that implements our concept, which means there exists internal state. We exclude utility methods in this section.

As with ordinary methods, we should only allow static methods that leverage internal state of the object. Or in line with static methods, this also holds for private constants. Static methods should provide some service to the user in which they leverage the privileged access to internal state.

An example of this is Java’s Optional. Optional provides static methods that support in the construction of instances. empty() provides an “empty” Optional, and because a static method is used it is possible to optimize for memory efficiency, as every “empty” Optional represents the same concept. Similarly, the of(...) method creates an Optional instance with the provided data inside, or throws an exception if the provided data was null. Both static methods exist to aid in construction. The actual constructor of Optional is private to preserve control on how Optional is instantiated.

Another example of this is the Singleton pattern. We use a static method to access the privately held instance. The static method ensures that a single instance, the same instance, is provided every time one is requested.

Tell, don’t ask

Lastly, there is the notion of “tell, don’t ask”. This principle is a guideline that explains to you to call methods on an object and pass on the necessary data, essentially telling the object to do the required data manipulations, instead of the opposite: querying an object for certain data and doing the manipulation yourself. Using this principle to explain the preservation of internal state consistency, is quite obvious. However, the advantage becomes less obvious when applying this for more than the goal of preserving state consistency. How much do you ask of an object? Where do you stop?

Following this philosophy very rigidly would cause objects to grow very large. You must provide methods for any use case where the object is involved. Additionally, this breaks down for given types, since these cannot be modified to include all of these “actions” that you would tell an object to do. On the other hand, if you do follow this principle and you have added all of the necessary methods, then you will see that this is one of the main causes for objects to very large.

Personally, either I really do not understand “Tell, don’t ask”, or there is nothing to it other than the mechanism for preserving internal state consistency.

References on “Tell, don’t ask”:

This post is part of the Object Oriented Programming series.
Other posts in this series: