首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >这会违反Liskov替代原则吗?

这会违反Liskov替代原则吗?
EN

Software Engineering用户
提问于 2021-06-02 15:44:48
回答 4查看 441关注 0票数 2

假设我有一组来自“旧系统”的对象,我希望将这些对象转换为一组新的相应类。每个特定的类都有自己的转换方式。

所以我有这个:

代码语言:javascript
运行
复制
interface OldInterface {}
class OldA implements OldInterface {}
class OldB implements OldInterface {}

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

我的转换器接口如下所示:

代码语言:javascript
运行
复制
class NotConvertibleException extends \RuntimeException {}

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

一个实际的转换器(例如,对于一个对象)如下所示:

代码语言:javascript
运行
复制
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数组作为其参数,为每个项找到适当的转换器,然后返回转换后的结果数组:

代码语言:javascript
运行
复制
/********************** 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()]));
EN

回答 4

Software Engineering用户

回答已采纳

发布于 2021-06-02 22:25:14

与其让律师讨论这是否违反了Liskov替代原则,不如让我们看看这个接口究竟可以用于什么。

在注释中,给出了在服务中使用接口的示例:

代码语言:javascript
运行
复制
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): UOldToNewConverterInterface实例。这是否真的有用,这在某种程度上是没有意义的,因为不管怎么说,你都是在一种没有这种功能的语言中工作。

作为文档,接口可能有一定的价值:它是一种以某种方式标记所有这些转换器的方法。但是,由于无法指定不同转换方法的实际类型契约,损失要比获得的损失多得多。

因为您不需要一个接口来调用PHP中的方法(知道$foo的类型实际上不会改变PHP调用$foo->convert($bar)的方式),所以您可以有一个完全空的接口,它只是标签转换器,根本不指定任何方法。接口可能会与其他东西一起结束,这些东西确实有一个有用的契约,比如function supports(OldInterface $old): bool

显然,使用instanceof通常是一种代码气味。那么,在这种特殊情况下,在不使事情变得过于复杂的情况下,有什么好办法可以避免呢?

简单:不要强迫转换器相关。对于转换器的外观有一个编码约定,对它们的名称有一个命名约定。为作业创建正确的转换器,并在任何地方使用强类型合同。

如果您不喜欢依赖任何地方都不强制的命名方法的想法,您可以将转换器表示为可调用。然后,您可以拥有某种类型的注册表,该注册表给出一个对象,返回一个可调用的可转换对象。也许转换器接口将以function getCallable(): callablefunction getCallableFor(OldInterface $old): callable结束。

票数 3
EN

Software Engineering用户

发布于 2021-06-02 22:07:52

In

我们可以说,由于接口定义中预见到的例外情况,这个设计是符合LSP的。然而,这一理论分析具有误导性,因为它所依据的是一份与预期不符的无用合同。在现实中,一般情况下,您的场景不允许具有有意义的结果的可替换性。

一些更多的参数

如果AConverterLSP作为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参数。

票数 2
EN

Software Engineering用户

发布于 2021-06-03 09:44:27

这是一个技术犯规,但基本上是可以原谅的。

虽然这表面上是在做LSP示例通常会证明的事情,但在这里构建映射器时,需要知道具体类型是少数几种情况之一,而向上转换实际上是一种相当合理的方法。

话虽如此,这里还是可以进行一些调整,以进一步改进它,但这在一定程度上取决于您的业务需求,以及在现有遗留代码中实现这一点有多难。我无法解释这一点。

而且,这是否违反了LSP,即使基本方法的契约明确指出,如果它不能转换给定的对象,它可能会抛出一个NotConvertibleException?

当然,如果合同没有漏洞,要求它抛出异常,那就更理想了。然而,有时这是不可行的。

您的合同规定,不兼容的值有一个已知的漏洞,这会导致异常。这意味着接口“承担”了不完美的设计,从而使AConverter从被裁定为违反规则的行为中解放出来。

最好不要有那个洞,但是当你不能让它消失的时候,公开和明确地承认是你能做的最好的,这就是你在这里所做的。

那么,在这种特殊情况下,在不使事情变得过于复杂的情况下,有什么好办法可以避免呢?

避免这种情况的唯一方法是这里不使用接口类型,而是使用具体类型。

这里有一点概念上的冲突。纯LSP要求一个基类型的所有实现(对于任何基本类型处理逻辑)之间的完全互换性。

因此,如果您构建了一个IOldInterface->INewInterface映射(和契约),就意味着您可以将IOldInterface的任何实现转换为INewInterface的任何现有实现。你显然不是,所以你的问题。

我要重新调整你的逻辑,以便更容易地解释这里的各个部分,但最后,它将基本上是相同的东西。另外,很抱歉使用C#语法,但我不是PHP。

为了尽可能地清理这一点,我建议不要在任何位置使用接口类型,因为您清楚地意识到,并不是所有的接口实现都同样受欢迎。最清晰的位置是AConverter,它在名字中。

代码语言:javascript
运行
复制
public class AConverter
{
    public NewA Convert(OldA a)
    {
        ...
    }
}

可以说,这个具体的AConverter是您所需要的。您将从一个对象转换为另一个对象。但是,我很有信心,在您的代码库中,代码库只适用于接口类型,而不是具体类型,因此它显然无法区分一种具体类型和下一种类型。

这突出了上下文边界。域显然能够通过其基本类型处理所有事情,但是映射逻辑本身就不能这样做(因为完全正常的原因,即对象实例化)。您不想重写整个域(也不应该重写),但同时,您也不想玷污您自己的映射上下文。

我在这里要做的是创建一个中介,它充当从接口到具体类的翻译。这实际上是您已经在做的事情,但是不是在AConverterBConverterCConverter、.中传播,而是将它放在一个类中。

代码语言:javascript
运行
复制
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是与大型预先存在的域兼容所需的最小数量的违规行为。这里的假设是,域不可挽回地处理接口类型,并且您无法或不愿意重写域逻辑。

在映射有界的上下文中,除了这个“网关”类之外,其他所有东西都完全附着在实心上。网关类作为一个捕获-所有的事情,你根本无法避免,因为域已经建立的方式,它是。

但是,由于这个网关“采取了各自的转换器”,这会使您的转换器更加牢固,这将使您的转换器在可测试性、可维护性、可读性、.而且,由于映射的真正“肉”将存在于这些转换器中,简化转换器本身(即主肉周围的一切)将是一个值得欢迎的改进。

票数 2
EN
页面原文内容由Software Engineering提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://softwareengineering.stackexchange.com/questions/426964

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档