设计模式学习笔记2 - 结构型模式

前段时间,在自己糊里糊涂地写了一年多的代码之后,接手了一坨一个同事的代码。身边很多人包括我自己都在痛骂那些乱糟糟毫无设计可言的代码,我不禁开始深思:自己真的比他高明很多吗?我可以很自信地承认,在代码风格和单元测试上可以取胜,可是设计模式呢?自己平时开始一个project的时候有认真考虑过设计模式吗?答案是并没有,我甚至都数不出有哪些设计模式。于是,我就拿起了这本设计模式黑皮书。

中文版《设计模式:可复用面向对象软件的基础》,译自英文版《Design Patterns: Elements of Reusable Object-Oriented Software》。原书由Erich Gamma, Richard Helm, Ralph Johnson 和John Vlissides合著。这几位作者常被称为“Gang of Four”,即GoF。该书列举了23种主要的设计模式,因此,其他地方经常提到的23种GoF设计模式,就是指本书中提到的这23种设计模式。

把书看完很容易,但是要理解透彻,融汇贯通很难,能够在实际中灵活地选择合适的设计模式运用起来就更是难上加难了。所以,我打算按照本书的组织结构(把23种设计模式分成三大类)写三篇读书笔记,一来自我总结,二来备忘供以后自己翻阅。与此同时,如果能让读者有一定的收获就更棒了。我觉得本书的前言有句话很对,“第一次阅读此书时你可能不会完全理解它,但不必着急,我们在起初编写这本书时也没有完全理解它们!请记住,这不是一本读完一遍就可以束之高阁的书。我们希望你在软件设计过程中反复参阅此书,以获取设计灵感”。


本节将介绍结构型模式,包括适配器模式、桥接模式、组合模式、装饰模式、外观模式、享元模式和代理模式。结构型模式涉及到如何组合类和对象以获得更大的结构。结构型类模式采用继承机制来组合接口和实现(一个简单的例子是多重继承),如Adapter模式;而结构型对象模式则描述了如何对一些对象进行组合,从而实现新功能的一些方法,如Composite模式。

1. 适配器模式(Adapter)

适配器模式,将一个接口装换成另一个接口,以符合客户的预期。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

动机:两个已有的接口不兼容,我们需要在不改变这两个接口现有功能的前提下,使它们能够协同工作。

结构图:


Adapter Pattern

代码示例:
下面是一个使用Adapter的简单例子,我们原本的接口是ITurkey,但是用户希望看到的是IDuck,所以就在两者之间增加了TurkeyAdapter,使得原本的ITurkey也能像IDuck一样使用。

public interface IDuck
{
    void Quack();
    void Fly();
}

public interface ITurkey
{
    void Gobble();
    void Fly();
}

public class TurkeyAdapter : IDuck
{
    private ITurkey turkey;
    
    public TurkeyAdapter(ITurkey turkey)
    {
        this.turkey = turkey;
    }
    
    public void Quack()
    {
        this.turkey.Gobble;
    }
    
    public void Fly()
    {
        foreach(int i in Enumerable.Range(0, 5))
        {
            this.turkey.Fly();
        }
    }
}

一个适配器只能够封装一个类吗?
适配器模式的工作是将一个接口转换成另一个。虽然大多数的适配器模式所采取的例子都是让一个适配器包装一个被适配者,但我们都知道这个世界其实复杂多了,所以你可能遇到一些状况,需要让一个适配器包装多个被适配者。这涉及另一个模式,被称为外观模式Facade。

万一我们想在增加对新接口的适配的同时保持旧的接口怎么办呢?
我们可以创建一个双向的适配器,支持两边的接口。想创建一个双向的适配器,就必须实现所涉及的两个接口,这样,这个适配器可以当做旧的接口,或者当做新的接口使用。

2. 桥接模式(Bridge)

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

动机:一个经典的应用场景是,利用多层继承实现跨平台,如考虑在一个用户界面工具箱中,一个可移植的Window抽象部分的实现。所以我们先定义一个抽象的接口Window,然后为了实现在不同系统中的应用,分别定义了Window的两个子类XWindow和PMWindow。当增加一个子类IconWindow,为了使它支持两个系统平台,我们必须为IconWindow实现两个新类XIconWindow和PMIconWindow。
桥接模式用一种巧妙的方法处理多层继承存在的问题,用抽象关联取代了传统的多层继承,将类之间的静态继承关系转换成动态的对象组合关系,使得系统更灵活更容易扩展,同时有效控制系统中类的个数。

结构图:


Bridge Pattern

代码示例:
下面是使用Bridge模式的一个简单的例子,其中我们分别定义了两个接口IBridgeIAbstractBridge,前者用来定义IBridge接口的具体功能,后者则提供了实现Bridge功能的具体调用方法。

