0

I have a collection of view objects. Each one of these objects must 100% implement the following interface:

/**
 * Interface that each base ViewBlock object must implement.
 */
interface ViewBlockInterface extends CosmeticInformationInterface, ScriptInterface
{

    public function getBlockName();
}

Now, here's 2 interfaces that some of these objects might choose to implement:

use Interfaces\StyleInterface;
use TemplateInterface;

As such, an object must be of type ViewBlockInterface but can be either StyleInterface or TemplateInterface or both, meaning that we can potentially have a case where an object is all 3 at the same time.

Here's my problem: in my generator (that ingests all these objects), I have no clean way of handling each interface. I have to rely on code like:

if( $object instanceof ScriptInterface) {
    //Do some stuff that should only happen if this check passes.
}
..
..
//Do the same thing for N interfaces

Is there no pattern, way of architecturing my objects to escape this?

I can re-write this any way I want without a problem.

4
  • 1
    Why not implement the logic that you do inside the if conditions inside the objects instead?
    – Did
    Commented Jan 9, 2020 at 9:07
  • Just add another method function execute() in the interface that requires to implement the logic in the clients and then in your generator you just call ...->execute();
    – Did
    Commented Jan 9, 2020 at 9:10
  • Can you add the other interfaces as well? And how are these objects going to get used? What are the purposes of each interface? Commented Jun 7, 2020 at 13:08
  • Your issue could perhaps be solved by the Visitor design pattern. This however depends on the functionality you're after.
    – Andy
    Commented Jun 8, 2020 at 5:59

2 Answers 2

1

Is there no pattern, way of architecturing my objects to escape this?

The good news is, there is. The slightly worse news is that it requires you to change how you view your types/classes interfaces and how they represent the logic you need them to perform.

The main problem you're battling with here is that your approach is something that is not really how OOP should generally be approached. The best way to describe the approach I mean is:

I have a [thing], now I want to figure out what this [thing] is

In OOP, your "things" are inherently defined by their internal structure, i.e. their type. Weakly typed languages don't enforce specific types at every turn, but even in a weakly typed language you are expected to know the kind of content you can find in the object you pass. You will write code like this:

void DoSomething($myObj) {
    $foo = $myObj.MyProp
}

...which proves the point that you knew that myObj would have a MyProp property. The language didn't make you specify the exact type in the method parameter, but there was an implicit expectation that you know what you'll be passing into the method.

Your intended code is exactly doing this kind of figuring out:

// I have a $object, now I want to figure out what this $object is
if( $object instanceof ScriptInterface) {
    //Do some stuff that should only happen if this check passes.
}

And it also very quickly show you how this will inevitably devolve into a violation of the Open-Closed Principle:

//Do the same thing for N interfaces

To summarize the linked page, OCP is violated when you start having an ever-expanding list of snippets that you always need to remember to adjust (e.g. when you develop interface N+1 in the future).
This leads to forgetfulness, which leads to runtime errors and your old code (with only N implementations) not being able to handle any N+1 interface objects. Sometimes, that is a giant roadblock that leads to high-pressure just-after-the-release bugfixes, which no one likes.

Maybe it helps if I show a diagram:

                             ┌──────────────┐
                       ┌─────┤      A1      │
                       │     └──────────────┘
┌──────────────────┐   │
│    MyService     ├───┘
├──────────────────┤         ┌──────────────┐
│A1-handling logic ├─────────┤      A2      │
│A2-handling logic │         └──────────────┘
│A3-handling logic ├───┐
└──────────────────┘   │
                       │     ┌──────────────┐
                       └─────┤      A3      │
                             └──────────────┘

If you develop an A4 tomorrow, then you're going to need to remember that MyService needs to be updated with A4-handling logic. That might be easy to remember in a tiny codebase, but in an enterprise-grade one, it is asking for trouble.

Keep in mind that clean coding practices are principles. In tiny codebases, the problem they solve may be trivial to keep track of and deal with, but the larger the codebase gets, the more impossible it becomes to track this problem. The main advice is to remain clean no matter the size of the codebase - plenty of people have been burned making shortcuts "just for now" and then getting stuck when the codebase grew.

The better approach here is to tie the new type to the new logic in a way that you can't forget it:

                                         ┌─────────────────┐
                                         │     A1 : IA     │
                                 ┌───────┼─────────────────┤
                                 │       │A1-handling logic│
                                 │       └─────────────────┘
┌──────────────────┐             │
│    MyService     │             │       ┌─────────────────┐
├──────────────────┤     ┌───────┴┐      │     A2 : IA     │
│                  ├─────┤   IA   ├──────┼─────────────────┤
│    IA.Handle()   │     └───────┬┘      │A2-handling logic│
│                  │             │       └─────────────────┘
└──────────────────┘             │
                                 │       ┌─────────────────┐
                                 │       │     A3 : IA     │
                                 └───────┼─────────────────┤
                                         │A3-handling logic│
                                         └─────────────────┘

The key things to notice here are:

  • IA acts as a common interface between the A types, so that all A types can be handled the same way by a consumer. In this case, it contains a Handle() method which MyService can call, but the A classes themselves get to (individually) choose what to do when Handle() is called.
  • MyService no longer knows any of the specific types involved. It only works with IA. Therefore, MyService is capable of handling any IA derived type - even if it doesn't exist yet today.
  • Each A class is aware of its own handling logic. Which makes sense, since types like these are created specifically because you have a particular role in mind. Therefore, we define the role in the type itself.
  • Compare both diagrams. Now imagine that a new A4 has to be developed. How does that impact the diagram?
    • In the first diagram, you need to both create an A4 and update MyService.
    • In the second diagram, all you need to do is create an A4 (which implements IA), and all the rest will be taken care of for you, because the code you wrote was change-friendly and didn't need to be reworked.

You haven't really mentioned what you intend to do with your specific types, so it's very hard to distill a concrete example of an interface that would suit your needs here.

The best advice I can give right now is to develop your common interface (IA) in a way that its implementations can all be handled the same way. For example:

interface IModule
{
    public function getName();
    public function getVersion();
    public function WriteToLog($log);
}

Every module has a name, a version number, and it is able to perform a specific job (writing to a $log). However, each module gets to decide on its own:

  • What its name is
  • How to calculate its version number
  • If/how it will write a message, and what kind of message, into $log

Your ModuleService, which handles all kinds of modules, can now be written to only handle IModule objects, and can blindly trust that you've made sure that all your modules correctly implement the IModule interface. This ensures that you don't have to constantly update your ModuleService logic whenever a new module is developed, as long as the IModule interface does not need an update.

0

Interfaces actually behaves like abstract methods. So, there is no other way of not calling the object from implemented interface in that class. You may have seen that methods are public in interface. The class that implements the interfaces must define all the methods that they inherit from the interfaces.

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.