Insights for Dealing With PHP OOP Limitations When Keeping Specific Implementations at the Edges of a Software
A short guide to help your code go further
Keeping specific implementations at the edges of software is a well-known best practice in software development, and it usually works well as a heuristic for building powerful object-oriented structures and easy-to-maintain code.
The general idea refers to having classes know each other’s interfaces (e.g., which operations could be invoked on each one) without knowing their specific implementation or “data type.” For example, which specific class or subclass an object belongs to).
Normally, this design practice is reached by means of defining software dependencies in terms of interfaces, thus leaving some dependency injection mechanism to figure out which specific implementations should be used throughout the execution thread.
Specific implementations are often defined at the beginning of the execution thread, at the very first layers of the software, before entering the core components. We refer to these places as the “edges of the software,” which conform to the actual entry point of every execution of it.
Having made a boring theoretical OOP introduction, I’ve faced a specific problematic pattern many times during my few years of experience working as a PHP developer. Here I describe it and provide some insights that helped me to deal with it in different scenarios while trying to stick to the above design practice.
The Problem
The problematic pattern can be described as having a class that makes use of the result that another one provides. The result is defined in terms of a generic interface, so we may know some operations that can be invoked on the instance. However, the result also has some specific operations and data pieces that the resulting consumer wants to use.
The consumer should be able to work with different possible result instances by implementing different result processing algorithms, depending on each result’s type. How can we achieve this without spreading the implementation details through all the code?
I know this is a little tedious to understand, so let’s work on this with a concrete example using a Command design pattern setup:
Despite the number of files, this setup is easy to understand: there’s a Command
interface that returns a CommandResult
instance as the result of executing a specific command. We have an ExecutionContext
class that defines the general structure of a specific context.
A context would be responsible for handling (executing and processing its result) each command. We can see that each specific command result subclass holds different operations, but the ExecutionContex
t class is only aware of the CommandResult
interface. How could we handle each command result type accordingly?
This has been my nemesis on many occasions. The lack of polymorphic type hinting and generics in PHP makes it difficult to work under these scenarios; unlike Java, which has good native solutions for cases like these. Anyway, I could identify three useful approaches that allowed me to address this problem successfully in the past:
- Type casting the generic result into the expected type
- Using the
Visitor
pattern - Restructuring components design
Type Casting the Generic Result Into the Expected One
To be honest, this is my least preferred solution. Since we do know that behind a CommandResult
instance there will be a specific class instance, we could try to provide different implementations for each specific instance as follows:
The processResult
implementation asks which specific type the result has in order to handle it accordingly. Since each private processCommandResultX
method expects to receive a CommandResultX
instance as defined on its signature, our IDE can help us with autocompletion and type hinting within each of these methods.
The processResult
method could be defined directly in the base ExecutionContext
class, leaving the definition of each “processCommandResultX
” method to each subclass.
Moreover, the base class could provide default implementations for each specific method, along with a default implementation that would be executed in case no instanceof
statement catches a specific command result (though, we may want to throw an error in this case, warning us that we forgot to handle it).
A more powerful and flexible version would make use of the Chain of Responsibility pattern to define each specific processing as a chain steel. That way a steel could process only CommandResultA
instances, leaving all other result types to the following steels, and so on.
We could have steels either at the beginning or at the end of the chain that make extra processing for all types of results, for example, enabling data logging for each result no matter which type it is.
Although having many instanceof
checks and processing methods could be a little tricky and verbose, this first approach keeps the specific implementation code at the edges of the software, as long as we consider the execution context classes part of the edges. Adding a new command result type entails, therefore:
- Adding a new specific processing method in the
ExecutionContext
hierarchy, and implementing it in the child classes - Adding a new “
instanceof
” check in theprocessResult
method - When using a Chain of Responsibility, adding a new chain steel and maybe modifying some existing ones
Using the Visitor Design Pattern
By means of the Visitor
design pattern, we can reach a more elegant setup of components, as shown below:
Here, we let each specific result control how it should be processed: each ExecutionContext
has a collection of specific processing methods. The specific result should choose the appropriate one by implementing the processOnContext
method.
Elegance is given by not having to check for each result’s class type using instanceof
statements. However, this approach is not so different from the previous one. Adding a new command along with a new command result type would entail:
- Adding a new
processCommandResultX
method in theExecutionContext
base class, and implementing it on each concrete context subclass.
Restructuring the Design
If we turn the theoretical mode on again, we could think that this problem is merely a design problem, and thus, this current setup doesn’t work well due to some misconceptions and assumptions that were made wrongly.
First, ExecutionContext
was designed to process CommandResult
instances. But we’re trying to process CommandResultA
and CommandResultB
instances. Hence, we’re mixing two different levels of abstraction in the same class. At any point in the code, we should work at either a low level or a high level of abstraction, but not with both of them simultaneously.
Second, CommandResultA
defines a completely new method called getString
, which isn’t defined at the parent class level. The same happens with CommandResultB
. Moreover, both command result types aren’t interchangeable with each other.
We’re violating the Likskow’s Substitution Principle, and this comes from the assumption that CommandResultA
and CommandResultB
are kinds of CommandResult
, which in turn is not true since they have nothing in common. Neither share the same data fields nor have the same operations.
Noticing these design errors might bring up good insights to refactor and redesign the current solution. However, since I assume that two commands could return different data fields as their results, I’ll definitely want to handle them differently. Command result types vary along with the processing algorithms. Therefore, we could try to keep them both at the very edges of the software, as you can see below:
I’ve added a ProcessingStrategy
set of classes that define how each specific result type should be processed. The ExecutionContext
now is a core component and must be set as a processing strategy before sending it a command. We keep the result type processing at the system's edges using this approach since they’re only defined in the index.php
file.
This approach supposes having one specific processing strategy per command result type, although the processResult
method expects a CommandResult
instance instead of a specific result type (like CommandResultA
).
As a consequence, I explicitly check that the passed-in result’s type is the one I expect to receive. I use two approaches: ProcessingStrategyA
casts the result to a CommandResultA
, whereas ProcessingStrategyB
explicitly checks the type of the given result and relies on PHPDoc for autocompletion and type hinting.
Note that a processing strategy that handles both types of results could be provided, too. Thus, this is a more flexible approach than the others. Also note that if we pass an unexpected command result type to the execution context, it will throw up an error. A safer alternative would include some error handling and default behavioral mechanisms.
A type-relaxed variation of this approach would be one where the ExecutionContext
class uses a processingStrategy
without explicitly defining its expected type, which you can see below:
This version allows the ExecutionContext
to use anything that has a processResult
method as a valid processing strategy. By relaxing the type requirements, we can delete the inheritance of ProcessingStrategy
, thus explicitly saying that a ProcessingStrategyA
instance will only be able to process CommandResultA
instances, instead of CommandResult
ones. The main benefit here is that we get rid of instanceof
checks and type casting, and the restriction of how a processing strategy processes a result is made explicit.
Adding a new command result type with this layout would entail:
- Adding a new processing strategy that can deal with the new result type, though an existing one could be reused.
I hope this article was helpful. Thank you for reading, and stay tuned for more!
I’m Lucas, a seasoned backend engineer with a passion for designing scalable, efficient systems. I write about backend development, from best practices to the latest technologies. Let’s connect on LinkedIn to discuss more!