目录
0. 学习目标
0.1 软件设计模式
0.2 软件体系结构
软件设计模式
0. 软件设计原则
开闭原则
设计模式的分类
根据其目的(模式是用来做什么的),面向对象的领域的设计模式可分为创建型(Creational),结构型(Structural)和行为型(Behavioral) 三种:
创建型模式主要用于创建对象
结构型模式主要用于处理类或对象的组合
行为型模式主要用于描述对类或对象怎样交互和怎样分配职责
创建型软件设计模式
创建型软件设计模式是为了将创建对象的责任委托给某个特殊的类,所以注意被创建的对象和谁创建,怎样创建,增强代码的灵活性
创建和使用分离
创建型模式隐藏了类的实例的创建细节,通过隐藏对象如何被创建和组合在一起达到使整个系统独立的目的。
模式包括:
1.模式的名称
2.模式的目的,即要解决的问题
3.实现方法
4.为实现该模式必须考虑的限制和约束因素
一、工厂模式
工厂方法与抽象工厂模式
1.简单工厂方法模式(开闭原则)
简单工厂模式:又称为静态工厂方法模式。
在简单工厂模式中,可以根据参数的不同返回不同类的实例。
简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。
Creator cr = new Creator();
Product p = cr.factory(“option”);
p.Operation();
优缺点
优点
- 实现了对责任的分割,它提供了专门的工厂类用于创建对象
- 客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可
- 通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类
缺点
- 由于工厂类集中了所有产品创建逻辑,一旦不能正常工作,整个系统都要受到影响
- 增加系统中类的个数
- 系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时
- 工厂角色无法形成基于继承的等级结构
简单工厂模式最大的缺点就是当有新产品要加入到系统中时,必须修改工厂类,加入必要的处理逻辑,这违背了“开闭原则”
并且要是工厂类失效,整个系统都失效了
类的个数很多
适用场景
所以一般只有在对应的类少,以及客户端只需要提供参数就得到对应的对象
实例一:简单电视机工厂
如果需要增加新的TV,则需要修改工厂类的传参的条件
2.工厂方法模式
将简单工厂方法中单一的工厂类改写为一个层次类
对应简单工厂模式的工厂角色无法形成基于继承的等级结构
Creator cr = null;
if ( x = a ) cr = new CreatorA();
else
cr = new CreatorB();
Product p = cr.factory();
在工厂方法模式中,核心的工厂类不再负责所有产品的创建,而是将具体创建工作交给子类去做
工厂方法模式退化后可以演变成简单工厂模式
优缺点
优点:
- 用户只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须知道具体产品类的类名(因为一个工厂创建一个产品)
- 工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部
- 在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品就可以了。
缺点:
- 在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。
- 增加了系统的抽象性和理解难度,且在实现时可能需要用到DOM、反射等技术,增加了系统的实现难度。
适用场景
- 一个类不知道它所需要的对象的类
- 一个类通过其子类来指定创建哪个对象(子类指定创建的对象)
- 将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无须关心是哪一个工厂子类创建产品子类,需要时再动态指定
实例一:汽车保险管理程序
PolicyProducer pp = null;
if ( x = “BodyPolicy” ) pp = new BodyPolicy();
else if ( x = “Collision” ) pp = new CollPolicy();
else if ( x = “PersonInjure” ) pp = new PersonPolicy();
else if ( x = “Property” ) pp = new PropPolicy();
else if ( x = “Comprehensive” ) pp = new ComPolicy();
AutoInsurance ai = pp.getInsurObj();
实例二:电视机工厂
3. 简单工厂模式与工厂方法模式的区别
- 两个模式的中心不同。
- 是否支持开闭原则。
- 创建对象逻辑判断的位置。
简单工厂模式的中心为一个实的工厂类,工厂方法模式的中心为抽象工厂或者接口
简单工厂模式不支持开闭原则,而工厂模式支持
简单工厂模式中,必要的创建对象的逻辑判断包含在工厂类中(实例一:汽车保险管理程序),在工厂方法模式中,工厂类不必包含创建对象的逻辑判断
4. 抽象工厂模式
但是有时候我们需要一个工厂可以提供多个产品对象,而不是单一的产品对象。
产品等级概念(继承同一东西)与产品族(同一个工厂下的不同继承不同东西)
优缺点
优点
- 抽象工厂模式隔离了具体类的生成,使得客户并不需要知道什么被创建。由于这种隔离,更换一个具体工厂就变得相对容易。
- 所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。另外,应用抽象工厂模式可以实现高内聚低耦合的设计目的,因此抽象工厂模式得到了广泛的应用。
- 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。这对一些需要根据当前环境来决定其行为的软件系统来说,是一种非常实用的设计模式。
- 增加新的具体工厂和产品族很方便,无须修改已有系统,符合“开闭原则”。
缺点
- 在添加新的产品对象时,难以扩展抽象工厂来生产新种类的产品,这是因为在抽象工厂角色中规定了所有可能被创建的产品集合,要支持新种类的产品就意味着要对该接口进行扩展,而这将涉及到对抽象工厂角色及其所有子类的修改,显然会带来较大的不便。
- 开闭原则的倾斜性(增加新的工厂和产品族容易,增加新的产品等级结构麻烦)
适用场景
实例一
Creator cr = null;
if ( x ==a ) cr = new ConcreteCreatorA();
else if ( x == b ) cr = new ConcreteCreatorB();
if ( y = “ManTie” ) ManTie mt = cr.factoryA();
else if ( y = “WomanTie” ) WomanTie wt = cr.factoryA();
else if ( y = “ManShoes” ) ManShoes ms = cr.factoryB();
else if ( y = “WomanShoes” ) WomanShoes ws = cr.factoryB();
else if ( y = “ManSuit” ) ManSuit msi = cr.factoryC();
else if ( y = “WomanSuit” ) WomanSuit wsi = cr.factoryC():
实例一:房屋销售查询系统
BuildingFactory bf = BuildingFactory.getBuildingFactory(“option”);
if ( x == “House” ) House hs = bf.getHouse();
else if ( x == “Cando” ) Cando cd = bf.getCando();
实例二:电器工厂
抽象工厂模式的可拓展性(两种情况开闭原则)
增加一个新的产品族,新的品牌的产品,只就需要一个新的具体工厂,不需要修改工厂接口,符合开闭原则
增加已有产品族的产品,需要修改具体的工厂和工厂接口,不符合开闭原则
不能横向扩展,只能纵向扩展,就是只能从冰箱A扩展到冰箱B(多一个品牌),不能扩展出汽车A
例如原来有两家公司A和B生产电视和冰箱,现在增加一个公司C也生产电视和冰箱则符合开闭原则
但如果是让原来的A和B新增加生产空调,则不符合开闭原则
5. 小结
抽象工厂模式是所有形式的工厂模式中最为抽象和最具一般性的一种形态。
抽象工厂模式与工厂方法模式最大的区别在于,工厂方法模式针对的是一个产品等级结构,而抽象工厂模式则需要面对多个产品等级结构。
6. 课程作业
简答题
1.简单工厂模式一般不符合开闭原则。在简单工厂模式中,如果要增加一个新产品类,相应地在工厂类中也要增加一个条件语句,用于创建新的产品类的对象。也就是说,必须修改工厂类的源代码。
2.工厂方法模式和抽象工厂模式符合开闭原则。因为在工厂方法模式和抽象工厂模式中,无需修改或者重新编译已经存在的代码,就可以添加新的产品类。
3.开闭模式是指对扩展开放,对修改关闭,说的更通俗点,就是说开发了一个软件,应该可以对它进行功能扩展(开放),而在进行这些扩展的时候,不需要对原来的程序进行修改(关闭)。
-
工厂方法模式符合开闭原则,因为它通过定义一个抽象的工厂接口和多个具体的工厂类,每个具体工厂类都实现了工厂接口,用于创建一类产品。当需要新增一种产品时,只需要添加一个对应的具体工厂类,而不需要修改已有的代码。这样做的好处是,在不修改现有代码的前提下,可以灵活地扩展系统,符合开闭原则的要求。
-
抽象工厂模式也符合开闭原则,它通过定义一个抽象的工厂接口和多个具体的工厂类,每个具体工厂类都负责创建一组相关的产品。当需要新增一组相关产品时,只需要添加对应的具体工厂类和产品类,而不需要修改已有的代码。这样可以保持系统的灵活性和可扩展性,符合开闭原则。
倘若要是增加一组产品族产品,就不符合开闭原则,需要修改原有代码 -
然而,简单工厂方法模式并不符合开闭原则。在简单工厂方法模式中,只有一个具体的工厂类负责创建所有的产品,根据不同的参数或条件来决定创建哪种产品。当需要新增一种产品时,需要修改具体工厂类的代码,违反了开闭原则。因此,在简单工厂方法模式下,开闭原则不成立。
画图题
在图2.17游戏软件设计中,游戏工厂(SlowGameFactory)类负责创建低级战士(SlowFighter)对象与低级怪物(SlowMonster)对象,并且将创建完的对象以其超类类型返回给用户界面客户端(ClientGUI)对象。然后,用户界面客户端(ClientGUI)对象将操纵低级战士(SlowFighter)对象与低级怪物(SlowMonster)对象,使得它们互相打斗。问题与任务:1、上述设计使用了什么设计模式?2、请在以上设计类图中添加4个新的类包括中级战士(MedFighter)、高级战士(SuperFighter)、中级怪物(MedMonster)和高级怪物(SuperMonster),使得中级战士(MedFighter)对象与中级怪物(MedMonster)对象是由一个抽象工厂类创建;高级战士(SuperFighter)对象与高级怪物(SuperMonster)对象由一个抽象工厂类创建,绘制新设计类图;3、除了以上添加的4个类以外,在以上类图中还应该添加什么类?4、描述新的设计类图;5、明确说明新设计的优点。
1、上述设计使用了抽象工厂设计模式
2、
3、SuperGameFactory和MedGameFactory
4、多个抽象产品类,每个抽象产品类可以派生出多个具体产品类。一个抽象工厂类,可以派生出多个具体工厂类。每个具体工厂类可以创建多个具体产品类的实例,也就是创建的是一个产品线下的多个产品。
5、工厂方法模式只有一个抽象产品类,而抽象工厂模式有多个。工厂方法模式的具体工厂类只能创建一个具体产品类的实例,而抽象工厂模式可以创建多个。
二、生成器模式
1. 生成者模式
概念
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
生成器模式是一步一步创建一个复杂的对象
Director director = new Director();
director.setBuilder(cb);
director.construct();
if ( cb == 1 ) Product1 = director.getObject();
else Product 2 = director.getObject();
1.确定Director,成员为xxxBuilder(表示要用到的生成器的父类),方法有设置生成器,获取产品(getXXX),创建整个产品的方法(这个方法用来调用生成器的各个方法,最后生成一个完整的产品)指挥者类针对抽象生成器类编程
2.确定XXXBuilder,成员为产品,方法就是构建这个产品所有部件所需的方法,以及getXXX获取产品,以及初始化产品的方法(肯定要先new申明一个空的产品,赋值给成员变量里的产品)
3.确定具体的生成器类,继承自XXXBuilder
4.确定具体产品,成员和方法就是该产品所需的各种方法属性等
实例一:房屋选购系统
题目
时序图
只需要选择一个HouseBuilder,再调用构建的方法constructWholeHouse,再一个个调用生成部件的方法,调用设置参数方法,就能返回产品
类图
Director director = new Director();
director.setHouseBuilder( hb );
director.constructWholeHouseObj();
House hsObj = director.getHouse();
优缺点
优点
- 在生成器模式中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
- 每一个具体生成器都相对独立,而与其他的具体生成器无关,因此可以很方便地替换具体生成器或增加新的具体生成器,用户使用不同的具体生成器即可得到不同的产品对象。
- 可以更加精细地控制产品的创建过程。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。
- 增加新的具体生成器无须修改原有类库的代码,指挥者类针对抽象生成器类编程,系统扩展方便,符合“开闭原则”。
缺点
- 生成器模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用生成器模式,因此其使用范围受到一定的限制。
- 如果产品的内部变化复杂,可能会导致需要定义很多具体生成器类来实现这种变化,导致系统变得很庞大。
适用场景
- 需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个成员属性。
- 需要生成的产品对象的属性相互依赖,需要指定其生成顺序。
- 对象的创建过程独立于创建该对象的类。在生成器模式中引入了指挥者类,将创建过程封装在指挥者类中,而不在生成器类中。
- 隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品。
2. 生成器模式与抽象工厂模式
- 与抽象工厂模式相比,生成器模式返回一个组装好的完整产品,而抽象工厂模式返回一系列相关的产品,这些产品位于不同的产品等级结构,构成了一个产品族。
- 在抽象工厂模式中,客户端实例化工厂类,然后调用工厂方法获取所需产品对象,而在生成器模式中,客户端可以不直接调用生成器的相关方法,而是通过指挥者类来指导如何生成对象,包括对象的组装过程和建造步骤,它侧重于一步步构造一个复杂对象,返回一个完整的对象。
- 如果将抽象工厂模式看成汽车配件生产工厂,生产一个产品族的产品,那么生成器模式就是一个汽车组装工厂,通过对部件的组装可以返回一辆完整的汽车。
3. 课程作业***
三、单例模式
1. 单例模式
单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供一个全局访问点。
要点:
某个类只能有一个实例
它必须自行创建这个实例
它必须自行向整个系统提供这个实例
基本思路
instance是静态变量,类型为Singleton,用于存储已经被创建的实例。getInstance()为实例创建方法,它创建实例的方式很特别,如果一个实例此前已经被创建了(因而被存储在变量instance中),则该方法返回instance;如果此前Single实例没有被创建,则该方法新创建并且返回Singleton类的一个实例,从而保证了实例的唯一性。
Public class Singleton {
private static Singleton instance;
private String name;
private Singleton (String name) {
this.name = name;
}
public static Singleton getInstance (String name) {
if (instance == null && ! name.equals(null))
instance = new Singleton (name);
return instance;
}
}
实例一:互联网连接问题
2. 多线程情况
多线程中的单例模式,在getInstance()方法应声明为synchronized,防止当唯一实例尚未创建时有两个线程同时调用创建方法,那么将导致两个现成各自创建了一个实例,从而违反了单例模式的实例唯一性的初衷
3. 优缺点
优点
- 提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它,并为设计及开发团队提供了共享的概念。
- 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
- 允许可变数目的实例。我们可以基于单例模式进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例。
缺点
- 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
- 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失。
4. 适用场景
- 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器,或者需要考虑资源消耗太大而只允许创建一个对象。
- 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
- 在一个系统中要求一个类只有一个实例时才应当使用单例模式。反过来,如果一个类可以有几个实例共存,就需要对单例模式进行改进,使之成为多例模式。
5. 课程作业
结构型软件设计模式
概述
- 动机
结构型软件设计模式的主要目的是将不同的类和对象组合在一起,形成更大或者更复杂的结构体。 - 内容
组合模式将一个或者多个相似的对象构成组合对象。
适配器模式提供一种机制改变原来不适合使用的接口。
外观模式新建一个外观类,通过调用原有的类库中众多的类的方式,实现外观类的所有方法。
一、组合模式
动机
组合模式:组合多个对象形成树形结构以表示“整体-部分”的结构层次。组合模式对单个对象(即叶子对象) 和 组合对象(即容器对象) 的 使用具有一致性
组合模式又可以称为“整体-部分”模式
组合模式结构
实例一:五子棋游戏
另外,还有两个辅助类,一个类是棋子图形类GChessPiece,其对象代表一个实际的棋子图像,该图像可以被添加到棋盘Board对象上;另外一个辅助类为GameOperations类,该类提供判断棋局结果的一些矩阵方法。
实例二:空军指挥系统
同时,按照飞机类型设计了4个抽象类Fighter、Bomber、Transporter与EPlane。
每个抽象类下面都有相应的子类,子类为具体的机种。Squadron、Group类被设计成Composite类AirUnit的子类。
AirUnit类封装了数据类型ArrayList,该类型为具有弹性长度的List,在里面可以装在各种类型的对象。
关于组合模式的讨论
1. 安全形式的组合模式
安全形式的组合模式:在Composite类中声明所有的用来管理子类对象的方法,包括add( )、remove( )以及getChild( )方法,而在Component接口和树叶类型的对象中不包含管理子类对象的方法。
优点:安全;
缺点:Component层次结构类的超类与组合子类的接口不一致。
2. 透明形式的组合模式
透明形式的组合模式:在Component类中声明所有的用来管理子类对象的方法,包括add( )、remove( ),以及getChild( )方法。
优点:所有的构件类都有相同的接口;
缺点:不够安全。
优缺点
优点:
- 可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,使得增加新构件也更容易。
- 客户端调用简单,客户端可以一致地使用组合结构或其中单个对象。
- 定义了包含叶子对象和容器对象的类层次结构,叶子对象可以被组合成更复杂的容器对象,而这个容器对象又可以被组合,这样不断递归下去,可以形成复杂的树形结构。
- 在组合体内加入对象构件更容易,客户端不必因为加入了新的对象构件而更改原有代码。
缺点:
- 使设计变得更加抽象,对象的业务规则如果很复杂,则实现组合模式具有很大挑战性,而且不是所有的方法都与叶子对象子类都有关联。
- 增加新构件时可能会产生一些问题,很难对容器中的构件类型进行限制。
适用环境
- 需要表示一个对象整体或部分层次,在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,可以一致地对待它们。
- 让客户能够忽略不同对象层次的变化,客户端可以针对抽象构件编程,无须关心对象层次结构的细节。
- 对象的结构是动态的并且复杂程度不一样,但客户需要一致地处理它们。
课程作业
主要是组合关系,部件。component 包含 composite和部件,composite又包含部件
其中Company为抽象类,定义了在组织结构图上添加(Add)和删除(Delete)分公司/办事处或者部门的方法接口。类ConcreteCompany表示具体的分公司或者办事处,分公司或办事处下可以设置不同的部门。类HRDepartment和FinanceDepartment分别表示人力资源部和财务部。
二、适配器模式
概念
- 适配器的实现就是把客户类的请求转化为对适配者的相应接口的调用
- 当客户类调用适配器的方法时,在适配器类的内部将调用适配者类的方法,而这个过程对客户类是透明的,客户类并不直接访问适配者类
- 适配器可以使由于接口不兼容而不能交互的类可以一起工作
两种模式(类继承,对象调用)
类适配器模式:在图3.19中,假如要使用类Adaptee中的方法Operation1,同时也要使用另外一个方法Operation2,而Operation2没有在类Adaptee中,怎样解决该问题呢?
方案:用一个Target接口声明所有需要的方法,并且用另外一个Adapter类实现Target接口中所有的方法。同时,Adapter类继承Adaptee类,如果3.20所示。
对象适配器模式:在Java语言中不允许有多继承,所以,如果同时有两个或者两个以上的类Adaptee1、Adaptee2需要被适配,则不能够继续使用类适配器模式进行设计,图3.22所示的合计图是不合法的。
实例一:客户信息验证
实例二:字符串排序
关于适配器模式的讨论
1. 适配器的作用
-
适配器模式是将接口不同而功能相近的两个接口加以转换,包括适配器角色补充一些源角色没有但目标接口需要的方法。
-
适配器模式可以用于增加新的方法,但主要意图是转换接口
2. 类适配器模式与对象适配器模式的区别
(本质上没什么区别,只是在实现上的区别,比如Java不能多继承,所以用对象适配器模式)
优缺点
优点:
- 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,而无须修改原有代码。
- 增加了类的透明性和复用性,将具体的实现封装在适配者类中,对于客户端类来说是透明的,而且提高了适配者的复用性。
- 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”。
类适配器模式
类适配器模式还具有如下优点:
由于适配器类是适配者类的子类,因此可以在适配器类中置换一些适配者的方法,使得适配器的灵活性更强。
缺点:
有的语言不支持多继承,其使用有一定的局限性
对象适配器模式
对象适配器模式还具有如下优点:
一个对象适配器可以把多个不同的适配者适配到同一个目标,也就是说,同一个适配器可以把适配者类和它的子类都适配到目标接口。
缺点:
与类适配器模式相比,要想置换适配者类的方法就不容易。
适用环境
- 系统需要使用现有的类,而这些类的接口不符合系统的需要。
- 想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。
适配器模式的扩展
默认适配器模式
默认适配器模式(Default Adapter Pattern)或缺省适配器模式
- 当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现(空方法),
- 那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求,它适用于一个接口不想使用其所有的方法的情况。
- 因此也称为单接口适配器模式。
双向适配器
在对象适配器的使用过程中,如果在适配器中同时包含对目标类和适配者类的引用,适配者可以通过它调用目标类中的方法,目标类也可以通过它调用适配者类中的方法,那么该适配器就是一个双向适配器。
课程作业
1、Java是单继承语言,不支持多继承
2、
三、外观模式
概念
- 外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子接口更加容易使用
- 外观模式又称为门面模式,它是一种对象结构型模式
引入外观角色之后,用户只需要直接与外观角色交互,用户与子系统之间的复杂关系由外观角色来实现,从而降低了系统的耦合度。
外观模式:为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这个子系统更加容易使用。
外观模式由三个角色组成:
1.== 外观角色==:外观模式的核心。它被客户角色调用,因此它熟悉子系统的功能。其内部根据客户角色已有的需求预定了几种功能组合。
2. 子系统角色:实现子系统的功能,对它而言,外观角色就和客户角色一样是未知的,它没有任何外观角色的信息和链接。
3. 客户角色:调用外观角色来完成要得到的功能
外观模式分析
根据“单一职责原则”,在软件中将一个系统划分为若干个子系统有利于降低整个系统的复杂性,一个常见的设计目标是使子系统间的通信和相互依赖关系达到最小,而达到该目标的途径之一就是引入一个外观对象,它为子系统的访问提供了一个简单而单一的入口。
外观模式也是“迪米特法则”的体现,通过引入一个新的外观类可以降低原有系统的复杂度,同时降低客户类与子系统类的耦合度。
一个统一的外观对象进行
客户端只需要与外观对象打交道,而不需要与子系统内部的很多对象打交道
外观模式的目的在于降低系统的复杂程度
外观模式从很大程度上提高了客户端使用的便捷性
实例一:
实例二:图形绘制
外观模式讨论
一个系统有多个外观类?
- 在外观模式中,通常只需要一个外观类,并且此外观类只有一个实例,换言之它是一个单例类。
- 在很多情况下为了节约系统资源,一般将外观类设计为单例类。
- 当然这并不意味着在整个系统里只能有一个外观类,在一个系统中可以设计多个外观类,每个外观类都负责和一些特定的子系统交互,向用户提供相应的业务功能。
不要试图通过外观类为子系统增加新行为
- 不要通过继承一个外观类在子系统中加入新的行为,这种做法是错误的。
- 外观模式的用意是为子系统提供一个集中化和简化的沟通渠道,而不是向子系统加入新的行为,
- 新的行为的增加应该通过修改原有子系统类或增加新的子系统类来实现,不能通过外观类来实现。
外观模式与迪米特法则
外观模式创造出一个外观对象,将客户端所涉及的属于一个子系统的协作伙伴的数量减到最少,使得客户端与子系统内部的对象的相互作用被外观对象所取代。
抽象外观类的引入 (因为违背开闭原则)
外观模式最大的缺点在于违背了“开闭原则”,当增加新的子系统或者移除子系统时需要修改外观类,可以通过引入抽象外观类在一定程度上解决该问题,客户端针对抽象外观类进行编程。
使用外观模式的目的
为一系列复杂的接口提供一个统一的接口,使该系统更容易使用
适配器模式与外观模式的区别
- 适配器模式转换接口的目的是将一个不适用的接口转换为可以被使用的接口,或将一些接口不同而功能相近的接口加以转换,以便可以被统一使用
- 外观模式简化接口的目的是为了更好地使用某个类库
课程作业
四、桥接模式
桥接模式(Bridge Pattern)是一种软件设计模式,它用于将抽象部分与其具体实现部分解耦,使它们可以独立地变化。桥接模式的核心思想是将一个系统分为多个维度,并通过桥接连接这些维度,从而实现更灵活的系统设计。
在桥接模式中,有两个主要的角色:
-
抽象部分(Abstraction):抽象部分定义了系统的高层接口,它包含了抽象方法和引用实现部分的对象。抽象部分通过委派调用实现部分的对象来完成具体的操作。
-
实现部分(Implementor):实现部分定义了抽象部分的具体实现,它提供了具体方法的实现细节。实现部分通常是一个接口或抽象类,它定义了可以由具体实现类来实现的方法。
通过桥接模式,可以实现抽象部分和实现部分的分离,使它们可以独立地变化。这样,在系统需要扩展或修改时,可以在不影响其他部分的情况下对其进行修改。
使用桥接模式的好处包括:
- 解耦:将抽象部分和实现部分分离,使它们可以独立地变化。
- 扩展性:容易添加新的抽象部分和实现部分,扩展系统的功能。
- 可维护性:修改抽象部分或实现部分的代码不会对另一部分产生影响,易于维护。
总之,桥接模式通过将抽象部分和实现部分解耦,提供了一种灵活的设计方式,使系统更加可扩展和可维护。它在许多领域中都有广泛的应用,特别是在需要处理多维度变化的情况下,可以帮助构建更具弹性和可扩展性的软件系统。
概念
对于有两个变化维度(即两个变化的原因)的系统,采用方案2来进行设计系统中类的个数更少,且系统扩展更为方便。
桥接模式将继承关系转换为关联关系,从而降低了类与类之间的耦合,减少了代码编写量。
桥接模式:将抽象部分与它的实现部分分离,使它们都可以独立地变化
它是一种对象结构型模式,又称为柄体模式或接口模式
代码
以下是一个简单的桥接模式的代码示例,展示了如何使用桥接模式来实现一个形状(Shape)和颜色(Color)之间的桥接关系:
# 实现部分接口
class Color:
def fill(self):
pass
# 具体实现部分类
class RedColor(Color):
def fill(self):
return "填充红色"
class BlueColor(Color):
def fill(self):
return "填充蓝色"
# 抽象部分接口
class Shape:
def __init__(self, color):
self.color = color
def draw(self):
pass
# 具体抽象部分类
class SquareShape(Shape):
def draw(self):
return f"绘制正方形,{self.color.fill()}"
class CircleShape(Shape):
def draw(self):
return f"绘制圆形,{self.color.fill()}"
# 使用桥接模式
red_color = RedColor()
blue_color = BlueColor()
square_shape = SquareShape(red_color)
print(square_shape.draw()) # 输出:绘制正方形,填充红色
circle_shape = CircleShape(blue_color)
print(circle_shape.draw()) # 输出:绘制圆形,填充蓝色
在上面的示例中,Color
是实现部分接口,RedColor
和 BlueColor
是具体实现部分类。Shape
是抽象部分接口,SquareShape
和 CircleShape
是具体抽象部分类。
通过在抽象部分中包含一个实现部分的对象,将抽象部分和实现部分连接起来。在创建具体抽象部分类的实例时,需要传入一个具体实现部分类的实例作为参数。
通过调用抽象部分的方法,并委托给实现部分的对象来完成具体的操作。这样,在不改变抽象部分和实现部分的代码的情况下,可以通过更换具体实现部分类的实例,来改变系统的行为。
在上述示例中,SquareShape
和 CircleShape
分别绘制正方形和圆形,并通过 color.fill()
调用委托给具体实现部分类的 fill()
方法来填充颜色。
这个示例展示了桥接模式的基本思想和结构,它通过将抽象部分和实现部分解耦,使得它们可以独立地变化和扩展。通过桥接模式,可以更灵活地设计和组织系统,使得系统具有更好的可扩展性和可维护性。
实例一:自动茶水销售机
实例二:不同度量的体积计算问题
实例三:跨平台视频播放器
优缺点
优点:
满足开闭原则
- 分离抽象接口及其实现部分。
- 桥接模式有时类似于多继承方案,但是多继承方案违背了类的单一职责原则(即一个类只有一个变化的原因),复用性比较差,而且多继承结构中类的个数非常庞大,桥接模式是比多继承方案更好的解决方法。
- 桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。
- 实现细节对客户透明,可以对用户隐藏实现细节。
缺点:
- 桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
- 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性。
使用环境
- 需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系
- 抽象化角色和实现化角色可以以继承的方式独立扩展而互不影响
- 存在两个独立变化的维度
- 不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统
关于桥接模式的讨论
适配器模式与桥接模式的联用
桥接模式用于系统的初步设计,当发现系统与已有类无法协同工作时,可以采用适配器模式
课程作业
行为型软件设计模式
概述
行为型设计模式是软件设计模式中的一类,用于处理对象之间的交互和通信。这些模式关注的是对象之间的行为和职责分配。以下是几种常见的行为型设计模式:
-
观察者模式(Observer Pattern):定义了一种一对多的依赖关系,当一个对象的状态发生变化时,其依赖对象将自动收到通知并进行相应的更新。
-
策略模式(Strategy Pattern):定义了一系列算法,并将其封装在可互换的策略对象中,使得算法的选择可以独立于使用它们的客户端。
-
迭代器模式(Iterator Pattern):提供一种顺序访问聚合对象中各个元素的方法,而无需暴露聚合对象的内部表示。
-
命令模式(Command Pattern):将请求封装成对象,使得可以将不同的请求、队列或者日志来参数化其他对象,并且能够支持撤销操作。
-
状态模式(State Pattern):允许对象在内部状态改变时改变它的行为,看起来就像是改变了它的类。
-
责任链模式(Chain of Responsibility Pattern):将请求的发送者和接收者解耦,使得多个对象都有机会处理请求,将这些对象连接成一条链,并沿着这条链传递请求,直到有对象处理它。
-
模板方法模式(Template Method Pattern):定义了一个算法的骨架,将一些步骤的具体实现延迟到子类中。
-
访问者模式(Visitor Pattern):定义了对某个对象结构中各元素的操作,可以在不改变元素类的前提下定义新的操作。
这些行为型设计模式提供了一些通用的解决方案,可以帮助开发人员更好地组织和管理对象之间的交互,使得系统更加灵活、可扩展和易于维护。不同的模式适用于不同的场景,开发人员可以根据具体需求选择合适的设计模式来解决问题。
动机
行为型软件设计模式关心算法和对象之间的责任分配,不仅是描述对象或类模式,更加侧重描述它们之间的通信模式
内容
迭代器模式抽象了访问和遍历一个集合中的对象的方式
访问者模式封装了分布于多个类之间的行为
中介者模式通过在对象间引入一个中介对象,避免对象间的显式引用
策略模式将算法封装在对象中,这样可以方便指定或改变一个对象使用的算法
状态模式封装了兑现过的状态,使得当对象的状态发生变化时,该对象可以改变自身的行为
一、迭代器模式
概念
怎样遍历一个聚合对象,又不需要了解聚合对象的内部结构,还能够提供多种不同的遍历方式?
迭代器模式的关键思想是
将对列表的访问和遍历从列表对象中分离出来,放入一个独立的迭代对象中
类图
代码
迭代器模式(Iterator Pattern)是一种行为型设计模式,它提供一种顺序访问聚合对象中各个元素的方法,而无需暴露聚合对象的内部表示。迭代器模式将遍历聚合对象的责任分离出来,使得聚合对象和迭代器对象可以独立地变化。
下面是一个简单的迭代器模式的代码示例,以便更好地理解:
// 定义迭代器接口
public interface Iterator {
boolean hasNext();
Object next();
}
// 定义具体的聚合对象
public class ConcreteAggregate implements Iterable {
private List<Object> elements = new ArrayList<>();
public void addElement(Object element) {
elements.add(element);
}
public Object getElement(int index) {
return elements.get(index);
}
public int getSize() {
return elements.size();
}
// 返回迭代器对象
@Override
public Iterator iterator() {
return new ConcreteIterator(this);
}
}
// 定义具体的迭代器对象
public class ConcreteIterator implements Iterator {
private ConcreteAggregate aggregate;
private int index;
public ConcreteIterator(ConcreteAggregate aggregate) {
this.aggregate = aggregate;
this.index = 0;
}
// 判断是否还有下一个元素
@Override
public boolean hasNext() {
return index < aggregate.getSize();
}
// 返回当前元素并移动到下一个位置
@Override
public Object next() {
if (hasNext()) {
return aggregate.getElement(index++);
}
return null;
}
}
// 客户端代码
public class Client {
public static void main(String[] args) {
ConcreteAggregate aggregate = new ConcreteAggregate();
aggregate.addElement("Element 1");
aggregate.addElement("Element 2");
aggregate.addElement("Element 3");
Iterator iterator = aggregate.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
在上述示例中,迭代器模式包括以下几个关键组件:
-
迭代器接口(Iterator):定义了迭代器的通用操作,例如
hasNext()
方法用于判断是否还有下一个元素,next()
方法用于返回当前元素并移动到下一个位置。 -
具体聚合对象(ConcreteAggregate):具体的聚合对象实现了
Iterable
接口,并在iterator()
方法中返回了具体的迭代器对象。 -
具体迭代器对象(ConcreteIterator):具体的迭代器对象实现了迭代器接口,并通过聚合对象来遍历聚合对象中的元素。
在客户端代码中,我们首先创建一个具体聚合对象(ConcreteAggregate
),并添加一些元素。然后通过调用iterator()
方法获取具体的迭代器对象(ConcreteIterator
),使用迭代器对象遍历聚合对象中的元素并打印出来。
迭代器模式的优点包括:
- 分离了聚合对象和遍历逻辑,使得
聚合对象的结构和遍历算法可以独立变化。
- 简化了聚合对象的接口,客户端无需关心聚合对象的内部结构,只需要通过迭代器进行遍历操作。
- 支持多种遍历方式,可以定义不同类型的迭代器来满足不同的遍历需求。
总之,迭代器模式通过将遍历操作从聚合对象中分离出来,提供了一种统一的访问元素的方式,并且使得聚合对象和迭代器对象可以独立地变化。这样可以简化代码结构,提高代码的可读性和可维护性。
实例一:
迭代器里的四个方法(一般)
1.boolean hasNext()
2.next()
3.remove()
4.getNumOfItems()
实例二:
优缺点
优点:
- 迭代器模式支持以不同的方式遍历同一个聚合,复杂的聚合可用多种方式进行遍历。
- 满足“开闭原则”的要求。
- 迭代器简化了聚合的接口。有了迭代器的遍历接口,聚合本身就不需要类似的遍历接口了,这样就简化了聚合的接口。
缺点:
- 由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类
- 类的个数成对增加,这在一定程度上增加了系统的复杂性。
模式适用环境
在以下情况下可以使用迭代器模式:
- 访问一个聚合对象的内容而无须暴露它的内部表示。
- 需要为聚合对象提供多种遍历方式。
- 为遍历不同的聚合结构提供一个统一的接口。
课程作业
在例4.4中增加一个迭代器,按照斜对角线迭代遍历矩阵。请画出类图
二、访问者模式
概念
- 对于系统中的某些对象,它们存储在同一个集合中,具有不同的类型
- 对于该集合中的对象,可以接受一类被称为访问者的对象来访问
- 不同的访问者其访问方式有所不同
目的:
封装一些施加于某种数据结构元素之上的操作,一旦这些操作需要修改的话,接受这个操作的数据结构可以保持不变
模式动机:
为不同类型的元素提供多种访问操作方式,且可以在不修改原有系统的情况下增加新的操作方式
缺点:将所有的税率运算分布到不同的结点类使得系统不容易理解、维护与改变。
如果要增加一个新的功能,则要在很多结点类都写入新的代码,且要重新编译所有的类。
当税率变化时要修改每个结点上的calculateTax()代码。
因此,该设计的可扩展性和可维护性都不好。
解决方案:分离“Tax”类及其“calculateTax()”功能。
将所有结点的税额计算方法都从原来的类中分离出来,放入另外一个独立的类,叫做“TaxCalculation”类,设计图如下。
设计思想:将原来分布于各结点中的计算税额的方法都放在一个TaxCalculation类中。当要计算某个结点的税额时,则调用TaxCalculation中某个相应的方法进行计算。
类图
访问者模式:表示一个作用于某对象结构中的个元素的操作,它使我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
访问者模式是一种对象行为型模式
首先分为两个部分,一是元素类,而是访问者类
先抽象再具体
在访问者类里要有访问元素的方法visitXXX(抽象元素父类做参数)之类的
在元素类里要有接受访问的方法,比如accept(抽象访问者父类做参数)等
实例一:名牌运动鞋专卖店销售软件
在客户程序中将直接创建运动鞋子类与访问者子类的对象,然后直接调用运动鞋子类的 accept 方法。根据用户输入的所购买鞋的数量和单价来计算总价和获得相应特点的功能分别由 Visitor 类的两个子类 PriceVisitor 和 ShoeInfoVisitor 来实现。访问者类图如下。
实例二:计算机部件销售软
优缺点
优点:
- 使得增加新的访问操作变得很容易。
- 将有关元素对象的访问行为集中到一个访问者对象中,而不是分散到一个个的元素类中。
- 可以跨过类的等级结构访问属于不同的等级结构的元素类。
- 让用户能够在不修改现有类层次结构的情况下,定义该类层次结构的操作。
缺点:
- 增加新的元素类很困难,违背了“开闭原则”的要求。
- 破坏封装。访问者模式要求访问者对象访问并调用每一个元素对象的操作,这意味着元素对象有时候必须暴露一些自己的内部操作和内部状态,否则无法供访问者访问。
适用场合
对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作
课程作业
三、中介者模式(迪米特法则)
概念
模式动机:
为了减少对象两之间复杂的引用关系,使之成为一个松耦合的系统,需要适用中介者模式
定义:
用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使耦合松散,而且可以独立地该变它们之间的交互
中介者模式又称为调停者模式,它是一种对象行为型模式
中介者模式的要点是将所有对象之间的交互细节抽象到一个独立的类中,这个类叫做中介者类Mediator
类图
中介者承担两方面的职责
中转作用(结构性):
通过中介者提供的中转作用,各个同事对象就不再需要显式引用其他同事,当需要和其他同事进行通信时,通过中介者即可。
该中转作用属于中介者在结构上的支持。
协调作用(行为性):
中介者可以更进一步的对同事之间的关系进行封装,同事可以一致地和中介者进行交互,而不需要指明中介者需要具体怎么做,中介者根据封装在自身内部的协调逻辑,对同事的请求进行进一步处理,将同事成员之间的关系行为进行分离和封装。
该协调作用属于中介者在行为上的支持。
实例一:旅游项目信息共享软件的设计
实例二:海岛微型飞机场
优缺点
优点:
缺点:
在具体中介者类中包含了同事之间的交互细节,可能会导致具体中介者类非常复杂,使得系统难以维护
适用场合
- 系统中对象之间存在复杂的引用关系,产生的相互依赖关系结构混乱且难以理解。
- 一个对象由于引用了其他很多对象并且直接和这些对象通信,导致难以复用该对象。
- 想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。可以通过引入中介者类来实现,在中介者中定义对象交互的公共行为,如果需要改变行为则可以增加新的中介者类
中介者模式与迪米特法则
在中介者模式中,通过创造出一个中介者对象,将系统中有关的对象所引用的其他对象数目减少到最少,使得一个对象与其同事之间的相互作用被这个对象与中介者对象之间的相互作用所取代。因此,中介者模式就是迪米特法则的一个典型应用。
课程作业
四、策略模式
类图
实例
使用策略模式对中国的十二属相(Chinese Zodiac)设计查询系统。
策略模式与状态模式
课程作业
五、状态模式
概念
与策略模式类似,状态模式将不同状态下的行为封装在不同的类中,每个类代表一个状态
状态模式的组成
- Context:定义了与客户程序的接口,它保持了一个concreteState的代表现在状态的实例
- State:定义了状态接口,它的各个子类封装了在各种不同状态下的行为
- ConcreteState子类:封装了在各种不同状态下的行为
类图
交通信号控制灯的实例
课程作业