C# 接口强制转换是否违反了里氏替换原则?

6
我想引用之前在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; }
}

违反LSP的原始方式如下:

 void MakeDuckSwim(IDuck duck)
    {
        if (duck is ElectricDuck)
            ((ElectricDuck)duck).TurnOn();
        duck.Swim();
    }

作者提出的一种解决方案是将逻辑放在电鸭的游泳方法中,以便使其自行启动:
public class ElectricDuck : IDuck
{
    public void Swim()
    {
        if (!IsTurnedOn)
            TurnOn();

        //swim logic  
    }

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

    public bool IsTurnedOn { get; set; }
}

我曾遇到其他情况,可以创建一个扩展接口来支持某种初始化:

public interface IInitializeRequired
{
    public void Init();
}

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
}

编辑:扩展接口的原因是基于作者说自动在游泳方法中打开可能会产生其他不希望的结果。

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

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

        duck.Swim();
    }

我提出了将初始化概念抽象化并创建一个名为IElectricDuck的扩展接口,其中包含TurnOn()方法,这样做似乎是正确的。然而,整个初始化概念可能只是因为电动鸭而存在。

这是否是更好的解决方案,还是它只是LSP违规的伪装。

谢谢。


为什么不通过另一个泛化来进一步减少问题呢?例如:IDuck.HasEnergy等,然后您可以强制执行“获取能量”,这在原则上对于每只鸭子都是相同的,但在细节上却有所不同(一只可能吃东西,另一只可能插入电池或打开开关)。 - Grant Thomas
我也在考虑这些方面,但问题仍然存在:该方法并没有为这些接口提供参数,而是在最高级别上进行接口转换。当添加其他泛化时,例如CanGetEnergy(),这可能会导致相同的LSP违规问题,因为现在所有方法都需要添加此逻辑。 - Andre
把逻辑放在电鸭游泳方法里面让它自己打开有什么问题? - Acaz Souza
Acaz:我认为这里对Electric duck的例子过于字面化了,问题是在游泳之前和可能之后做某事(其他),例如如果该方法建立了一个SQL连接,那么在方法结束之前它也应该关闭吗?如果不希望这样做怎么办?那么连接何时关闭?我正在寻找一种处理这个问题的方法。 - Andre
5个回答

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();
} 

另一个选项是接受未初始化的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
        }
    }
}  

嗨Heinzi,这看起来是一个更好的方法,但是你如何将其与其他形式的接口转换联系起来,例如在.NET中拥有一些接口引用并在Disposal时使用var disp =(Someinterface as IDisposable)? - Andre
@Andre:通常,创建对象的代码负责处理它的释放(使用using语句),所以我想这是相当罕见的情况。 - Heinzi

4
我仍然认为你最后的例子是违反LSP的,因为从逻辑上讲你确实这样做了。正如你所说,初始化实际上并没有概念,只是一个hack。
事实上,你的MakeDuckSwim方法不应该知道任何鸭子的具体情况(无论它是否需要在初始化之前被初始化,是否需要在初始化后被喂食,等等)。 它只需要让提供的鸭子游泳!
很难在这个例子上判断(因为它不是真实的),但看起来似乎在“更高层”中有一个工厂或其他东西,可以创建特定的鸭子。
你可能错过了工厂的概念吗?
如果有一个工厂,那么它应该确切地知道它正在创建的鸭子,因此可能应该负责知道如何初始化鸭子,而你的其余代码则使用IDuck而不需要在行为方法内部使用任何“if”。
显然,您可以直接向IDuck接口引入“初始化”的概念。例如,“普通”鸭子需要被喂养,电子鸭需要被打开等等。但听起来有点靠不住 :)

嗨Alexey,感谢您的回答。您认为在使用接口的方法中,如果需要执行其他操作,例如IDisposable,如果需要处理对象,则没有其他方法,只能进行强制转换并检查是否可处理并进行处理?这是我能想到的最简单的例子。 - Andre
@Andre Disposable 是一个不同的故事。Disposable 是技术性的,而不是功能性的。换句话说,Disposable 与业务逻辑无关。IComparable、IEquatable 等也是如此。它们并不代表从业务逻辑角度来看的行为。即使在处理可丢弃的对象时,你只需要一个 IDisposable 接口,而不用关心它实际上是什么。 - Alexey Raga

0
public interface ISwimBehavior
{
    void Swim();
}

public interface IDuck
{
    void ISwimBehavior { get; set; }
}

public class Duck : IDuck
{
    ISwimBehavior SwimBehavior { get { return new SwimBehavior(); }; set; }
}

public class ElectricDuck : IDuck
{
    ISwimBehavior SwimBehavior { get { return new EletricSwimBehavior(); }; set; }
}

行为类:

public class SwimBehavior: ISwimBehavior
{
    public void Swim()
    {
        //do something to swim
    }
}

public class EletricSwimBehavior: ISwimBehavior
{
    public void Swim()
    {
        if (!IsTurnedOn)
            this.TurnOn();

        //do something to swim
    }

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

    public bool IsTurnedOn { get; set; }
}

2
我不明白这有什么帮助?你能加一些注释吗? - Myles McDonnell

0

我认为首先你需要回答一个关于电鸭的问题 - 当有人要求它们游泳时,它们是否会自动开启?如果是这样,在Swim方法中打开它们。

如果不是,那么就由鸭子的客户端负责将其打开,如果鸭子因为关闭而无法游泳,那么最好抛出一个InvalidOperationException异常。


0

可能是这样的:

public interface IDuck
{
    bool CanSwim { get; }
    void Swim();
}

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

    public bool CanSwim { get { return true; } }
}

public class ElectricDuck : IDuck
{
    public void Swim()
    {
        //swim logic  
    }

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

    public bool IsTurnedOn { get; set; }
    public bool CanSwim { get { return IsTurnedOn; } }
}

客户端将会被更改为:

void MakeDuckSwim(IDuck duck)
{
        if (duck.CanSwim)
        {
            duck.Swim();
        }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接