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()]));
OldToNewConverterInterface
interface? What benefit does it give you over, say, having anOldAToNewAConverterInterface
interface, anOldBToNewBConverterInterface
interface, and so forth?OldInterface
object as parameter, and that sometimes needs to convert them to their new form. That service's constructor could be injected with anOldToNewConverterInterface
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 ofOld<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.