Better Programming

Advice for programmers.

Follow publication

Insights for Dealing With PHP OOP Limitations When Keeping Specific Implementations at the Edges of a Software

Lucas Pereyra
Better Programming
Published in
7 min readMay 25, 2022
Photo by Ben Griffiths on Unsplash

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 ExecutionContext 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 the processResult 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 the ExecutionContext 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!

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Lucas Pereyra
Lucas Pereyra

Written by Lucas Pereyra

Software Engineer specialized in backend development, performance optimizations and abstraction-guided development. Enjoy my writings and let's connect!

No responses yet

Write a response