C#界面投射违反Liskov替换原则

And*_*dre 6 c# solid-principles

我想指的是在之前使用的例子SO 与鸭和鸭电:

public interface IDuck
{
    void Swim();
}

public class Duck : IDuck
{
    public void Swim()
    {
        //do something to swim
    }
}

 public class ElectricDuck : IDuck
{
    public void Swim()
    {
        if (!IsTurnedOn)
            return;

        //swim logic  
    }

    public void TurnOn()
    {
        this.IsTurnedOn = true;
    }

    public bool IsTurnedOn { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

LSP的原始违规将如下所示:

 void MakeDuckSwim(IDuck duck)
    {
        if (duck is ElectricDuck)
            ((ElectricDuck)duck).TurnOn();
        duck.Swim();
    }
Run Code Online (Sandbox Code Playgroud)

作者的一个解决方案是将Logic放入电动鸭子的游泳方法中以使自己开启:

public class ElectricDuck : IDuck
{
    public void Swim()
    {
        if (!IsTurnedOn)
            TurnOn();

        //swim logic  
    }

    public void TurnOn()
    {
        this.IsTurnedOn = true;
    }

    public bool IsTurnedOn { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

我遇到过其他场景,可以创建支持某种初始化的扩展接口:

public interface IInitializeRequired
{
    public void Init();
}
Run Code Online (Sandbox Code Playgroud)

然后可以使用此界面扩展Electric Duck:

 public class ElectricDuck : IDuck, IInitializeRequired
{
    public void Swim()
    {
        if (!IsTurnedOn)
            return;

        //swim logic  
    }

    public void TurnOn()
    {
        this.IsTurnedOn = true;
    }

    public bool IsTurnedOn { get; set; }

    #region IInitializeRequired Members

    public void Init()
    {
        TurnOn();
    }

    #endregion
}
Run Code Online (Sandbox Code Playgroud)

编辑:扩展界面的原因是基于作者说,在游泳方法中自动打开可能会有其他不希望的结果.

然后,该方法而不是检查和转换为特定类型可以寻找扩展接口:

void MakeDuckSwim2(IDuck duck)
    {
        var init = duck as IInitializeRequired;
        if (init != null)
        {
            init.Init();
        }

        duck.Swim();
    }
Run Code Online (Sandbox Code Playgroud)

事实上,我使初始化概念更抽象,然后用TurnOn()方法创建一个名为IElectricDuck的扩展接口,这可能使我觉得这样做是正确的,但整个Init概念可能只存在因为电子鸭.

这是一种更好的方式/解决方案,还是伪装成LSP违规.

谢谢

Hei*_*nzi 5

这是伪装的LSP违规.您的方法接受IDuck,但它需要验证动态类型(无论是否IDuck实现IInitializeRequired)才能工作.


修复此问题的一种可能性是接受一些鸭子需要初始化并重新定义接口的事实:

public interface IDuck 
{ 
    void Init();

    /// <summary>
    /// Swims, if the duck has been initialized or does not require initialization.
    /// </summary>
    void Swim();
} 
Run Code Online (Sandbox Code Playgroud)

另一个选择是接受未初始化的ElectricDuck并不是真正的鸭子; 因此,它没有实现IDuck:

public class ElectricDuck
{  
    public void TurnOn()
    {  
        this.IsTurnedOn = true;
    }

    public bool IsTurnedOn { get; set; }  

    public IDuck GetIDuck()
    {
        if (!IsTurnedOn)
            throw new InvalidOperationException();

        return new InitializedElectricDuck();  // pass arguments to constructor if required
    }

    private class InitializedElectricDuck : IDuck
    {
        public void Swim()
        {
            // swim logic
        }
    }
}  
Run Code Online (Sandbox Code Playgroud)


Ale*_*aga 4

我仍然将你的最后一个例子视为 LSP 违规,因为从逻辑上讲,你正是这样做的。正如您所说,实际上没有初始化的概念,它只是作为一种黑客而编造的。

事实上,你的 MakeDuckSwim 方法不应该知道任何鸭子的具体细节(是否应该首先初始化,初始化后提供一些目的地,等等)。它只需让提供的鸭子游泳即可!

在这个例子中很难说清楚(因为它不是真实的),但看起来“上层”的某个地方有一个工厂或某个东西可以为你创造一只特定的鸭子。

你可能错过了这里工厂的概念吗?

如果有的话,那么应该确切地知道它正在创建什么鸭子,所以可能应该负责知道如何初始化鸭子,并且代码的其余部分仅适用于 IDuck,而行为方法内没有任何“ifs”。

显然,您可以直接向 IDuck 接口引入“初始化”的概念。比如说,一只“普通”鸭子需要喂食,需要打开一只电动鸭子,等等:)但这听起来有点狡猾:)