2

Say I have a set of objects from an "old system" that I want to convert to a newer set of corresponding classes. Each specific class has its own way of being converted.

So I have this:

interface OldInterface {}
class OldA implements OldInterface {}
class OldB implements OldInterface {}

interface NewInterface {}
class NewA implements NewInterface {}
class NewB implements NewInterface {}

My converter interface would look like this:

class NotConvertibleException extends \RuntimeException {}

interface OldToNewConverterInterface
{
    /**
     * @throws NotConvertibleException
     */
    public function convert(OldInterface $old): NewInterface;
}

And one actual converter (say, for A objects) would look like this:

class AConverter implements OldToNewConverterInterface
{
    public function convert(OldInterface $old): NewInterface
    {
        if (!$old instanceof OldA) {
            throw new NotConvertibleException(
                sprintf('This converter cannot convert objects of type %s.', get_class($old))
            );
        }

        $new = new NewA(/* ... */);
        // convert code goes here

        return $new;
    }
}

Now obviously, using instanceof is more often than not a code smell. So what would be a good way to avoid it in this specific case, without making things overly complex?

Also, is this violating LSP even though the base method's contract explicitly indicates that it may throw a NotConvertibleException if it's not able to convert a given object? I'm guessing that's a yes (as it still strenghtens preconditions), but asking just in case. [edit: seems like some people would say it isn't.]

Note that this topic has already been discussed here but I would love to see how the answer applies to a more concrete case (as that question was very general).

Edit - Here's an example that might warrant the need to have such an interface: a MassConverter service that takes an array of OldInterface as its argument, discovers the appropriate converter for each item, then returns the converted result array:

/********************** OBJECTS **********************/

interface OldInterface {}
class OldA implements OldInterface {}
class OldB implements OldInterface {}

interface NewInterface {}
class NewA implements NewInterface {}
class NewB implements NewInterface {}

/********************** CONVERTERS **********************/

interface ConverterInterface
{
    public function supports(OldInterface $old): bool;
    public function convert(OldInterface $old): NewInterface;
}

class AConverter implements ConverterInterface
{
    public function supports(OldInterface $old): bool
    {
        return $old instanceof OldA;
    }

    public function convert(OldInterface $old): NewInterface
    {
        return new NewA();
    }
}

class BConverter implements ConverterInterface
{
    public function supports(OldInterface $old): bool
    {
        return $old instanceof OldB;
    }

    public function convert(OldInterface $old): NewInterface
    {
        return new NewB();
    }
}

/********************** CONVERTER DISCOVERER **********************/

interface ConverterDiscovererInterface
{
    public function discoverConverterFor(OldInterface $old): ?ConverterInterface;
}

class ConverterDiscoverer implements ConverterDiscovererInterface
{
    /**
     * @param ConverterInterface[] $converters
     */
    public function __construct(private array $converters) {}

    public function discoverConverterFor(OldInterface $old): ?ConverterInterface
    {
        foreach ($this->converters as $converter) {
            if ($converter->supports($old)) {
                return $converter;
            }
        }

        return null;
    }
}

/********************** MASS CONVERTER **********************/

class MassConverter
{
    public function __construct(private ConverterDiscovererInterface $converterDiscoverer) {}

    /**
     * @param OldInterface[] $oldObjects
     * @return NewInterface[]
     */
    public function massConvert(array $oldObjects): array
    {
        $newObjects = [];

        foreach ($oldObjects as $oldObject) {
            $converter = $this->converterDiscoverer->discoverConverterFor($oldObject);
            if ($converter !== null) {
                $newObjects[] = $converter->convert($oldObject);
            }
        }

        return $newObjects;
    }
}

/********************** SAMPLE **********************/

$converterDiscoverer = new ConverterDiscoverer([new AConverter(), new BConverter()]);
$massConverter = new MassConverter($converterDiscoverer);

var_dump($massConverter->massConvert([new OldA(), new OldB()]));
3
  • 1
    What's the purpose of the OldToNewConverterInterface interface? What benefit does it give you over, say, having an OldAToNewAConverterInterface interface, an OldBToNewBConverterInterface interface, and so forth? Commented Jun 2, 2021 at 17:46
  • @TannerSwett If we imagine a service class which has a method that takes an OldInterface object as parameter, and that sometimes needs to convert them to their new form. That service's constructor could be injected with an OldToNewConverterInterface instance without having to know which concrete object it will be. Here's a quick example. Also it would seem a bit of a shame to have dozens of Old<X>ToNew<X>ConverterInterface versions with mostly the same contract and the same idea (convert old -> new) without being able to enforce it across all.
    – Jeto
    Commented Jun 2, 2021 at 18:16
  • ^ actually ignore this, that's a bad example. See the bottom part of my edited question instead (also mentioned in below answer).
    – Jeto
    Commented Jun 2, 2021 at 23:33

4 Answers 4

3

Rather than playing lawyers over whether this violates the Liskov Substitution principle, let's look at what this interface could actually be used for.

In the comments you give this example of using the interface in a service:

class SomeService
{
    private OldToNewConverterInterface $converter;

    public function __construct(OldToNewConverterInterface $converter)
    {
        $this->converter = $converter;
    }

    public function doSomethingWithOld(OldInterface $old): void
    {
        // Do stuff

        // At some point, convert
        $new = $this->converter->convert($old);

        // Do some other stuff
    }
}

The problem is that the service doesn't learn anything useful from the interface. It might end up with a $converter that only knows how to convert instances of OldDogInterface, and a $old which only implements OldHelicopterInterface.

Indeed, since an implementation that always throws a NotConvertibleException would be compliant, the only thing the service knows by contract is that it has to catch that, rather than a built-in Error for "method not found".

In a language with generics, you could make SomeService<T, U> depend on an instance of OldToNewConverterInterface<T, U> which declared a method convert(T $old): U. Whether that would actually be useful is somewhat of a moot point since you're working in a language without that facility anyway.

As documentation, the interface possibly has some value: it's a way of labelling all these converters in some way. But you lose much more than you gain, by not being able to specify the actual type contract of the different convert methods.

Since you don't need an interface in order to call a method in PHP (knowing the type of $foo doesn't actually change how PHP calls $foo->convert($bar)) you can have a completely empty interface, that just labels converters without specifying any methods at all. The interface might end up with other things, which do have a useful contract, like function supports(OldInterface $old): bool.

