zoukankan      html  css  js  c++  java
  • 【设计模式】如何用组合替代继承

    如果问面向对象的三大特性是什么,多数人都能回答出来:封装、继承、多态。

    继承 作为三大特性之一,近来却越来越不推荐使用,更有极端的语言,直接语法中就不支持继承,例如 Go。这又是为什么呢?

    为什么不推荐使用继承?

    假设我们要设计一个关于鸟的类。

    我们将“鸟类”定义为一个抽象类 AbstractBird。所有更细分的鸟,比如麻雀、鸽子、乌鸦等,都继承这个抽象类。

    大部分鸟都会飞,那我们可不可以在 AbstractBird 抽象类中,定义一个 Fly() 方法呢?

    答案是否定的。尽管大部分鸟都会飞,但也有特例,比如鸵鸟就不会飞。鸵鸟继承具有 Fly() 方法的父类,那鸵鸟就具有“飞”这样的行为,这显然不符合我们对现实世界中事物的认识。

    解决方案一

    在鸵鸟这个子类中重写 Fly() 方法,让它抛出异常。

    public class AbstractBird
    {
        public virtual void Fly()
        {
            Console.WriteLine("I'm flying.");
        }
    }
    
    //鸵鸟
    public class Ostrich : AbstractBird
    {
        public override void Fly()
        {
            throw new NotImplementedException("I can't fly.");
        }
    }
    

    这种设计思路虽然可以解决问题,但不够优美。因为除了鸵鸟之外,不会飞的鸟还有很多,比如企鹅。对于这些不会飞的鸟来说,我们都需要重写 Fly() 方法,抛出异常。

    这违背了迪米特法则(也叫最少知识原则),暴露不该暴露的接口给外部,增加了类使用过程中被误用的概率。

    解决方案二

    通过 AbstractBird 类派生出两个更加细分的抽象类:会飞的鸟类 AbstractFlyableBird 和不会飞的鸟类 AbstractUnFlyableBird,让麻雀、乌鸦这些会飞的鸟都继承 AbstractFlyableBird,让鸵鸟、企鹅这些不会飞的鸟,都继承 AbstractUnFlyableBird 类。

    此时,继承关系变成了三层,还行得通。

    如果要再添加一个游泳 Swim() 的方法,那情况就复杂了,要分为四中情况:

    • 会飞会游泳
    • 会飞不会游泳
    • 不会飞会游泳
    • 不会飞不会游泳

    如果再有其他行为加入,抽象类的数量就会几何级数增长。

    我们要搞清楚某个类具有哪些方法、属性,必须阅读父类的代码、父类的父类的代码……一直追溯到最顶层父类的代码。

    使用组合

    针对“会飞”这样一个行为特性,我们可以定义一个 Flyable 接口,只让会飞的鸟去实现这个接口。针对会游泳,定义一个 Swimable 接口,会叫定义一个 Tweetable 接口。

    public interface Flyable
    {
        void Fly();
    }
    
    public interface Swimable
    {
        void Swim();
    }
    
    public interface Tweetable
    {
        void Tweet();
    }
    
    //麻雀
    public class Sparrow : Flyable, Tweetable
    {
        public void Fly() => Console.WriteLine("I am flying.");
    
        public void Tweet() => Console.WriteLine("!@#$%^&*……");
    }
    
    //企鹅
    public class Penguin : Swimable, Tweetable
    {
        public void Swim() => Console.WriteLine("I am swimming.");
    
        public void Tweet() => Console.WriteLine("!@#$%^&*……");
    }
    

    麻雀和企鹅都会叫,Tweet 实现了两遍,这是坏味道。我们可以用组合来消除这个坏味道。

    public interface Flyable
    {
        void Fly();
    }
    
    public interface Swimable
    {
        void Swim();
    }
    
    public interface Tweetable
    {
        void Tweet();
    }
    
    public class FlyAbility : Flyable
    {
        public void Fly() => Console.WriteLine("I am flying.");
    }
    
    public class SwimAbility : Swimable
    {
        public void Swim() => Console.WriteLine("I am swimming.");
    }
    
    public class TweetAbility : Tweetable
    {
        public void Tweet() => Console.WriteLine("!@#$%^&*……");
    }
    
    //麻雀
    public class Sparrow : Flyable, Tweetable
    {
        FlyAbility flyAbility = new FlyAbility();
        TweetAbility tweetAbility = new TweetAbility();
    
        public void Fly() => flyAbility.Fly();
    
        public void Tweet() => tweetAbility.Tweet();
    }
    
    //企鹅
    public class Penguin : Swimable, Tweetable
    {
        SwimAbility swimAbility = new SwimAbility();
        TweetAbility tweetAbility = new TweetAbility();
    
        public void Swim() => swimAbility.Swim();
    
        public void Tweet() => tweetAbility.Tweet();
    }
    

    虽然现在主流的思想都是多用组合少用继承,但是从上面的例子可以看出,继承改写成组合意味着要做更细粒度的类的拆分,要定义更多的类和接口。类和接口的增多也就或多或少地增加代码的复杂程度和维护成本。所以,在实际的项目开发中,我们还是要根据具体的情况,来具体选择该用继承还是组合。


    本文出自极客时间 王争 老师的课程《设计模式之美》。原文示例为 java,因为我是做 C# 的,所以本文示例代码我改成了 C# 。

  • 相关阅读:
    欧拉公式求四面体的体积
    欧拉公式求四面体的体积
    I
    I
    闭包传递(floyed)
    闭包传递(floyed)
    Python hypot() 函数
    Python cos() 函数
    Python atan2() 函数
    Python atan() 函数
  • 原文地址:https://www.cnblogs.com/gl1573/p/13129284.html
Copyright © 2011-2022 走看看