//Helps in providing truly decoupled architecture
public interface IBridge
{
    void Func1();
    void Func2();
}

public class Bridge1 : IBridge
{
    public void Func1()
    {
        Console.WriteLine("Func1 in Bridge1");
    }
    
    public void Func2()
    {
        Console.WriteLine("Func2 in Bridge1");
    }
}

public class Bridge2 : IBridge
{
    public void Func1()
    {
        Console.WriteLine("Func1 in Bridge2");
    }
    
    public void Func2()
    {
        Console.WriteLine("Func2 in Bridge2");
    }
}

public interface IAbstractBridge
{
    void CallFunc1();
    void CallFunc2();
}

public class AbstractBridge : IAbstractBridge
{
    public IBridge bridge;
    
    public AbstractBridge(IBridge bridge)
    {
        this.bridge = bridge;
    }
    
    public void CallFunc1()
    {
        this.bridge.Func1();
    }
    
    public void CallFunc2()
    {
        this.bridge.Func2();
    }
}

适用情况:
(1) 当你不希望抽象部分与实现部分之间有一个固定的绑定关系。比如,若需要运行时刻可以对实现部分进行切换。
(2) 类的抽象及它的实现都应该可以通过子类的方法进行扩充。
(3) 对一个抽象的实现部分的修改不会影响客户。
(4) 当想在多个对象间共享实现(可能使用引用计数),但同时要求客户并不知道这一点。

3. 组合模式(Composite)

组合模式,是将对象组合成树形结构以表示“整体-部分”的层次结构,从而使得用户对单个对象和组合对象的使用具有一致性。

动机:对于树形结构,由于容器对象和叶子对象在功能上的区别,在使用这些对象的代码中必须有区别地对待容器对象和叶子对象,而实际上大多数情况下我们希望一致地处理它们,因为对于这些对象的区别对待将会使得程序非常复杂。

结构图


Composite Pattern

代码示例:
下面是组合模式的简单示例,我们让LeafNormal都实现自IComposite,即用IComposite抽象它们都具有的功能来实现对它们操作的统一性。

public interface IComposite
{
    void Operation();
}

public class LeafComposite : IComposite
{
    public void Operation()
    {
        Console.WriteLine("Operating leaf");
    }
}

public class NormalComposite : IComposite
{
    public void Operation()
    {
        Console.WriteLine("Operating composite");
    }
    
    public void Add(IComposite composite)
    {
    }
    
    public void Remove(IComposite composite)
    {
    }
    
    public IComposite GetChild(int i)
    {
        throw new NotImplementedException();
    }
}

适用情况:
(1) 你想表示对象的部分-整体层次结构;
(2) 你希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。

4. 装饰模式(Decorator)

装饰模式,可以动态地给一个对象添加一些额外的职责。继承也可以实现增加功能,但是装饰模式会比生成子类更加灵活。

动机:有时我们希望给某个对象而不是整个类添加一些功能,例如,一个图形用户界面工具箱允许你对任意一个用户界面组件添加一些特性,如边框。我们可以采用继承机制实现一个带有边框特性的组件子类,但是这样边框的选择是静态的,用户不能控制对组件加边框的方式和时机。一种较为灵活的方式是将组件嵌入另一个对象中,由这个对象添加边框。我们称这个嵌入的对象为装饰。

结构图:


Decorator Pattern

代码示例:
下面是装饰模式的简单示例。我们原本可能有一系列不同的类实现IShape,现在我们需要对这些图形添加颜色。如果直接在IShape里面添加颜色,则所有实现它的类都需要更改,因此我们使用装饰模式定义ColoredShape,其中包含一个IShape对象的引用。

public class IShape
{
    string GetName();
}

public class Circle : IShape
{
    private double radius;
    public string GetName()
    {
        return "A circle";
    }
}

public ColoredShape : IShape
{
    private string color;
    public IShape shape;
    
    public string GetName()
    {
        return shape.GetName() + "which is in " + color;
    }
}

适用情况:
(1) 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责;
(2) 处理那些可以撤销的职责;
(3) 当不能采用生成子类的方法进行扩充时。一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。另一种情况可能是因为类定义被隐藏,或类定义不能用于生成子类。

5. 外观模式(Facade)

外观模式,为子系统中的一组接口提供一个一致的界面。外观模式定义一个高层接口,这个接口使得这一子系统更加容易使用。