Now obviously, using instanceof is more often than not a code smell. So what would be a good way to avoid it in this specific case, without making things overly complex?

Simple: don't force the converters to be related. Have a coding convention for what converters look like, and a naming convention for what they're called. Create the right converter for the job, and use strong type contracts everywhere.

If you don't like the idea of relying on a named method that's not enforced anywhere, you could represent the converters as callables. You could then have a registry of some sort, which given an object, returned a callable that could convert it. Perhaps the converter interface would end up with function getCallable(): callable or function getCallableFor(OldInterface $old): callable.

6
  • Very clear, thanks! My initial example didn't make much sense indeed, I got confused. To answer the question you were initially asking in the coments, how about this example of a mass converter making use of a discoverer service to find the appropriate converter to be use for each item in an array of OldInterface instances? How could that work without the existence of ConverterInterface?
    – Jeto
    Commented Jun 2, 2021 at 23:20
  • @Jeto Well, in PHP, that works just fine without an interface - method calls are all "duck typed" anyway - but I agree there would be value in declaring the supports() method in an interface. I've edited my answer to add a few more thoughts and ideas.
    – IMSoP
    Commented Jun 3, 2021 at 8:14
  • Interesting stuff! I like the callable idea, it would at least provide some minimal form of contract (as converters with an __invoke method, no matter its signature, could pass themselves as such). I understand there may be no flawless, rock-solid (OOP-wise) solution here until PHP gets generics, though. Thank you again for your time.
    – Jeto
    Commented Jun 3, 2021 at 9:19
  • "In a language with generics": PHP+PHPStan or PHP+Psalm
    – bdsl
    Commented Jun 3, 2021 at 20:52
  • 1
    @bdsl Yeah, I thought of mentioning that, but ultimately it seemed like too much of a digression: there's not really an obvious use for generics here, so extra complexity to add them seems unwarranted. I suppose a generic MassConverter<T,U> could wrap a single Converter<T,U>, but the one in the question determines all its types at run-time, so has nothing for the static analyser to analyse.
    – IMSoP
    Commented Jun 3, 2021 at 21:04
2

In short

One could defend that this design is LSP compliant thanks to the exceptions foreseen in the interface definition. However, this theoretical analysis is misleading since it is based on a useless contract that does not correspond to the expectations. In reality, there your scenario allows no subsituability with a meaningful result in general.

Some more arguments

If AConverter would comply with LSP as a subtype of Converter, then you would be able to use an AConverter object whenever a Converter object is expected and the program should still keep its promises.

