zoukankan      html  css  js  c++  java
  • 设计模式-桥接模式

    延续上一篇装饰器模式的话题,我们继续对需求进行升级。

    示例

    需求

    还是以奶茶店为例,但是我们不再仅仅考虑奶茶的成分了,要想奶茶卖的好,还得需要一个响亮的品牌,奶茶有很多品牌,如一点点,COCO,喜茶等,除此之外,我们还要对奶茶的规格进行区分,如大杯、中杯、小杯等,不同品牌价格不同,不同规格价格也不同(不考虑太复杂的情况,就假设每种品牌和规格都有一个价格基数,总价直接累加)。

    初级方案

    乍一看,跟上一篇装饰器模式中的需求没什么区别,不就是把配料换成了品牌和规格吗?我们还是先看看继承的方案:

    再看看部分实现代码:

    public abstract class Drink
    {
        public string Name { get; set; }
    
        public int Price { get; set; }
    
        public abstract string Desc { get; }
    
        public abstract int Cost { get; }
    }
    
    public class Naicha : Drink
    {
        public Naicha()
        {
            Name = "奶茶";
            Price = 8;
        }
        public override string Desc => this.Name;
        public override int Cost => this.Price;
    }
    
    public class CoCoNaicha:Naicha
    {
        public CoCoNaicha()
        {
            Name += "[CoCo]";
            Price += 2;
        }
    }
    
    public class DaCoCoNaicha : CoCoNaicha
    {
        public DaCoCoNaicha()
        {
            Name += "+大杯";
            Price += 3;
        }
    }
    

    问题

    初级方案和遇到的问题也跟装饰器模式几乎是一模一样的:

    • 类爆炸;
    • 大量代码重复;
    • 如果增加品牌,或者调整规格价格,代码维护困难,严重违反开闭原则。

    思考

    到了这里,我们不妨思考一下如下两个问题:

    1. 考虑能否使用装饰器模式实现?
    2. 这里的品牌、规格跟装饰器模式中用到的配料有什么区别?

    很明显,通过装饰器模式肯定是可以达成目标的,毕竟他们看起来差不多,但是这样好不好呢?这就需要回答第二问题,品牌、规格跟配料有区别吗?

    • 首先,一杯奶茶可以同时加多种配料,但是不能同时属于多种品牌或者多种规格,例如可以有红豆布丁奶茶,却不可以有一点点COCO奶茶;
    • 其次,奶茶配料可以加多份,例如多冰,多糖,双份布丁等,但是奶茶只能属于一种品牌、一种规格;
    • 最后,一杯奶茶可以不加配料,但是一定会属于某一个品牌或者规格。

    改进

    发现了吗?区别还是很大的。由于有了这些区别,实现方式自然也应该是有所不同的。其实,这里没有那么复杂,不需要一步就跳到装饰者模式,我们依旧用最老套的改进方式---继承转组合---就可以了。
    改进后的类图如下所示:

    再看看部分实现代码,这里直接将品牌和规格聚合到了饮品基类中,其中,品牌和规格面向抽象编程,我想就不用细说了:

    public abstract class Drink
    {
        private readonly BrandBase _brand;
    
        private readonly SkuBase _sku;
        public Drink(BrandBase brand, SkuBase sku)
        {
            this._brand = brand;
            this._sku = sku;
        }
    
        public string Name { get; set; }
    
        public int Price { get; set; }
    
    
        public string Desc
        {
            get
            {
                return this.Name + this._brand.BrandName + this._sku.SkuType;
            }
        }
    
        public int Cost
        {
            get
            {
                return this.Price + this._brand.Price + this._sku.Price;
            }
        }
    }
    
    public class Naicha : Drink
    {
        public Naicha(BrandBase brand, SkuBase sku):base(brand,sku)
        {
            Name = "奶茶";
            Price = 8;
        }
    }
    

    品牌和规格的部分代码如下:

    public abstract class SkuBase
    {
        public abstract string SkuType { get; }
    
        public abstract int Price { get; }
    }
    
    public class Dabei : SkuBase
    {
        public override string SkuType => "大杯";
    
        public override int Price => 3;
    }
    
    public abstract class BrandBase
    {
        public abstract string BrandName { get; }
    
        public abstract int Price { get; }
    }
    
    public class CoCo : BrandBase
    {
        public override string BrandName => "[CoCo]";
    
        public override int Price => 2;
    }
    

    比想象中的要简单,一步就到位了。我们再来看看,如果要扩展增加瑞辛咖啡呢?

    public class Kafei : Drink
    {
        public Kafei(BrandBase brand, SkuBase sku) : base(brand, sku)
        {
            Name = "咖啡";
            Price = 12;
        }
    }
    
    public class Ruixin : BrandBase
    {
        public override string BrandName => "[瑞辛]";
    
        public override int Price => 2;
    }
    

    没错,就是这么简单,直接在饮品和品牌两个维度上增加咖啡和瑞辛就可以了,原有的代码不用做任何修改,一步改造直接就满足了开闭原则。没错,这就是桥接模式。

    简化UML

    将上述案例中的类图简化并抽象就可以得到桥接模式的UML类图了:

    • Abstraction:抽象化角色,并保存一个对实现化对象的引用。
    • RefinedAbstraction:修正抽象化角色,改变和修正父类对抽象化的定义。
    • Implementor:实现化角色,这个角色给出实现化角色的接口,但不给出具体的实现。
    • ConcreteImplementor:具体实现化角色,这个角色给出实现化角色接口的具体实现。

    定义

    桥接模式是将抽象部分与它的实现部分分离,使它们都可以独立地变化。

    有的人可能会说,既然是桥接模式,那么桥在哪里呢?其实,Abstraction就是桥,从奶茶店的例子来看,Drink就是桥,桥接的是DrinkBrandBaseSkuBase三个维度,你没看错,这里的Drink起到了两个作用,一个作用是饮品基类,另一个作用就是桥。其实,桥的目的就是为了将多个维度联系起来,因此,也可以单纯的通过多继承实现,或者单纯的通过组合实现。但是,高级语言一般都不支持多继承,并且,我们也知道,继承并不是一个好的设计方式,因此不选择多继承;另外,如果纯粹的通过组合实现,就需要额外定义一个无意义的桥类,这个类同时将DrinkBrandBaseSkuBase组合进来,虽然可行,但这明显是不够优雅的,因此,桥接模式依然采用的是继承+组合的模式。

    优缺点

    优点

    • 分离抽象部分与它的实现部分
    • 相对于继承有更少的子类,使用更灵活,可在多个维度上自由扩展

    缺点

    • 增加系统的理解与设计难度;
    • 独立变化的维度的识别比较困难;
    • 客户端使用成本较高,需要对各个维度进行构造。

    跟装饰器模式的区别

    • 装饰器模式是为了动态地给一个对象增加功能,而桥接模式是为了让类在多个维度上自由扩展;
    • 装饰器模式的装饰者和被装饰者需要继承自同一父类,而桥接模式通常不需要;
    • 装饰器模式通常可以嵌套使用,而桥接模式不能。

    总结

    有看过前一篇装饰器模式相关介绍的朋友可能会有所疑惑,我们这里同样改动了Drink基类啊,前面不是说不应该修改原有的类吗?其实原因很简单,因为目的不一样了,装饰器模式是为了动态附加职能,而桥接模式是为了可以在多个维度上自由扩展。说到底,桥接模式适用在设计阶段,也就是在设计Drink类的时候,目的是为了把Drink设计的更好用,而不是动态的在原有的Drink类上额外增加内容,正因如此,在设计之初,维度的识别也就显得至关重要了。

    源码链接

  • 相关阅读:
    Win64 驱动内核编程-12.回调监控进线程创建和退出
    我的主站 SHARELIST -分享列表 (功能持续完善中 2019-11-24 版本0.3)
    Caddy-基于go的微型serve用来做反向代理和Gateway
    Docker Swarm删除节点
    Docker Swarm集群搭建
    Docker Machine-Windows
    Docker Compose
    Docker网络配置进阶
    Docker网络相关
    Docker数据管理
  • 原文地址:https://www.cnblogs.com/FindTheWay/p/13609158.html
Copyright © 2011-2022 走看看