假设我有一组来自“旧系统”的对象,我希望将这些对象转换为一组新的相应类。每个特定的类都有自己的转换方式。
所以我有这个:
interface OldInterface {}
class OldA implements OldInterface {}
class OldB implements OldInterface {}
interface NewInterface {}
class NewA implements NewInterface {}
class NewB implements NewInterface {}我的转换器接口如下所示:
class NotConvertibleException extends \RuntimeException {}
interface OldToNewConverterInterface
{
/**
* @throws NotConvertibleException
*/
public function convert(OldInterface $old): NewInterface;
}一个实际的转换器(例如,对于一个对象)如下所示:
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;
}
}显然,使用instanceof通常是一种代码味道。那么,在这种特殊情况下,在不使事情变得过于复杂的情况下,有什么好办法可以避免呢?
而且,这是否违反了LSP,即使基本方法的契约明确指出,如果它不能转换给定的对象,它可能会抛出一个NotConvertibleException?我猜这是一个“是”(因为它仍然会增加先决条件),但只是为了以防万一。[编辑:看起来像有些人会说这不是。]
请注意,这个主题已经讨论过这里了,但我想看看答案如何适用于更具体的情况(因为这个问题非常普遍)。
编辑-下面是一个可能需要这样一个接口的示例:一个MassConverter服务,它使用一个OldInterface数组作为其参数,为每个项找到适当的转换器,然后返回转换后的结果数组:
/********************** 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()]));发布于 2021-06-02 22:25:14
与其让律师讨论这是否违反了Liskov替代原则,不如让我们看看这个接口究竟可以用于什么。
在注释中,给出了在服务中使用接口的示例:
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
}
}问题是,服务没有从接口中学到任何有用的东西。最后可能会有一个只知道如何转换$converter实例的OldDogInterface,以及一个只实现OldHelicopterInterface的$old。
事实上,由于总是抛出NotConvertibleException的实现将是兼容的,服务通过契约所知道的唯一的事情就是必须捕获它,而不是一个内置的“方法未找到”错误。
在具有泛型的语言中,可以使SomeService依赖于声明方法convert(T $old): U的OldToNewConverterInterface实例。这是否真的有用,这在某种程度上是没有意义的,因为不管怎么说,你都是在一种没有这种功能的语言中工作。
作为文档,接口可能有一定的价值:它是一种以某种方式标记所有这些转换器的方法。但是,由于无法指定不同转换方法的实际类型契约,损失要比获得的损失多得多。
因为您不需要一个接口来调用PHP中的方法(知道$foo的类型实际上不会改变PHP调用$foo->convert($bar)的方式),所以您可以有一个完全空的接口,它只是标签转换器,根本不指定任何方法。接口可能会与其他东西一起结束,这些东西确实有一个有用的契约,比如function supports(OldInterface $old): bool。
显然,使用instanceof通常是一种代码气味。那么,在这种特殊情况下,在不使事情变得过于复杂的情况下,有什么好办法可以避免呢?
简单:不要强迫转换器相关。对于转换器的外观有一个编码约定,对它们的名称有一个命名约定。为作业创建正确的转换器,并在任何地方使用强类型合同。
如果您不喜欢依赖任何地方都不强制的命名方法的想法,您可以将转换器表示为可调用。然后,您可以拥有某种类型的注册表,该注册表给出一个对象,返回一个可调用的可转换对象。也许转换器接口将以function getCallable(): callable或function getCallableFor(OldInterface $old): callable结束。
发布于 2021-06-02 22:07:52
我们可以说,由于接口定义中预见到的例外情况,这个设计是符合LSP的。然而,这一理论分析具有误导性,因为它所依据的是一份与预期不符的无用合同。在现实中,一般情况下,您的场景不允许具有有意义的结果的可替换性。
如果AConverter将LSP作为Converter的一个子类型,那么无论何时需要Converter对象,您都可以使用AConverter对象,并且程序仍应遵守其承诺。
对于每一种类型的"合同“,LSP更精确地表达了这一点:前提条件不应加强,后置条件不应削弱,必须保持不变(历史规则在您的示例中似乎不相关)。
铭记这一点:
AConverter的前提条件似乎得到了加强,因为它只接受OldInterface的一些特殊情况作为参数,并抛出所有其他参数的异常。Converter合同没有做出任何承诺,因为转换例外是可能的结果的一部分,并且没有做出更具体的承诺。更仔细地审视这些合同,就会支持遵守的想法:
Converter:前提条件:convert()输入参数必须由任何OldInterface生成。后置条件:返回一个NewInterface对象或抛出异常不变量: n.a。AConverter:前提条件:convert()输入参数必须由任何OldInterface生成。没有强化的前提条件;好的!后置条件:如果输入对象是一个NewAInterface对象,或者抛出异常,则返回一个OldAInterface对象。这看起来像是一个强化后条件;ok不变: n.a;ok。因此,从理论上讲,它是符合LSP的。
然而,在实践中,Converter根本没有提供任何合同,而AConverter提供了至少有一些承诺的合同。那么,合同是否真的与没有合同相符呢?
我们很容易感觉到有些地方不对劲。LSP的目的是促进可替代性,也就是说,如果某物对Converter有效,它将对AConverter起作用。但在这里,没有替代的余地。类型之间只有兼容性。因此,在现实中,我可以不使用“成功”的AConverter参数,在任何地方都可以使用Converter参数。
发布于 2021-06-03 09:44:27
虽然这表面上是在做LSP示例通常会证明的事情,但在这里构建映射器时,需要知道具体类型是少数几种情况之一,而向上转换实际上是一种相当合理的方法。
话虽如此,这里还是可以进行一些调整,以进一步改进它,但这在一定程度上取决于您的业务需求,以及在现有遗留代码中实现这一点有多难。我无法解释这一点。
而且,这是否违反了LSP,即使基本方法的契约明确指出,如果它不能转换给定的对象,它可能会抛出一个NotConvertibleException?
当然,如果合同没有漏洞,要求它抛出异常,那就更理想了。然而,有时这是不可行的。
您的合同规定,不兼容的值有一个已知的漏洞,这会导致异常。这意味着接口“承担”了不完美的设计,从而使AConverter从被裁定为违反规则的行为中解放出来。
最好不要有那个洞,但是当你不能让它消失的时候,公开和明确地承认是你能做的最好的,这就是你在这里所做的。
那么,在这种特殊情况下,在不使事情变得过于复杂的情况下,有什么好办法可以避免呢?
避免这种情况的唯一方法是这里不使用接口类型,而是使用具体类型。
这里有一点概念上的冲突。纯LSP要求一个基类型的所有实现(对于任何基本类型处理逻辑)之间的完全互换性。
因此,如果您构建了一个IOldInterface->INewInterface映射(和契约),就意味着您可以将IOldInterface的任何实现转换为INewInterface的任何现有实现。你显然不是,所以你的问题。
我要重新调整你的逻辑,以便更容易地解释这里的各个部分,但最后,它将基本上是相同的东西。另外,很抱歉使用C#语法,但我不是PHP。
为了尽可能地清理这一点,我建议不要在任何位置使用接口类型,因为您清楚地意识到,并不是所有的接口实现都同样受欢迎。最清晰的位置是AConverter,它在名字中。
public class AConverter
{
public NewA Convert(OldA a)
{
...
}
}可以说,这个具体的AConverter是您所需要的。您将从一个对象转换为另一个对象。但是,我很有信心,在您的代码库中,代码库只适用于接口类型,而不是具体类型,因此它显然无法区分一种具体类型和下一种类型。
这突出了上下文边界。域显然能够通过其基本类型处理所有事情,但是映射逻辑本身就不能这样做(因为完全正常的原因,即对象实例化)。您不想重写整个域(也不应该重写),但同时,您也不想玷污您自己的映射上下文。
我在这里要做的是创建一个中介,它充当从接口到具体类的翻译。这实际上是您已经在做的事情,但是不是在AConverter、BConverter、CConverter、.中传播,而是将它放在一个类中。
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();
}
}这仍然是你在一开始就遇到的同样的违规行为。除此之外,还违反了OCP。但是,这是合理的,因为this是与大型预先存在的域兼容所需的最小数量的违规行为。这里的假设是,域不可挽回地处理接口类型,并且您无法或不愿意重写域逻辑。
在映射有界的上下文中,除了这个“网关”类之外,其他所有东西都完全附着在实心上。网关类作为一个捕获-所有的事情,你根本无法避免,因为域已经建立的方式,它是。
但是,由于这个网关“采取了各自的转换器”,这会使您的转换器更加牢固,这将使您的转换器在可测试性、可维护性、可读性、.而且,由于映射的真正“肉”将存在于这些转换器中,简化转换器本身(即主肉周围的一切)将是一个值得欢迎的改进。
https://softwareengineering.stackexchange.com/questions/426964
复制相似问题