LSP expresses this more precisely with a view to the "contract" of each type: preconditions shall not be strengthened, post conditions shall not be weakened, invariant must be preserved (the history rule seems not relevant in your example).

Keeping this in mind:

  • At first sight, it seems that the preconditions of AConverter is strengthened, since it accepts only some special cases of OldInterface as parameters, and throws an exceptions for all the others.
  • On the other side, one could claim that the Converter contract does not make any promises since a conversion exception is part of the possible outcomes and no more specific promise is made.

A closer look at the contracts supports the idea of compliance:

  • Converter:

    precondition: the convert() input parameter must by any OldInterface.
    postcondition: a NewInterface object is returned or an exception is thrown
    invariant: n.a.

  • AConverter:

    precondition: the convert() input parameter must by any OldInterface. There is no strengthening of precondition; ok!
    postcondition: a NewAInterface object is returned if the input object is an OldAInterface, or an exception is thrown. This looks like a strengthened post-condition; ok
    invariant: n.a; ok.

So, it is LSP compliant, in theory.

In practice however, Converter offers no contract at all, whereas AConverter offers a contract with at least some promises. So is a contract really compliant with the absence of a contract?

We can easily feel that something is not right. LSP aims to promote substituability, i.e. that if something works for a Converter, it will work for an AConverter. But here, there is no substituability. There is only a compatibility of types. So I can in reality not use "successfully" an AConverter argument, everywhere a Converter argument is expected.

1
  • Great detailed answer, thank you very much. Especially about the LSP, I was actually curious about it regardless of whether my proposed solution made sense.
    – Jeto
    Commented Jun 2, 2021 at 23:23
2

It's a technical foul, but mostly an excusable one.

While this is ostensibly doing the exact thing that LSP examples tend to demonstrate, as you are building a mapper here, needing to know concrete types is one of the few cases where upcasting is actually a reasonably well-justified approach.

That being said, things can be rejigged a bit here to further improve it, but this somewhat depends on your business requirements and how hard this is to implement in the existing legacy code. I can't account for that.

Also, is this violating LSP even though the base method's contract explicitly indicates that it may throw a NotConvertibleException if it's not able to convert a given object?

Well, it's definitely more ideal to have contract which doesn't have holes which requires it to throw exceptions. However, sometimes that is just not feasible.

Your contract stipulates that there is a known hole for incompatible values and that it leads to an exception. That means the interface "takes on" the imperfect design, thereby liberating AConverter from being ruled as in violation.

It's better not to have that hole, but when you can't make it go away, publically and explicitly acknowledging is the best you can do, and that's what you've done here.

So what would be a good way to avoid it in this specific case, without making things overly complex?

The only way to avoid this is by not using interface types here, but rather concrete types.

There is a bit of a conceptual conflict going on here. Pure LSP dictates the requirement of perfect interchangeability between all implementations of a base type (in regards to any base-type-handling logic).

Therefore, if you build a IOldInterface->INewInterface mapping (and contract), you are implying that you are okay with any implementation of IOldInterfacebeing converted to any existing implementation of INewInterface. You clearly are not, hence your question.

I'm going to reshuffle your logic a bit to more easily explain the individual pieces here, but at the end it'll be mostly the same thing. Also, sorry for the C# syntax but I'm no PHP dev.

In order to clean this up as best as you can, I suggest not using interface types in any location where you are acutely aware that not all implementations of that interface are equally welcome. The clearest location for that is AConverter, it's in the name.

public class AConverter
{
    public NewA Convert(OldA a)
    {
        ...
    }
}

Arguably, this concrete AConverter is all you need. You convert from one object to the next. However, I'm fairly confident that somewhere in your codebase, the codebase only works with the interface types, not the concrete types, and it is therefore patently unable to distinguish one concrete type from the next.

This highlights a contextual boundary. The domain clearly is able to handle everything by its base type, but your mapping logic inherently cannot do so (for a perfectly normal reason, i.e. object instantiation). You don't want to rewrite the entire domain (nor should you), but at the same time, you also don't want to sully your own mapping context.

What I would do here is create an intermediary, which acts as the translator from the interface to the concrete class. This is effectively what you were already doing, but instead of spreading it around in AConverter, BConverter, CConverter, ..., you are keeping it together in one class.

public class INewInterfaceConverter
{
    // Tip: inject these dependencies!
    private readonly AConverter _aConverter = new AConverter();
    private readonly BConverter _bConverter = new BConverter();
    private readonly CConverter _cConverter = new CConverter();

