Object Oriented Programming: Implementation and usage - Generalized code
Tue, May 10, 2016 ❝Generalized code in the context of implementation and usage logic.❞Contents
In the previous article we defined the notions of implementation and usage (logic). We noted how implementation logic is allowed access to internal state, while any other logic can only use the object, i.e. access its public fields and methods.
This article extends on the previous article by looking at the way we can reuse logic, either usage or implementation logic. In this article we focus on internal state only. Expectations, which may be stored with the internal state inside the object, are there only for use and are not managed by the implementation logic.
A new perspective
We discussed how implementation logic is really tied to the internal state, while usage logic cannot access the internal state and relies completely on the public interface, the logic that is provided by the object. So, what is the consequence of this? What can we actually vary for each type of logic?
Implementation logic
Let’s start with implementation logic. Implementation logic exists to manage the internal state of the object. It’s primary function is to preserve state consistency after every execution. In order for the logic to work we generally allow implementation logic to temporarily make internal state inconsistent, as long as state consistency is restored as soon as the logic is finished executing. An implementation method is typically considered such a unit. At the start of the method’s execution, the internal state is consistent. Anywhere during execution it may be inconsistent. However, when the method is done executing, internal state consistency has to be restored. Keep in mind that even in the case of errors/exceptions we would need to ensure that state consistency is restored.
Now, notice how all the implementation logic itself needs to be consistent for a single type. The logic is typically divided into multiple methods. Each of these methods needs to work towards the same goal in internal state management. All methods need to have the same notion of (in)consistent state, otherwise one method might set a consistent state that another method would consider inconsistent and fail as a result.
To illustrate this with a quick example: a writer object writes to some resource. You need to be sure that the close
method closes the same resource that the open
method opened. And that the write
-method writes to that resource. It does not make sense to open a network connection, subsequently attempt to write to a printer, and then try to close an arbitrary file.
We depend on all of the implementation logic to manage the same internal state with a shared, common goal.
Usage patterns
The usage logic, on the other hand, is the complete opposite. Given some object, we access certain parts of its public interface. The exact implementation is not really relevant, as long as the public interface is available. Usage logic relies completely on provided methods (the implementation logic) to preserve correct internal state, while usage logic calls whatever function that is offered at its own convenience. (That is, within the bounds defined by the object.)
What we notice here is that usage logic is more of a usage pattern. The exact type is really not that important. It only relies on the public interface and that interface can be provided by any number of different objects. The usage pattern does not care whether a writer object writes to network, disk or printer, as long as the object functions when we subsequently call open
, write
and close
. As we noted before, we even rely on the implementation logic to preserve internal state consistency, so we do not even care what the open
method opens or the close
method closes. If the usage pattern fits with the object’s public interface, we are okay.
Internal state determines implementation logic
We said earlier that what is considered “implementation logic” is really only determined by whether or not it accesses internal state. This is simply because only implementation logic has strict requirements of the common goal and internal state consistency. As soon as the requirement of managing internal state disappears, logic reverts to being a usage pattern for an object. At that point, the exact object does not really matter anymore, as long as the public interface is the same/similar.
Abstraction from single type
To extend on the distinction between implementation and usage logic, we will look at how we can abstract implementation logic as well as usage patterns from a single type to a more general “common base case”.
Shared implementation
Let’s look at the analog for implementation logic first. As said before, implementation logic is closely tied to the internal state that it manages. This is also visible in the way that the various programming languages support inheritance. Especially in programming languages that provide generalization/specialization based on class hierarchy.
For implementation logic, we reuse code by selecting the common part of the internal state and corresponding implementation logic, and separating that as a shared type. We provide implementation logic that manages this subset of internal state, of course with all the consistency guarantees. This common part becomes a class of its own. This may be a class that is useful on its own, or it may clearly be an incomplete implementation. In the second case, it may contain unimplemented methods as these are specific to each implementation. In that case we make this class abstract. This is also known as the Template method pattern and is a pattern that relies on inheritance in order to complete the incomplete logic prepared by the pattern.
For each of the concrete implementations, we simply base it on the shared type, adding only those parts of the internal state and its corresponding implementation logic that are lacking in the base type. The resulting class is a fully implemented type, ready to be used.
Notice how we split up internal state but we never separate implementation logic from the corresponding internal state? Implementation logic is closely tied to internal state, as it represents the behavior corresponding to the state and it is responsible for the consistency guarantee.
Additionally, I intentionally chose to use the term “shared implementation”. Note that however many classes we derive from the (abstract) class in which we provide this shared implementation, we are always talking about the same base implementation. The “shared implementation” does not change for different derivatives. Variations only occur in the derivative’s own state management. Now, you might want to point out how we can override methods from the (abstract) base implementation. Even so, we must ensure that we preserve consistency.
Now compare this to the way we reuse usage patterns …
Usage contract
Similar to implementation logic, usage patterns can also be reused. Usage patterns take a different approach to logic reuse. As noted before, usage patterns do not really care about the exact implementation, as long as the public interface that it relies on is available. If we subsequently call open
, write
and close
on an object, we only rely on the object having these methods. We do not know or even care about how it implements the underlying behavior. The usage pattern is satisfied with knowing that it can address the object in a way that fits the pattern. It relates to the concept rather than a concrete type.
In that fashion, there is the notion of an ‘interface’. The interface is the usage contract that a type guarantees when it provides this interface. How you express that an interface is provided depends on the type of language. Sometimes this is explicitly mentioned. A significant number of the programming language take this approach, such as Java and C#. In other languages an interface is satisfied implicitly, such as Go. Or it is not stated at all and usage patterns just assume that the contract is there, such as Python.
In any case, the usage pattern is completely separated from the object that provides the usage contract. A usage pattern can be defined based on the usage contract, i.e. it is based on the interface, and only at runtime when it is clear which concrete type is provided, will the usage logic first “discover” what concrete type is used. Usage pattern and object may meet as late as at runtime.
This is of course a completely different approach to reusability compared to the shared implementation for implementation logic. While implementation logic is closely tied to the internal state it manages, usage patterns rely only on the “placeholder” usage contract, creating an abstraction from any specific type of object.
Distinguishing generalized usage from shared implementations
As noted in the previous section, generalized usage patterns rely on a usage contract to define what the expected behavior for an object is and what should be implemented to satisfy the usage contract. We generally define a usage contract by defining an Interface. Most languages allow us to implement multiple interfaces at the same time, since the interface itself does not provide any implementation that might conflict with that of other interfaces. It only provides the required method signatures. (And it documents required semantics as these cannot be expressed in syntax.)
Interfaces, at least in the Object Oriented Programming variation based on class hierarchies, are actually purely abstract classes. These purely abstract classes define what the class should look like, i.e. what kind of methods are expected, without providing any internal state. Having internal state would imply some sort of concrete implementation or restrict possible variations. Analogous with the basic implementation logic vs. usage logic article, the absence or presence of internal state distinguishes a usage contract from a shared (base) implementation.
A class hierarchy is not a necessary component for usage contracts. Go provides a notion of interfaces that does not rely on class hierarchy to determine whether or not an interface is implemented, i.e. a usage contract is provided. Instead it simply verifies that all defined method signatures are provided by the objects that pretend to implement the interface.
The requirement is:
- The availability of a usage contract.
This defines the public interface of an object. It is the primary mechanism that allows us to write a general usage pattern.
Furthermore, by ensuring full orthogonality from state (i.e., any kind of implementation), we maximize our ability to generalize and reuse a usage patterns:
- The absence of any internal state / implementation ensures that we are not biased toward any kind of implementation, thus fully applicable to any object that satisfies the usage contract. (Orthogonality)
A caveat for implicit interfaces
We mentioned before that in the case of Go, interfaces are implicit. This means specifically that when required methods are available, we assume that the interface is implemented. The advantage is that we can use any type that satisfies the interface. Even types that existed before the interface was defined. The disadvantage is that we simply assume that the semantics of the type perfectly match those of the interface. The semantics are never confirmed explicitly, nor are guaranteed to be preserved in case the implementation changes.