动机:当一个系统较为庞杂时,一个常见的设计是将它划分成若干个相互依赖较小的子系统,引入外观对象为子系统提供一个单一而简单的界面就是方法之一。例如有一个编程环境,它允许应用程序访问它的编译子系统。这个编译子系统包含了多个不同的子类,有些特殊应用程序需要直接访问这些类,但是大多数用户只是希望编译一些代码。所以,为了提供一个高层的接口并且对客户屏蔽这些类,编译子系统还包括一个Compiler类,这个类定义了一个编译器功能的统一接口。Compiler类就是一个外观,它给用户提供了一个单一而简单的编译子系统接口。

结构图:


Facade Pattern

代码示例:
下面是外观模式的简单示例。在我们的汽车制造系统下有很多小系统,然而我们使用时一般都是需要一台完整的车。所以我们提供了一个CarFacade类屏蔽了CreateCompeleteCar方法的细节。

public class CarModel
{
    public void SetModel()
    {
    }
}

public class CarEngine()
{
    public void SetEngine()
    {
    }
}

public class CarBody
{
    public void SetBody()
    {
    }
}

public class CarAccessories
{
    publiv void SetAccessories()
    {
    }
}

public class CarFacade
{
    private readonly CarModel model;
    private readonly CarEngine engine;
    private readonly CarBody body;
    private readonly CarAccessories accessories;
    
    public CarFacade()
    {
        model = new CarModel();
        engine = new CarEngine();
        body = new CarBody();
        accessories = new CarAccessories();
    }
    
    public void CreateCompeleteCar()
    {
        model.SetModel();
        engine.SetEngine();
        body.SetBody();
        accessories.SetAccessories();
    }
}

适用情况:
(1) 当你要为一个复杂子系统提供一个简单接口时。
(2) 客户程序与抽象类的实现部分之间存在着很大的依赖性。引入Façade将这个子系统与客户以及其他的子系统分离,可以提高子系统的独立性和可移植性。
(3) 当你需要构建一个层次结构的子系统时,使用Façade模式定义子系统中每层的入口点。如果子系统之间是相互依赖的,你可以让它们仅通过Façade进行通讯,从而简化了它们之间的依赖关系。

6. 享元模式(Flyweight)

享元模式,运用共享技术有效地支持大量细粒度的对象。

动机:例如大多数文档编辑器的实现都有文本格式化和编辑功能,这些功能在一定程序上是模块化的。面向对象的文档编辑器通常使用对象来表示嵌入的成分,如表格和图形。然而通常并不是对每个字符都用一个对象来表示,因为这样会耗费大量的内存产生难以接受的运行开销。享元模式描述了如何共享对象,使得可以细粒度地使用它们而无需高昂的代价。

结构图:


Flyweight Pattern

代码示例:
下面是享元模式的简单示例。假设我们的系统中记录了一些员工信息,其中员工信息中又包含他所在的公司的一些信息。因此我们为这个公司创建了一个静态的共享对象FlyweightPointer.Company,这样所有的员工就不需要再单独存储公司相关的信息。

public class Flyweight
{
    public string CompanyName {get; set;}
    public string CompanyLocation {get; set;}
    public string CompanyWebsite {get; set;}
    public byte[] CompanyLogo {get; set;}
}

public static class FlyweightPointer
{
    public static readonly Flyweight Company = new Flyweight
    {
        CompanyName = "CompanyName",
        CompanyLocation = "CompanyLocation",
        CompanyWebsite = "www.website.com"
        // Load CompanyLogo
    };
}

public class Employee
{
    public string Name {get; set;}
    public string Company
    {
        get 
        {
            return FlyweightPointer.Company.CompanyName;
        }
    }
}

适用情况:
Flyweight模式的有效性很大程度上取决于如何使用它以及在何处使用它,当以下情况都成立时使用Flyweight模式:
(1) 一个应用程序使用了大量的对象;
(2) 完全由于使用大量的对象造成很大的存储开销;
(3) 对象的大多数状态都可变为外部状态;
(4) 如果删除对象的外部状态,那么可以用相对较小的共享对象取代很多组对象;
(5) 应用程序不依赖于对象标识。

7. 代理模式(Proxy)

代理模式,为其他对象提供一种代理以控制对这个对象的访问。

动机:对一个对象进行访问控制的一个原因是为了只有在我们确实需要这个对象时才对它进行创建和初始化。考虑一个可以在文档中嵌入图形对象的文档编辑器,有些图形对象的创建开销可能会很大,而我们要求必须迅速打开文档,即打开文档的时候应避免一次性创建所有开销很大的对象。问题的解决方案是使用另一个对象,即图像Proxy,替代那个真正的图像,在需要的时候Proxy负责实例化这个图像对象。

结构图:


Proxy Pattern