    public INewInterface Convert(IOldInterface o)
    {
        if(o is OldA)
            return _aConvertor.Convert(o);
        else if(o is OldB)
            return _bConvertor.Convert(o);
        else if(o is OldC)
            return _cConvertor.Convert(o);

        throw new NotConvertibleException();
    }
}

This is still the same kind of violation that you had in the beginning. On top of that, it's also an OCP violation. However, it is a justifiable one, because this is the minimal amount of violation required to be compatible with a large pre-existing domain. The assumption here is that the domain is irreparably working with the interface types, and you are not able or willing to rewrite the domain logic.

In your mapping bounded context, everything except this one "gateway" class is perfectly adhering to SOLID. The gateway class acts as a catch-all for the things that you simply cannot avoid due to the domain having been built the way that it is.

However, because this gateway "takes the sins" that the individual converters otherwise would, this makes your convertors more SOLID-adherent, which can lead to significant improvements in testability, maintainability, readability, ... And because the real "meat" of the mapping is going to live in these converters, simplifying the converters themselves (i.e. everything around the main meat) will be a welcome improvement.

3
  • Regarding "Whether you use generics is up to you" - it's not an option if you're using PHP, because PHP doesn't support generics; and unfortunately is unlikely to any time soon, since its type system is enforced almost entirely at run-time, and implementing generics that way throws up a lot of complexity and performance problems.
    – IMSoP
    Commented Jun 3, 2021 at 13:22
  • @IMSoP Thanks for the info! I had done a quick google but failed to see that the mention of generics wasn't listing an existing feature, but rather wishlisting it. I've removed the generics from the answer as it's not relevant to OP then.
    – Flater
    Commented Jun 3, 2021 at 17:40
  • Thanks for this very insightful answer.
    – Jeto
    Commented Jun 3, 2021 at 18:16
1

The solution we want here is this: we need to add a method to the old classes which converts from them to the new classes.

This works fine in an OOP system in which new methods can be easily introduced over existing classes.

Here is one I made myself, for TXR Lisp:

;; We have some old classes
(defstruct old-a ()
  datum)

(defstruct old-b ()
  datum)

;; We have some new ones
(defstruct new-a ()
  datum)

(defstruct new-b ()
  datum)

;; converting

;; convert a to new: just a new method on old-a

(defmeth old-a conv-new (me)
  ;; let us s say that to convert means to make
  ;; the new type object, and pass down the datum
  (new new-a datum me.datum))

;; convert b to new: likewise, on old-b.

(defmeth old-b conv-new (me)
  (new new-b datum me.datum))

Interactive:

1> (new old-a datum 42)
#S(old-a datum 42)
2> *1.(conv-new)  ;; ask obj from line 1 to convert itself
#S(new-a datum 42)
3> (new old-b datum 73)
#S(old-b datum 73)
4> *3.(conv-new)  ;; ask obj from line 3 to convert itself
#S(new-b datum 73)

Are you sure you can't just do this simple thing in PHP? That language doesn't exactly have a reputation for bondage and discipline.

If you're not allowed to do this, or perceive that you should not do this, then you're stuck inventing some secondary class, the "converter object" where the conversion logic lives.

In that case, because you have only single dispatch, the useless converter object "eats" the dispatch, leaving the object to be passed in an ordinary argument not subject to OOP dispatch.

The caller is then forced to come up with the correct converter for the correct object, which the converter sanity checks.

In that situation, the caller could instead be given an abstract factory. The converters could possibly be singleton objects not having to be created each time, so it's more of a library.

IConverter *conv = convLibrary.GetConverterFor(obj);

INewStyle = conv->Convert(obj);

GetConverterFor does all the instanceOf analysis, and comes up with the correct converter, which the caller can safely use on that same object, or other objects of the same type.

This seems to be saying "I want to be a non-OOP regular function":

INewStyle = ConvertToNew(obj); 

If we have a large collection of objects of the same type that we need to convert, there may be an efficiency gain in the former pattern, due to the possibility of getting a converter just once, and then applying it repeatedly, whereas ConvertToNew does the type switch each time it is called.

And of course this plain old function

ConvertToNew(obj); 

is just hankering after that

obj.ConvertToNew();

which for some reason we couldn't have.

1
  • Thanks. The reason I can't have old.convertToNew() in my case (but I should've probably mentioned it from the start) is that the objects in question are supposed to be "dumb" and not have any behavior, only data. Also, they shouldn't have any knowledge about the other system (old objects shouldn't know about new objects / how they work, and vice versa).
    – Jeto
    Commented Jun 5, 2021 at 23:09

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.