代码示例:
下面是代理模式的简单示例。我们需要操作的真实的实例是Car,但是为了对操作Car附加一些限制,我们增加了ProxyCar来做一些额外的检查,只有当满足一定的条件后,ProxyCar才会将真实的request转发给Car,即this.realCar.DriveCar()

public interface ICar
{
    void DriveCar();
}

// Real Object
public class Car : ICar
{
    public void DriveCar()
    {
        Console.WriteLine("Car has been driven!");
    }
}

// Proxy Object
public class ProxyCar : ICar
{
    private Driver driver;
    private ICar realCar;
    
    public ProxyCar(Driver driver)
    {
        this.driver = driver;
        this.realCar = new Car();
    }
    
    public void DriveCar()
    {
        if(driver.Age <= 16)
        {
            Console.WriteLine("Sorry, the driver is too young to drive.");
        }
        else
        {
            this.realCar.DriveCar();
        }
    }
}

public class Driver
{
    public int Age {get; private set;}
    
    public Driver(int age)
    {
        this.Age = age;
    }
}

适用情况:

(1)远程代理为一个对象在不同的地址空间提供局部代表。
(2)虚代理根据需要创建开销很大的对象。
(3)保护代理控制对原始对象的访问,保护代理用于对象应该有不同的访问权限的时候。
(4)智能指针取代了简单的指针,它在访问对象时执行了一些附加操作。

8. 小结

结构型模式的目标都是如何组合类和对象以获得更大的结构,以上这七种模式存在一定的相似性,但是也有各己的优缺点和适用情况。下面是一些相似模式的比较:

(1)Adapter vs. Bridge

它们都是给另一对象提供一定程度的间接性,因而有利于系统的灵活性。它们都涉及到从自身以外的一个接口向这个对象转发请求。

Adapter模式主要是为了解决两个已有接口之间不匹配的问题。它不需要考虑这些接口如何实现,也不考虑它们各自将如何演化。

Bridge模式则对抽象接口与它的实现部分进行桥接。虽然这一模式允许你修改实现它的类,它仍然为用户提供了一个稳定的接口。Bridge模式也会在系统演化时适应新的实现。Bridge模式一般用于设计阶段,因为它的使用者必须事先知道,一个抽象将有多个实现部分并且抽象和实现两者是独立演化的。

(2)Adapter vs. Façade

前者是两个接口的适配器,后者定义了一个新的接口。

(3)Adapter vs. Decorator

前者是使两个已有的接口能够协同工作,后者是在现有接口缺乏某种功能而在不生成子类的前提下给对象添加职责。

(4)Composite vs. Decorator

前者旨在构造类,使多个相关的对象能够以统一的方式处理,而多重对象可以被当做一个对象来处理。后者旨在使你能够不需要生成子类即可给对象添加职责。这就避免了静态实现所有的功能组合从而导致子类急剧增加。

(5)Decorator vs. Proxy

两种模式都是描述了怎样为对象提供一定程度上的间接引用,Proxy和Decorator对象的实现部分都保留了一个指向另一个对象的指针,它们向这个对象发送请求。

像Decorator模式一样,Proxy模式构成一个对象并为用户提供一致的接口。但是不同的是,Proxy模式不能动态地添加或者分离性质,它也不是为递归组合而设计的。它的目的是,当直接访问一个实体不方便或者不符合需求时,为这个实体提供一个替代者。

Proxy模式中,实体定义了关键功能,而Proxy提供或者拒绝对它的访问。在Decorator模式中,组件仅提供了部分功能,而一个或多个Decorator负责完成其他功能。Decorator模式适用于编译时不能确定对象的全部功能的情况。

参考文献:
《设计模式 可复用面向对象软件的基础》
《Head first 设计模式》
设计模式之结构型模式

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353

推荐阅读更多精彩内容

  • 设计模式汇总 一、基础知识 1. 设计模式概述 定义:设计模式(Design Pattern)是一套被反复使用、多...
    MinoyJet阅读 3,937评论 1 15
  • 设计模式基本原则 开放-封闭原则(OCP),是说软件实体(类、模块、函数等等)应该可以拓展,但是不可修改。开-闭原...
    西山薄凉阅读 3,792评论 3 14
  • 1 场景问题# 1.1 复杂的奖金计算## 考虑这样一个实际应用:就是如何实现灵活的奖金计算。 奖金计算是相对复杂...
    七寸知架构阅读 3,993评论 4 67
  • 本文是《设计模式——可复用面对对象软件的基础》的笔记。 面对对象设计的几个原则:1.针对接口编程,而不是针对实现编...
    Lension阅读 1,193评论 0 0
  • 一、设计模式的分类 总体来说设计模式分为三大类: 创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者...
    RamboLI阅读 749评论 0 1