设计模式与软件体系结构复习资料——设计模式

考试题型:选择、简答、设计

软件设计模式

第一章 面向对象设计概述

1. 面向对象设计的表示方法

  (Class)封装了数据和行为,是面向对象的重要组成部分,它是具有相同属性、操作、关系的对象集合的总称。

  在系统中,每个类具有一定的职责,职责指的是类所担任的任务,即类要完成什么样的功能,要承担什么样的义务。一个类可以有多种职责,设计得好的类一般只有一种职责,在定义类的时候,将类的职责分解成为类的属性和操作(即方法)

  类之间的关系

  1. 关联
    在这里插入图片描述
  2. 聚合/组合
    在这里插入图片描述
      组合是一种较为紧密的关系,从生命周期上看,部分和整体是共存亡的关系。聚合则是一种较为松散的关系,部分和整体的生命周期未必一致。
  3. 泛化
    在这里插入图片描述
  4. 接口和实现
    5.

2. 优秀软件系统特性

  软件工程和建模大师Peter Coad认为,一个好的系统设计应该具备如下三个性质:

  • 可扩展性(Extensibility)
  • 灵活性(Flexibility)
  • 可插拔性(Pluggability)

软件的可维护性和可复用性

  • 软件的复用(Reuse)或重用拥有众多优点,如可以提高软件的开发效率,提高软件质量,节约开发成本,恰当的复用还可以改善系统的可维护性
  • 面向对象设计复用的目标在于实现支持可维护性的复用
  • 在面向对象的设计里面,可维护性复用都是以面向对象设计原则为基础的,这些设计原则首先都是复用的原则,遵循这些设计原则可以有效地提高系统的复用性,同时提高系统的可维护性。

面向对象设计原则和设计模式也是对系统进行合理重构的指南针。

  • 重构(Refactoring)是在不改变软件现有功能的基础上,通过调整程序代码改善软件的质量、性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。

3. 面向对象设计原则

设计原则名称设计原则简介重要性
单一职责原则类的职责要单一,不能将太多的职责放在一个类中★★★★☆
开闭原则软件实体对扩展是开放的,但对修改是关闭的,即在不修改一个软件实体的基础上去扩展其功能★★★★★
里氏代换原则在软件系统中,一个可以接受基类对象的地方必然可以接受一个子类对象★★★★☆
依赖倒转原则针对抽象层编程,而不要针对具体类编程★★★★★
接口隔离原则使用多个专门的接口来取代一个统一的接口★★☆☆☆
合成复用原则在系统中应该尽量多使用组合和聚合关联关系,尽量少使用甚至不使用继承关系★★★★☆
迪米特法则一个软件实体对其他实体的引用越少越好,或者说如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,而是通过引入一个第三者发生间接交互★★★☆☆

3.1 单一职责原则

  定义:一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中

  一个类(或者大到模块,小到方法)承担的职责越多,它被复用的可能性越小,而且如果一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作。

  单一职责原则是实现高内聚、低耦合的指导方针,在很多代码重构手法中都能找到它的存在,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关重构经验。

示例:
  某个基于Java的C/S系统的“登录功能”通过如下登录类(Login)实现:
在这里插入图片描述
  请思考:使用单一职责原则对其进行重构。
解析:
请添加图片描述


3.2 开闭原则

  开闭原则由Bertrand Meyer于1988年提出,它是面向对象设计中最重要的原则之一。
开闭原则定义如下:

  • 一个软件实体应当对扩展开放,对修改关闭
  • 也就是说在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展,即在不修改源代码的情况下改变这个模块的行为。

  开闭原则还可以通过一个更加具体的“对可变性封装原则”来描述,
  可变性封装原则要求找到系统的可变因素并将其封装起来

示例:
  某图形界面系统提供了各种不同形状的按钮,客户端代码可针对这些按钮进行编程,用户可能会改变需求要求使用不同的按钮,原始设计方案如图所示:
在这里插入图片描述
请对该系统进行重构,使之满足开闭原则的要求。
解析:
请添加图片描述


3.3 里氏代换原则

  里氏代换原则由2008年图灵奖得主、美国第一位计算机科学女博士、麻省理工学院教授Barbara Liskov和卡内基.梅隆大学Jeannette Wing教授于1994年提出。

里氏代换原则有两种定义方式,第一种定义方式相对严格,其定义如下:

  • 如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有变化,那么类型S是类型T的子类型。

第二种更容易理解的定义方式如下:

  • 所有引用基类(父类)的地方必须能透明地使用其子类的对象。

  里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。

示例:
在这里插入图片描述
  如果需要更换一个加密算法类或者增加并使用一个新的加密算法类,如将CipherA改为CipherB,则需要修改客户类Client和数据操作类DataOperator的源代码,违背了开闭原则。请使用里氏代换原则对其进行重构,使得系统可以灵活扩展,符合开闭原则。

解析:


3.4 依赖倒转原则

依赖倒转原则的定义如下:

  • 高层模块不应该依赖低层模块,它们都应该依赖抽象
  • 抽象不应该依赖于细节,
  • 细节应该依赖于抽象

另一种表述为:要针对接口编程,不要针对实现编程

简单来说,依赖倒转原则就是指:

  • 代码要依赖于抽象的类,而不要依赖于具体的类;
  • 要针对接口或抽象类编程,而不是针对具体类编程。

  实现开闭原则的关键是抽象化,并且从抽象化导出具体化实现,如果说开闭原则是面向对象设计的目标的话,那么依赖倒转原则就是面向对象设计的主要手段

  依赖倒转原则的常用实现方式之一是在代码中使用抽象类,而将具体类放在配置文件中,即控制反转,其具体实现例如:

  • 依赖注入
    • 构造注入(Constructor Injection):通过构造函数注入实例变量。
    • 设值注入(Setter Injection):通过Setter方法注入实例变量。
    • 接口注入(Interface Injection):通过接口方法注入实例变量。

示例:
在这里插入图片描述
  某系统提供一个数据转换模块,可以将来自不同数据源的数据转换成多种格式,如:可以转换来自数据库的数据(DatabaseSource)、也可以转换来自文本文件的数据(TextSource),转换后的格式可以是XML文件(XMLTransformer)、也可以是XLS文件(XLSTransformer)等。
  由于需求的变化,该系统可能需要增加新的数据源或者新的文件格式,每增加一个新的类型的数据源或者新的类型的文件格式,客户类MainClass都需要修改源代码,以便使用新的类,但违背了开闭原则。
  请使用依赖倒转原则对其进行重构。
解析:
请添加图片描述


3.5 接口隔离原则

  接口隔离原则的定义如下:客户端不应该依赖那些它不需要的接口。
另一种定义方法如下:一旦一个接口太大,则需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。

  接口隔离原则是指使用多个专门的接口,而不使用单一的总接口。每一个接口应该承担一种相对独立的角色,不多不少,不干不该干的事,该干的事都要干。

  1. 一个接口就只代表一个角色,每个角色都有它特定的一个接口,此时这个原则可以叫做“角色隔离原则”。
  2. 接口仅仅提供客户端需要的行为,即所需的方法,客户端不需要的行为则隐藏起来,应当为客户端提供尽可能小的单独的接口,而不要提供大的总接口。

  使用接口隔离原则拆分接口时,首先必须满足单一职责原则,将一组相关的操作定义在一个接口
中,且在满足高内聚的前提下,接口中的方法越少越好。
  可以在进行系统设计时采用定制服务的方式,即为不同的客户端提供宽窄不同的接口,只提供用户需要的行为,而隐藏用户不需要的行为。


3.6 合成复用原则

  合成复用原则,又称为组合/聚合复用原则,其定义如下:

  • 尽量使用对象组合,而不是继承来达到复用的目的。
  • 合成复用原则就是指在一个新的对象里通过组合关系和聚合关系来使用一些已有的对象,使之成为新对象的一部分;新对象通过委派调用已有对象的方法达到复用其已有功能的目的。

  简言之:要尽量使用组合/聚合关系,少用继承。

  在面向对象设计中,可以通过两种基本方法在不同的环境中复用已有的设计和实现,即通过组合/
聚合关系或通过继承。

  • 继承复用:实现简单,易于扩展。破坏系统的封装性;从基类继承而来的实现是静态的,不可能在运行时发生改变,没有足够的灵活性;只能在有限的环境中使用。(“白箱”复用 )
  • 组合/聚合复用耦合度相对较低,选择性地调用成员对象的操作;可以在运行时动态进行。(“黑箱”复用 )

  组合/聚合可以使系统更加灵活类与类之间的耦合度降低,一个类的变化对其他类造成的影响相对较少,因此一般首选使用组合/聚合来实现复用
  其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用

示例:
在这里插入图片描述
  某教学管理系统部分数据库访问类设计如图所示。
  若需要更换数据库连接方式,例如原来采用JDBC连接数据库,现在采用数据库连接池连接,则需要修改DBUtil类源代码。
  如果StudentDAO采用JDBC连接,但是TeacherDAO采用连接池连接,则需要增加一个新的DBUtil类,并修改StudentDAO或TeacherDAO的源代码,使之继承新的数据库连接类,这将违背开闭原则,系统扩展性较差。
  请使用合成复用原则对其进行重构。
解析:

在这里插入图片描述


3.7 迪米特法则

  迪米特法则又称为最少知识原则,它有多种定义方法,其中几种典型定义如下:

  1. 不要和“陌生人”说话。
  2. 只与你的直接朋友通信。
  3. 每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。

  迪米特法则来自于1987年秋美国东北大学(Northeastern University)一个名为“Demeter”的研究项目。
  简单地说,迪米特法则就是指一个软件实体应当尽可能少的与其他实体发生相互作用。这样,当一个模块修改时,就会尽量少的影响其他的模块,扩展会相对容易。这是对软件实体之间通信的限制,它要求限制软件实体之间通信的宽度和深度

在迪米特法则中,对于一个对象,其朋友包括以下几类:

  1. 当前对象本身(this);
  2. 以参数形式传入到当前对象方法中的对象
  3. 当前对象的成员对象
  4. 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友
  5. 当前对象所创建的对象

任何一个对象,如果满足上面的条件之一,就是当前对象的“朋友”,否则就是“陌生人”


第二章 创建型软件设计模式

模式:

  • 在某一背景下某个问题的一种解决方案。
  • 每个模式都描述了一个在环境中会不断重复出现的问题,并进而叙述了这个问题解决方案的要素,
  • 通过这种方式,解决方案能百万次的反复应用,但是具体方式又不会完全相同。

  根据其目的(模式是用来做什么的),面向对象的领域的设计模式可分为创建型(Creational),结构型(Structural)和行为型(Behavioral)三种:

  • 创建型模式主要用于创建对象
  • 结构型模式主要用于处理类或对象的组合
  • 行为型模式主要用于描述对类或对象怎样交互和怎样分配职责

  创建型模式(Creational Pattern)对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离
  为了使软件的结构更加清晰,外界对于这些对象只需要知道它们共同的接口,而不清楚其具体的实现细节,使整个系统的设计更加符合单一职责原则。

  创建型模式在创建什么(What),由谁创建(Who),何时创建(When)等方面都为软件设计者提供了尽可能大的灵活性。
  创建型模式隐藏了类的实例的创建细节通过隐藏对象如何被创建和组合在一起 达到使整个系统独立的目的

创建型模式的目标和内容:

  • 目标
      其目的是在哪个对象被创建、谁负责创建对象、怎样创建对象、何时创建对象等方面增强灵活性。
  • 内容
      创建型软件设计模式是解决对象创建机制的设计模式。

1. 工厂模式

1.1 简单工厂方法模式

  简单工厂模式:又称为静态工厂方法模式
  在简单工厂模式中,可以根据参数的不同返回不同类的实例
  简单工厂模式专门定义一个类来负责创建其他类的实例被创建的实例通常都具有共同的父类

示例一
  某电视机厂专为各知名电视机品牌代工生产各类电视机,当需要海尔牌电视机时只需要在调用该工厂的工厂方法时传入参数“Haier”,需要海信电视机时只需要传入参数“Hisense”,工厂可以根据传入的不同参数返回不同品牌的电视机。
  请使用简单工厂方法模式来模拟该电视机工厂的生产过程。
解析:
请添加图片描述

优点

  • 工厂类含有必要的判断逻辑,可以决定在什么时候创建哪一个产品类的实例,客户端可以免除直接创建产品对象的责任,而仅仅“消费”产品;简单工厂方法模式通过这种做法实现了对责任的分割,它提供了专门的工厂类用于创建对象。
  • 客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可,对于一些复杂的类名,通过简单工厂模式可以减少使用者的记忆量。
  • 通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性

缺点

  • 由于工厂类集中了所有产品创建逻辑,一旦不能正常工作,整个系统都要受到影响。
  • 使用简单工厂方法模式将会增加系统中类的个数,在一定程序上增加了系统的复杂度和理解难度。
  • 系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。
  • 简单工厂方法模式由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构。

适用环境

  • 工厂类负责创建的对象比较少:由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。
  • 客户端只知道传入工厂类的参数,对于如何创建对象不关心:客户端既不需要关心创建细节,甚至连类名都不需要记住,只需要知道类型所对应的参数。

1.2 工厂方法模式

  在简单工厂模式中,只提供了一个工厂类,该工厂类处于对产品类进行实例化的中心位置,它知道每一个产品对象的创建细节,并决定何时实例化哪一个产品类。
  简单工厂模式最大的缺点是当有新产品要加入到系统中时,必须修改工厂类,加入必要的处理逻辑,这违背了“开闭原则”。
  在简单工厂模式中,所有的产品都是由同一个工厂创建,工厂类职责较重,业务逻辑较为复杂,具体产品与工厂类之间的耦合度高,严重影响了系统的灵活性和扩展性。
  为克服简单工厂方法模式的不足,人们试图改善工厂类结构以解决这一问题。

  软件设计者们发现,可以将简单工厂方法模式中单一的工厂类改写为一个层次类来解决这个问题。

  • 首先需要一个接口作为超类,名为factory,接口中有一个方法,叫做create();
  • 然后可以用和产品类相同的结构产生创建者类结构,其中包含factoryA和factoryB,各自负责创建相应的ProductA和ProductB的对象。
    请添加图片描述
      工厂方法模式是简单工厂模式的进一步抽象和推广。由于使用了面向对象的多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。
      在工厂方法模式中,核心的工厂类不再负责所有产品的创建,而是将具体创建工作交给子类去做这个核心类仅仅负责给出具体工厂必须实现的接口,而不负责哪一个产品类被实例化这种细节,
      这使得工厂方法模式可以允许系统在不修改工厂角色的情况下引进新产品
      当系统扩展需要添加新的产品对象时,仅仅需要添加一个具体产品对象以及一个具体工厂对象,原有工厂对象不需要进行任何修改,也不需要修改客户端,很好地符合了“开闭原则。而简单工厂模式在添加新产品对象后不得不修改工厂方法,扩展性不好。
      工厂方法模式退化后可以演变成简单工厂模式

示例二
请添加图片描述
  用工厂方法模式重构上述设计,当需要生产新品牌电视机,原有的工厂无须做任何修改,使得整个系统具有更加的灵活性和可扩展性。
解析:
请添加图片描述

优点
  在工厂方法模式中,工厂方法用来创建客户所需要的产品,同时还向客户隐藏了哪种具体产品类将被实例化这一细节,用户只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须知道具体产品类的类名。
  基于工厂角色和产品角色的多态性设计是工厂方法模式的关键。它能够使工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。工厂方法模式之所以又被称为多态工厂模式,是因为所有的具体工厂类都具有同一抽象父类。
  使用工厂方法模式的另一个优点是在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品就可以了。这样,系统的可扩展性也就变得非常好,完全符合“开闭原则

缺点
  在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。
  由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,且在实现时可能需要用到DOM、反射等技术,增加了系统的实现难度。

简单工厂模式与工厂方法模式的区别

  1. 两个模式的中心不同。工厂方法模式的中心是抽象工厂类或者接口,而简单工厂方法模式的中心是一个实的工厂类(Concrete Factory Class)。在简单工厂模式类中,工厂方法是静态的( Static),而在工厂模式中工厂方法是动态的(Dynamic )。
  2. 简单工厂模式不支持开闭原则,工厂方法模式支持开闭原则。在简单工厂模式中,如果要增加一个新产品类,相应地在工厂类中也要增加一个条件语句,用于创建新的产品类的对象。也就是说,必须修改工厂类的源代码。因此简单工厂模式不支持开闭原则。在工厂方法模式中,增加新产品,只需要在Product类的结构体中增加一个实类,并且在工厂类层次结构体中增加一个相应的能产生该新产品类对象的实类。这种模式不需要修改客户类( Client Class) ,无需修改或者重新编译抽象的工厂方法类与已经存在的具体工厂方法类。这样,在无需修改或者重新编译已经存在的代码的情况下,可以添加新的产品类(当然必须也要同时添加工厂方法类)。因此,工厂方法模式支持开闭原则。
  3. 在简单工厂模式中,必要的创建对象的逻辑判断包含在工厂类中在工厂方法模式中,工厂类不必包含创建对象的逻辑判断

适用场景
  一个类不知道它所需要的对象的类:在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。
  一个类通过其子类来指定创建哪个对象:在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。
  将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无须关心是哪一个工厂子类创建产品子类,需要时再动态指定,可将具体工厂类的类名存储在配置文件或数据库中。


1.3 抽象工厂模式

  在工厂方法模式中具体工厂负责生产具体的产品,每一个具体工厂对应一种具体产品,工厂方法也具有唯一性,一般情况下,一个具体工厂中只有一个工厂方法或者一组重载的工厂方法。但是有时候我们需要一个工厂可以提供多个产品对象,而不是单一的产品对象

产品等级结构:——同一产品类型,不同工厂生产
  产品等级结构即产品的继承结构,如一个抽象类是电视机,其子类有海尔电视机、海信电视机、TCL电视机,则抽象电视机与具体品牌的电视机之间构成了一个产品等级结构,抽象电视机是父类,而具体品牌的电视机是其子类。
产品族:——同一工厂生产,不同产品类型
  在抽象工厂模式中,产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品,如海尔电器工厂生产的海尔电视机、海尔电冰箱,海尔电视机位于电视机产品等级结构中,海尔电冰箱位于电冰箱产品等级结构中。
在这里插入图片描述
抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。
抽象工厂模式又称为Kit模式,属于对象创建型模式。

请添加图片描述
示例
  一个电器工厂可以产生多种类型的电器,如海尔工厂可以生产海尔电视机、海尔空调等,TCL工厂可以生产TCL电视机、TCL空调等,相同品牌的电器构成一个产品族,而相同类型的电器构成了一个产品等级结构。
  请使用抽象工厂模式模拟该场景。
解析:

请添加图片描述
抽象工厂模式的可扩展性
  增加新的工厂符合开闭原则,增加新类型的产品不符合开闭原则。

优点
  抽象工厂模式隔离了具体类的生成,使得客户并不需要知道什么被创建。由于这种隔离,更换一个具体工厂就变得相对容易。
  所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。另外,应用抽象工厂模式可以实现高内聚低耦合的设计目的,因此抽象工厂模式得到了广泛的应用。
  当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。这对一些需要根据当前环境来决定其行为的软件系统来说,是一种非常实用的设计模式。
  增加新的具体工厂和产品族很方便,无须修改已有系统,符合“开闭原则”

缺点
  在添加新的产品对象时,难以扩展抽象工厂来生产新种类的产品,这是因为在抽象工厂角色中规定了所有可能被创建的产品集合,要支持新种类的产品就意味着要对该接口进行扩展,而这将涉及到对抽象工厂角色及其所有子类的修改,显然会带来较大的不便。
  开闭原则的倾斜性(增加新的工厂和产品族容易,增加新的产品等级结构麻烦)

适用场合
  一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是重要的。
  系统中有多于一个的产品族,而每次只使用其中某一产品族
  属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。
  系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于具体实现。

抽象工厂模式包含四个角色

  1. 抽象工厂用于声明生成抽象产品的方法;
  2. 具体工厂实现了抽象工厂声明的生成抽象产品的方法,生成一组具体产品,这些产品构成了一个产品族,每一个产品都位于某个产品等级结构中;
  3. 抽象产品为每种产品声明接口,在抽象产品中定义了产品的抽象业务方法;
  4. 具体产品定义具体工厂生产的具体产品对象,实现抽象产品接口中定义的业务方法。

1.4 练习
  1. 在简单工厂方法模式、工厂方法模式与抽象工厂模式中,哪个模式符合开闭原则?为什么?哪个模式一般不符合开闭原则?或者说明在哪种情况下,开闭原则成立,哪种情况下,开闭原则不成立。

答:简单工厂方法模式一般不符合开闭原则;工厂方法模式符合开闭原则;抽象工厂模式在增加新的工厂和产品族时符合开闭模式,在增加新的产品等级结构时不符合开闭模式。

  1. 在图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. 抽象工厂模式

请添加图片描述

  1. MedGameFactory类和SuperGameFactory类
  2. 新设计的类图采用抽象工厂模式设计,SlowGameFactory工厂、MedGameFactory工厂和SuperGameFactory工厂继承自GameAbstFactory;SlowFighter、MedFighter和SuperFighter继承自Fighter;SlowMonster、MedMonster和SuperMonster继承自Monster;SlowGameFactory工厂负责创建SlowFighter和SlowMonster,MedGameFactory工厂负责创建MedFighter和MedMonster,SuperGameFactory工厂负责创建SuperFighter和SuperMonster;
  3. 新设计的类图在增加新的怪物和战士时符合开闭原则,可以在不修改原有代码的基础上完成扩展。

2. 生成器模式

  无论是在现实世界中还是在软件系统中,都存在一些复杂的对象,它们拥有多个组成部分,如:汽车的组成包括底盘(Chassis)、外壳(Frame)、轮子(Wheels)、发动机(Engine)、邮箱(GasTank)、加速器(Accelerater)、刹车(Brake)、方向盘(SteeringWheel)、电池(Battery)、影像(Audio)、天窗(Sunroof)、巡航控制(Cruise Control)、卫星导航系统(GPS),等等。
  而对于大多数用户而言,无须知道这些部件的装配细节,也几乎不会使用单独某个部件,而是使用一辆完整的汽车,
  可以通过生成器模式对其进行设计与描述,生成器模式可以将部件和其组装过程分开,一步一步创建一个复杂的对象用户只需要指定复杂对象的类型就可以得到该对象,而无须知道其内部的具体构造细节。

  在软件开发中,也存在大量类似汽车一样的复杂对象,它们拥有一系列成员属性,这些成员属性中有些是引用类型的成员对象。而且在这些复杂对象中,还可能存在一些限制条件,如某些属性没有赋值则复杂对象不能作为一个完整的产品使用;有些属性的赋值必须按照某个顺序,一个属性没有赋值之前,另一个属性可能无法赋值等。
  复杂对象相当于一辆有待建造的汽车,而对象的属性相当于汽车的部件,建造产品的过程就相当于组合部件的过程。由于组合部件的过程很复杂,因此,这些部件的组合过程往往被“外部化”到一个称作Builder的对象里,Builder返还给客户端的是一个已经建造完毕的完整产品对象,而用户无须关心该对象所包含的属性以及它们的组装方式。

请添加图片描述
示例
  某公司要设计一个房屋选购系统,系统内的房屋分为两种类型:普通型(Normal House)与豪华型(Luxury House)。不同房屋型的区别体现在面积(Area)大小以及卧室(Bedroom)、卫生间(Bathroom)、车库(Garage)、花园(Garden)和游泳池(Swimming Pool)的数量上。根据用户的选择,具体选择房屋的各种指标。本程序采用生成器模式,设计类图如下:

请添加图片描述

优点
  在生成器模式中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象
  每一个具体生成器都相对独立,而与其他的具体生成器无关,因此可以很方便地替换具体生成器或增加新的具体生成器,用户使用不同的具体生成器即可得到不同的产品对象。
  可以更加精细地控制产品的创建过程。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。
  增加新的具体生成器无须修改原有类库的代码,指挥者类针对抽象生成器类编程,系统扩展方便,符合“开闭原则”。

缺点
  生成器模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用生成器模式,因此其使用范围受到一定的限制。
  如果产品的内部变化复杂,可能会导致需要定义很多具体生成器类来实现这种变化,导致系统变得很庞大。

适用场合
  需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个成员属性。
  需要生成的产品对象的属性相互依赖,需要指定其生成顺序
  对象的创建过程独立于创建该对象的类。在生成器模式中引入了指挥者类,将创建过程封装在指挥者类中,而不在生成器类中。
  隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品

生成器模式与抽象工厂模式
  与抽象工厂模式相比,生成器模式返回一个组装好的完整产品,而抽象工厂模式返回一系列相关的产品,这些产品位于不同的产品等级结构,构成了一个产品族。
  在抽象工厂模式中,客户端实例化工厂类,然后调用工厂方法获取所需产品对象,而在生成器模式中,客户端可以不直接调用生成器的相关方法,而是通过指挥者类来指导如何生成对象,包括对象的组装过程和建造步骤,它侧重于一步步构造一个复杂对象,返回一个完整的对象。
  如果将抽象工厂模式看成汽车配件生产工厂,生产一个产品族的产品,那么生成器模式就是一个汽车组装工厂,通过对部件的组装可以返回一辆完整的汽车。


2.1 练习

  在书本P27页房屋选购系统例子的设计中,添加一个经济型房屋生成器类,命名为EconHouseBuilder。 请绘制新设计的类图。
解:

请添加图片描述


3. 单例模式

  对于系统中的某些类来说,只有一个实例很重要,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个窗口管理器或文件系统;一个系统只能有一个计时工具或ID(序号)生成器。

  单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供一个全局访问点。
  单例模式的要点有三个:

  1. 某个类只能有一个实例
  2. 必须自行创建这个实例
  3. 必须自行向整个系统提供这个实例

单例模式是一种对象创建型模式。单例模式又名单件模式或单态模式。

基本思路
  将构造方法声明为private类型,其他类无法使用该构造方法来创建对象
  提供一个可以获得实例的getInstance方法,该方法是静态方法,确保客户程序得到的始终是同一个对象

请添加图片描述

示例
  考虑一个单位的互联网连接问题。该单位对外的互联网使用一个统一的IP地址,所有的内部用户都使用一个统一的内部服务器。当一个用户要连接到互联网上时,该用户应首先连接到内部服务器上。显然,如果一个用户已经开始了一个连接,该用户不可能再有第二个连接。
在这里插入图片描述
  单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时有两个线程同时调用创建方法,那么就将导致两个线程各自创建了一个实例,从而违反了单例模式的实例唯一性的初衷。为了改善以上的设计,可以将设计中的getInstance()方法声明为synchronized类型的。
在这里插入图片描述

优点
  提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它,并为设计及开发团队提供了共享的概念。
  由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
  允许可变数目的实例。我们可以基于单例模式进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例。

缺点
  由于单例模式中没有抽象层,因此单例类的扩展有很大的困难
  单例类的职责过重,在一定程度上违背了“单一职责原则。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
  滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失。

适用环境
  系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器,或者需要考虑资源消耗太大而只允许创建一个对象。
  客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
  在一个系统中要求一个类只有一个实例时才应当使用单例模式。反过来,如果一个类可以有几个实例共存,就需要对单例模式进行改进,使之成为多例模式。


3.1 练习
  1. 使用单例模式设计一个类图来表示随机数产生器(Randomer),整个应用程序中只需要一个类的实例来产生随机数(Random),客户端程序(Client)从类中获取这个实例,调用这个实例的方法有设置起始数方法setSeed()和随机产生下一个数方法nextInt()。请按要求画出类图及其关键属性和操作。

解: 请添加图片描述

  1. 用单例模式设计一个打印机管理软件(Spooler),每台计算机可以有若干个打印机(Printer),但只能有一个打印机管理软件(Spooler),以避免两个打印作业同时输出到打印机中。客户端从类中获取Spooler的实例,调用这个实例的方法打印作业PrintJob(File job)。请按要求画出类图及其关键属性和操作。

解:
请添加图片描述


第三章 结构型软件设计模式

动机:
  结构型软件设计模式的主要目的是将不同的类和对象组合在一起,形成更大或者更复杂的结构体。
内容:

  1. 组合模式将一个或者多个相似的对象构成组合对象
  2. 适配器模式提供一种机制改变原来不适合使用的接口
  3. 外观模式新建一个外观类,通过调用原有的类库中众多的类的方式,实现外观类的所有方法

1. 组合模式

动机
  对于树形结构,当容器对象(如文件夹)的某一个方法被调用时,将遍历整个树形结构,寻找也包含这个方法的成员对象(可以是容器对象,也可以是叶子对象,如子文件夹和文件)并调用执行。(递归调用
  由于容器对象和叶子对象在功能上的区别,在使用这些对象的客户端代码中必须有区别地对待容器对象和叶子对象,而实际上大多数情况下客户端希望一致地处理它们,因为对于这些对象的区别对待将会使得程序非常复杂。
  组合模式描述了如何将容器对象和叶子对象进行递归组合,使得用户在使用时无须对它们进行区分,可以一致地对待容器对象和叶子对象,这就是组合模式的模式动机。

定义
  组合模式(Composite Pattern):组合多个对象形成树形结构以表示“整体-部分”的结构层次。组合模式对单个对象(即叶子对象)和组合对象(即容器对象)的使用具有一致性。
  组合模式又可以称为“整体-部分”(Part-Whole)模式,属于对象的结构模式,它将对象组织到树结构中,可以用来描述整体与部分的关系。

结构
Component:为组合模式中的对象声明接口
Leaf:在组合模式中表示叶结点对象
Composite:表示组合部件
Client:通过Component接口操纵组合部件的对象。

请添加图片描述
关于组合模式的讨论

  1. 安全形式的组合模式
      在Composite类中声明所有的用来管理子类对象的方法,包括add( )、remove( )以及getChild( )方法,而在Component接口和树叶类型的对象中不包含管理子类对象的方法
      优点是安全;缺点是Component层次结构类的超类与组合子类的接口不一致
    请添加图片描述
  2. 透明形式的组合模式
      在Component类中声明所有的用来管理子类对象的方法,包括add( )、remove( ),以及getChild( )方法。
      优点是所有的构件类都有相同的接口;缺点是不够安全
    请添加图片描述

优点
  可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,使得增加新构件也更容易。
  客户端调用简单,客户端可以一致的使用组合结构或其中单个对象。
  定义了包含叶子对象和容器对象的类层次结构,叶子对象可以被组合成更复杂的容器对象,而这个容器对象又可以被组合,这样不断递归下去,可以形成复杂的树形结构
  更容易在组合体内加入对象构件,客户端不必因为加入了新的对象构件而更改原有代码。

缺点
  使设计变得更加抽象,对象的业务规则如果很复杂,则实现组合模式具有很大挑战性,而且不是所有的方法都与叶子对象子类都有关联。
  增加新构件时可能会产生一些问题,很难对容器中的构件类型进行限制

适用环境
  需要表示一个对象整体或部分层次,在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,可以一致地对待它们。
  让客户能够忽略不同对象层次的变化,客户端可以针对抽象构件编程,无须关心对象层次结构的细节。
  对象的结构是动态的并且复杂程度不一样,但客户需要一致地处理它们


1.1 练习
  1. 使用组合模式设计一个杀毒软件(AntiVirus)的框架,该软件既可以对某个文件夹(Folder)杀毒,也可以对某个指定的文件(File)进行杀毒,文件种类包括文本文件TextFile、图片文件ImageFile、视频文件VideoFile。请画出类图。

解:请添加图片描述

  1. 如图所示是某公司的组织结构图,现采用组合设计模式来设计,其中公司(Company)为抽象类,具体公司(ConcreteCompany)表示具体的分公司或者办事处,人力资源部(HRDepartment)和财务部(FinanceDepartment)表示不同的部门,以上的类都拥有Add()方法和Delete()方法。请画出类图。

在这里插入图片描述

解:
请添加图片描述


2. 适配器模式

  在软件开发中,为了解决接口不一致的问题,两个软件模块之间往往也需要通过一个适配器类Adapter进行“适配”。这样的模式叫做适配器设计模式。
  通常情况下,客户端可以通过目标类的接口访问它所提供的服务
  有时,现有的类可以满足客户类的功能需要,但是它所提供的接口不一定是客户类所期望的,这可能是因为现有类中方法名与目标类中定义的方法名不一致等原因所导致的。
  在这种情况下,现有的接口需要转化为客户类期望的接口,这样保证了对现有类的重用。如果不进行这样的转化,客户类就不能利用现有类所提供的功能,适配器模式可以完成这样的转化。
  适配器提供客户类需要的接口,适配器的实现就是把客户类的请求转化为对适配者的相应接口的调用。
  也就是说:当客户类调用适配器的方法时,在适配器类的内部将调用适配者类的方法,而这个过程对客户类是透明的,客户类并不直接访问适配者类。
  因此,适配器可以使由于接口不兼容而不能交互的类可以一起工作

该模式可以分为两种,分别为类适配器模式(Class Adapter Pattern)和对象适配器模式(Object Adapter Pattern)。
请添加图片描述
示例
  某公司购买了一个用于验证客户信息的离架产品类InfoValidation,但是卖方没有提供源代码。该类可以用于检查客户输入的信息,包含验证姓名、地址、电话区号和手机号码等功能。如果还需要增加一个验证社会安全号(SSN)的功能,则可以使用类适配器模式来实现。
在这里插入图片描述
解析:
请添加图片描述

示例
  使用对象适配器实现字符串序列排序。要求从一个.txt文件读入一些英文字符串,并且对这些字符串进行排序。
  这里已有一个类FileInput,其主要功能包含从一个文件中读入字符串;另外,在一个Java类库中有一个Arrays,其中包含功能sort,用于对多个字符串进行排序。
在这里插入图片描述
解析:
请添加图片描述

类适配器模式与对象适配器模式的区别
  在Java语言中,使用对象适配器模式可以把多种不同的源类都适配到同一个Target接口,而使用类的适配器模式是做不到这一点的。
  如果一个被适配源类中有大量的方法,使用类适配器模式比较容易,只需要让Adapter类继承被适配的源类即可。而此时使用对象适配器模式则要在Adapter类中明确写出Target角色中的每个方法,并且在每个方法中要调用被适配的源类中的相应的方法。

优点
  将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,而无须修改原有代码。
  增加了类的透明性和复用性,将具体的实现封装在适配者类中,对于客户端类来说是透明的,而且提高了适配者的复用性。
  灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”。

  由于类适配器是适配者类的子类,因此可以在类适配器中置换一些适配者的方法,使得适配器的灵活性更强

  一个对象适配器可以把多个不同的适配者适配到同一个目标,也就是说,同一个适配器可以把适配者类和它的子类都适配到目标接口。

缺点
  对于Java、C#等不支持多重继承的语言,一次最多只能适配一个适配者类,而且目标抽象类只能为抽象类,不能为具体类,其使用有一定的局限性,不能将一个适配者类和它的子类都适配到目标接口。

  与类适配器模式相比,要想置换适配者类的方法就不容易。如果一定要置换掉适配者类的一个或多个方法,就只好先做一个适配者类的子类,将适配者类的方法置换掉,然后再把适配者类的子类当做真正的适配者进行适配,实现过程较为复杂。

适用场合

  • 系统需要使用现有的类,而这些类的接口不符合系统的需要
  • 想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。

适配器模式的扩展

  1. 默认适配器模式(Default Adapter Pattern)或缺省适配器模式
      当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求,它适用于一个接口不想使用其所有的方法的情况。因此也称为单接口适配器模式
    在这里插入图片描述
  2. 双向适配器
      在对象适配器的使用过程中,如果在适配器中同时包含对目标类和适配者类的引用,适配者可以通过它调用目标类中的方法,目标类也可以通过它调用适配者类中的方法,那么该适配器就是一个双向适配器。
    在这里插入图片描述

3. 外观模式

  引入外观角色之后,用户只需要直接与外观角色交互,用户与子系统之间的复杂关系由外观角色来实现,从而降低了系统的耦合度。

  外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
  外观模式又称为门面模式,它是一种对象结构型模式

外观模式由三个角色组成:
外观角色:外观模式的核心。它被客户角色调用,因此它熟悉子系统的功能。其内部根据客户角色已有的需求预定了几种功能组合。
子系统角色:实现子系统的功能,对它而言,外观角色就和客户角色一样是未知的,它没有任何外观角色的信息和链接
客户角色:调用外观角色来完成要得到的功能

  根据“单一职责原则”,在软件中将一个系统划分为若干个子系统有利于降低整个系统的复杂性,一个常见的设计目标是使子系统间的通信和相互依赖关系达到最小,而达到该目标的途径之一就是引入一个外观对象,它为子系统的访问提供了一个简单而单一的入口。
  外观模式也是“迪米特法则”的体现,通过引入一个新的外观类可以降低原有系统的复杂度,同时降低客户类与子系统类的耦合度。

  在外观模式中,通常只需要一个外观类,并且此外观类只有一个实例,换言之它是一个单例类。
  在很多情况下为了节约系统资源,一般将外观类设计为单例类当然这并不意味着在整个系统里只能有一个外观类,在一个系统中可以设计多个外观类,每个外观类都负责和一些特定的子系统交互,向用户提供相应的业务功能。
  不要通过继承一个外观类在子系统中加入新的行为,这种做法是错误的
  外观模式的用意是为子系统提供一个集中化和简化的沟通渠道,而不是向子系统加入新的行为

抽象外观类的引入
  外观模式最大的缺点在于违背了“开闭原则,当增加新的子系统或者移除子系统时需要修改外观类,可以通过引入抽象外观类在一定程度上解决该问题,客户端针对抽象外观类进行编程。
  对于新的业务需求,不修改原有外观类,而对应增加一个新的具体外观类,由新的具体外观类来关联新的子系统对象,同时通过修改配置文件来达到不修改源代码并更换外观类的目的。
在这里插入图片描述


3.1 练习
  1. 某系统需要提供一个文件加密模块,加密流程包括三个操作,分别是读取源文件FileReader()、加密CipherMachine()、保存加密后的文件FileWriter()。读取文件和保存文件使用流来实现,这三个操作相对独立,其业务代码封装在三个不同的类中。现在需要提供一个统一的加密外观类(EncryptFacade),用户可以直接使用该加密外观类完成源文件(fileNameSrc)的读取、读取后的文件(plainStr)的加密和加密后文件(encryptStr)的保存三个操作,而不需要与每一个类进行交互,使用外观模式设计该加密模块,要求画出类图。

解:
请添加图片描述


4. 桥接模式

  对于有两个变化维度(即两个变化的原因)的系统,采用桥接模式来进行设计系统中类的个数更少,且系统扩展更为方便。
  桥接模式将继承关系转换为关联关系,从而降低了类与类之间的耦合,减少了代码编写量。
  将抽象部分与它的实现部分分离,使它们都可以独立地变化。
  是一种对象结构型模式,又称为柄体(Handle and Body)模式接口(Interface)模式

优点

  • 分离抽象接口及其实现部分
  • 桥接模式有时类似于多继承方案,但是多继承方案违背了类的单一职责原则(即一个类只有一个变化的原因),复用性比较差,而且多继承结构中类的个数非常庞大,桥接模式是比多继承方案更好的解决方法。
  • 桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。
  • 实现细节对客户透明,可以对用户隐藏实现细节

缺点

  • 桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
  • 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性

适用环境

  • 如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。
  • 抽象化角色和实现化角色可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。
  • 一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
  • 虽然在系统中使用继承是没有问题的,但是由于抽象化角色和具体化角色需要独立变化,设计要求需要独立管理这两者。
  • 对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用

适配器模式与桥接模式的联用

  • 桥接模式和适配器模式用于设计的不同阶段,桥接模式用于系统的初步设计,对于存在两个独立变化维度的类可以将其分为抽象化和实现化两个角色,使它们可以分别进行变化;
  • 在初步设计完成之后,当发现系统与已有类无法协同工作时,可以采用适配器模式
  • 但有时候在设计初期也需要考虑适配器模式,特别是那些涉及到大量第三方应用接口的情况。

4.1 练习
  1. 现欲实现一个图像浏览系统,要求该系统能够显示BMP、 JPEG和GIF三种格式的文件,并且能够在Windows和Linux两种操作系统上运行。系统首先将BMP、JPEG和GIF三种格式的文件,使用文件解析parseFile()方法解析为像素矩阵,然后使用doPaint()方法像素矩阵显示在屏幕上。系统必须具有较好的扩展性以支持新的文件格式和操作系统。为满足上述需求并减少所需生成的子类数目,采用桥接设计模式,请画出类图。

解析:
请添加图片描述


第四章 行为型软件设计模式

动机
  行为型软件设计模式关心算法和对象之间的责任分配,不仅是描述对象或类模式,更加侧重描述它们之间的通信模式。

内容

  • 迭代器模式抽象了访问和遍历一个集合中的对象的方式;
  • 访问者模式封装了分布于多个类之间的行为;
  • 中介者模式通过在对象间引入一个中介对象,避免对象间的显式引用;
  • 策略模式将算法封装在对象中,这样可以方便指定或改变一个对象使用的算法;
  • 状态模式封装了兑现过的状态,使得当对象的状态发生变化时,该对象可以改变自身的行为。

1. 迭代器模式

  一个聚合对象,如一个列表(List)或者一个集合(Set),应该提供一种方法来让别人可以访问它的元素,而又不需要暴露它的内部结构
  针对不同的需要,可能还要以不同的方式遍历整个聚合对象,但是我们并不希望在聚合对象的抽象层接口中充斥着各种不同遍历的操作。
  怎样遍历一个聚合对象,又不需要了解聚合对象的内部结构,还能够提供多种不同的遍历方式?

  在迭代器模式中,提供一个外部的迭代器来对聚合对象进行访问和遍历,
  迭代器定义了一个访问该聚合元素的接口,并且可以跟踪当前遍历的元素,了解哪些元素已经遍历过而哪些没有。
  有了迭代器模式,我们会发现对一个复杂的聚合对象的操作会变得如此简单。

  迭代器模式的关键思想是将对列表的访问和遍历从列表对象中分离出来,放入一个独立的迭代对象中
  迭代器模式能够提供一种方法按照顺序访问一个聚合对象元素中的所有元素,而又不需要暴露该对象的内部表示

在这里插入图片描述

  • Aggregate聚合接口,其实现子类将创建并且维持一个一种数据类型的聚合体。另外,它还定义了创建相应迭代器对象的接口creaIterator。

  • ConcreteAggregate封装了一个数据存储结构,实现一个具体的聚合,如列表、java类型ArrayList等。一个聚合对象包含一些其他的对象。另外,该类提供了创建相应迭代器对象的方法createIterator,该方法返回类型为ConcreteIterator的一个对象。

  • Iterator迭代器定义访问和遍历元素的接口

  • ConcreteIterator具体迭代器实现迭代器接口,对该聚合遍历时跟踪当前位置。

示例
  随机生成一个整数矩阵(Matrix)。矩阵可以看做是聚合类型的数据。考虑对该矩阵进行两种不同方式的遍历(OddNumIterator和CircularIterator)。采用迭代器模式设计类图。

解析:
请添加图片描述
优点

  • 迭代器模式支持以不同的方式遍历同一个聚合,复杂的聚合可用多种方式进行遍历。
  • 满足“开闭原则”的要求
    • 当修改某一个遍历算法时不会影响其他的遍历算法。
    • 当修改遍历的聚合的结构代码时,如果该聚合的结构没有改变,则相应的遍历算法代码也不需要改变。
  • 迭代器简化了聚合的接口。有了迭代器的遍历接口,聚合本身就不需要类似的遍历接口了,这样就简化了聚合的接口。

缺点

  • 由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。

适用环境

  • 访问一个聚合对象的内容而无须暴露它的内部表示。
  • 需要为聚合对象提供多种遍历方式。
  • 为遍历不同的聚合结构提供一个统一的接口。
1.1 练习
  1. 使用迭代器模式设计一个类图来表示对商品列表( ProductList)的双向(前后向)遍历,遍历的方式是双向迭代器( Myiterator )。其中商店管理系统的商品名称存储在一个字符串数组( productsName)中,双向迭代器的前后向遍历方式分别为( index1、 index2),迭代器实现了抽象类的操作方法( next、 isLast、previous、 isFirst、 getNextItem、 getPreviousItem)。请按要求画出类图及其关键属性和操作。

解析:
请添加图片描述


2. 访问者模式

  • 对于系统中的某些对象,它们存储在同一个集合中,
  • 具有不同的类型
  • 对于该集合中的对象,可以接受一类称为访问者的对象来访问,
  • 不同的访问者其访问方式有所不同

访问者模式为解决这类问题而诞生。

  在实际使用时,对同一集合对象的操作并不是唯一的,对相同的元素对象可能存在多种不同的操作方式。而且这些操作方式并不稳定,可能还需要增加新的操作,以满足新的业务需求。此时,访问者模式就是一个值得考虑的解决方案。
  访问者模式的目的是:封装一些施加于某种数据结构元素之上的操作,一旦这些操作需要修改的话,接受这个操作的数据结构可以保持不变
  为不同类型的元素提供多种访问操作方式,且可以在不修改原有系统的情况下增加新的操作方式,这就是访问者模式的模式动机。

  访问者模式(Visitor Pattern):表示一个作用于某对象结构中的各元素的操作,它使我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作。访问者模式是一种对象行为型模式。

示例:
假设要实现一个计算机部件销售软件。考虑到计算机部件的种类相对固定,所以使用访问者模式进行设计。计算机部件(ComputerParts)包括Motherboard、Microprocessor、Memory、VideoCard、Monitor等。使用复合对象CompositeStructure类来表示计算机部件的集合,包含有attach、detach、accept三个方法。每个计算机部件类都有一个getPrice()方法、一个getDescription()方法与一个accept()方法;PriceVisitor通过调用getPrice()方法,达到计算价格的目的。类似地,PartsInfoVisitor类负责调用getDescription方法,实现获取部件的具体描述。

解析:
请添加图片描述

优点

  • 使得增加新的访问操作变得很容易
  • 将有关元素对象的访问行为集中到一个访问者对象中,而不是分散到一个个的元素类中。
  • 可以跨过类的等级结构访问属于不同的等级结构的元素类
  • 让用户能够在不修改现有类层次结构的情况下,定义该类层次结构的操作

缺点

  • 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类都意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作,违背了“开闭原则”的要求。
  • 破坏封装。访问者模式要求访问者对象访问并调用每一个元素对象的操作,这意味着元素对象有时候必须暴露一些自己的内部操作和内部状态,否则无法供访问者访问。

适用场合

  • 一个对象结构包含很多类型的对象,希望对这些对象实施一些依赖其具体类型的操作。在访问者中针对每一种具体的类型都提供了一个访问操作,不同类型的对象可以有不同的访问操作。
  • 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。
  • 访问者模式使得我们可以将相关的访问操作集中起来定义在访问者类中,对象结构可以被多个不同的访问者类所使用,将对象本身与对象的访问操作分离。
  • 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作
2.1 练习
  1. 假设有一个汽车配件类的层次类如下图,为方便起见,这里仅列出部分配件。由于汽车配件类的具体配件名称很少改变,而配件的价格与生产厂家却经常改变,请使用访问者模式重新设计以上的类,画出新的类图。
    在这里插入图片描述

解析:
请添加图片描述


3. 中介者模式

  在面向对象的软件设计与开发过程中,根据“单一职责原则”,我们应该尽量将对象细化,使其只负责或呈现单一的职责。
  对于一个模块,可能由很多对象构成,而且这些对象之间可能存在相互的引用,为了减少对象两两之间复杂的引用关系,使之成为一个松耦合的系统,我们需要使用中介者模式,这就是中介者模式的模式动机。

中介者模式(Mediator Pattern)定义:
  用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
  中介者模式又称为调停者模式,它是一种对象行为型模式

在这里插入图片描述
  设计类图4.36就是以上逻辑图的具体体现,叫做中介者模式设计类图。
  该设计类图由两部分组成,一部分是中介者类,另外一部分是参与者类。中介者模式的各组成部分的含义说明如下:

  • Mediator:中介者接口。
  • ConcreteMediator:具体的中介者,可以有多个具体的中介者。
  • Colleague:参与者类接口。
  • ConcreteColleague:具体的参与者。

中转作用(结构性)
  通过中介者提供的中转作用,各个同事对象就不再需要显式引用其他同事,当需要和其他同事进行通信时,通过中介者即可
  该中转作用属于中介者在结构上的支持。
协调作用(行为性)
  中介者可以更进一步的对同事之间的关系进行封装,同事可以一致地和中介者进行交互,而不需要指明中介者需要具体怎么做,中介者根据封装在自身内部的协调逻辑,对同事的请求进行进一步处理,将同事成员之间的关系行为进行分离和封装
  该协调作用属于中介者在行为上的支持。

示例:
  在一个小海岛上有一个微型军用飞机场,该机场只有三种类型的飞行:轰炸机(Bomber)、战斗机(BattlePlane)和运输机(Transporter)。
  机场的设置包括一个飞机库与一条供飞机起飞与降落的跑道。
  飞机库里可以停放飞机,防止飞机在地面被破坏,要求为该机场设计一个软件管理系统。
  控制塔(ControlTower)作为中介者类实现对飞机的起飞降落的协调。
  轰炸机、战斗机和运输机作为参与者类,实现了参与者接口类(Airplane)的方法,能够对飞机的起飞和降落操作( excuteTakeoff、 excuteLanding )以及进出库进行管理。按照中介者模式设计该系统。

解析:
请添加图片描述

优点

  • 所有对象的交互行为都被转入到一个独立的中介者对象中,使得用户更容易通过中介者修改对象之间的相互关系行为。当修改一个交互行为的时候,可以通过修改或者更换其中一个具体的中介者子类来完成。简化了对象之间的交互。另外,将对象的交互转移到一个专门的类中也提高了对象的可重用性。
  • 因为对象之间不直接有交互,使对象的单元测试更加容易
  • 低耦合使得一个类的修改不会影响到其他的类。可以简化各同事类的设计和实现

缺点
  在具体中介者类中包含了同事之间的交互细节,可能会导致具体中介者类非常复杂,使得系统难以维护。

适用环境

  • 系统中对象之间存在复杂的引用关系,产生的相互依赖关系结构混乱且难以理解。
  • 一个对象由于引用了其他很多对象并且直接和这些对象通信,导致难以复用该对象。
  • 想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。可以通过引入中介者类来实现,在中介者中定义对象交互的公共行为,如果需要改变行为则可以增加新的中介者类
3.1 练习
  1. 使用中介者模式来说明联合国的作用。将联合国(UnitedNations)定义为抽象中介者类,联合国下属机构如WTO等作为具体中介者类,能够设置国家如setChina、提供了发消息和收消息的方法(declare、getMessage);国家(Country)作为抽象参与者类,而将中国(China)、美国(America)作为具体参与者类,实现了发消息和收消息的功能(declare、getMessage)。要求绘制相应的类图。

解析:
请添加图片描述


4. 策略模式

  策略模式定义了一系列算法,将每一个算法封装起来,并且使它们之间可以相互替换。策略模式让算法的变化不会影响到使用算法的客户
  策略模式是一个比较容易理解和使用的设计模式,策略模式是对算法的封装,它把算法的责任和算法本身分割开,委派给不同的对象管理。策略模式通常把一个系列的算法封装到一系列的策略类里面,作为一个抽象策略类的子类。用一句话来说,就是“准备一组算法,并将每一个算法封装起来,使得它们可以互换”。

在这里插入图片描述

  • Strategy:定义了一个共同的接口,所有具体的算法类实现这个接口。环境(上下文)类Context使用这个接口调用具体的算法类。
  • ConcreteStrategy:封装了具体的算法,实现同一个接口。
  • Context:环境(上下文)类。用于配置一个具体的算法策略对象,维持一个策略接口类型的参考(Reference),并且可以定义一些让接口Strategy的具体对象访问的接口。在简单情况下,Context类可以省略。

优点

  • 得到一系列可以复用的算法,这些算法继承一个共同的抽象类,因此共有的功能可以放到超类中;
  • 将不同算法封装在不同的策略子类中,使逻辑更加清晰,各个算法可以独立地变化
  • 使功能改变或者扩展更容易,修改一个算法不必重新编译“Client”与“Context”。

缺点

  • 客户程序必须知道不同策略接口的各个子类的行为,必须理解每个子类有哪些不同。因此,在客户类中通常存在许多与策略类各个分支相关的条件语句,用于选择产生策略子类对象,然后将这些对象传递给Context类,而Context类则直接使用此对象调用策略模式的策略子类的方法。
4.1 练习
  1. 某电影院售票系统(MovieTicket)为不同类型的用户提供了不同的打折方式(Discount),学生凭学生证可享受8折优惠(StudentDiscount),儿童可享受减免10元的优惠(ChildrenDiscount),VIP用户除享受半价优惠外还可以进行积分(VIPDiscount),不同的用户实现了不同的计算方法(calculate)。售票系统可定义价格(price)和折扣(discount)两个属性,同时拥有设置价格(setPrice),设置折扣(setDiscount)、获取价格(getPrice)的方法,使用策略模式设计该系统,绘制相应的类图。

解析:
请添加图片描述


5. 状态模式

  状态模式可以有效地消除在客户程序中的条件语句,并且使得状态转换非常清楚。状态模式将不同状态下的行为封装在一个层次类的不同子类中。下图为状态模式的结构类图。
在这里插入图片描述

  • Context定义了与客户程序的接口,它保持了一个concreteState的代表现在状态的实例。
  • State定义了状态接口,它的各个子类封装了在各种不同状态下的行为。
  • ConcreteState子类:封装了在各种不同状态下的行为

  状态模式描述了对象状态的变化以及对象如何在每一种状态下表现出不同的行为
  状态模式的关键是引入了一个抽象类来专门表示对象的状态,这个类我们叫做抽象状态类,而对象的每一种具体状态类都继承了该类,并在不同具体状态类中实现了不同状态的行为,包括各种状态之间的转换。

示例
交通信号控制灯的实例
  考虑交通信号控制灯,他有红黄绿三个状态,三个状态在不断的转换。首先设计一个java抽象类trafficLight,然后由几个具体的状态类RedState、YellowState GreenState继承该接口。
  三个实现类的任务是:完成一些任务,例如完成拍照、统计车辆个数、记录违章车辆等; 负责改变状态,例如判断何时由黄色信号变为绿色信号;提供交通灯所需要的颜色。
在这里插入图片描述
方案1
  由状态类负责更新Context类的状态变量,Context类负责创建状态子类的对象。Context类保持一个string类型的状态变量state与一个TrafficLight类型的light变量;状态类TrafficLight保持一个Context类型的变量cxt。首先,程序运行开始时,Context类一次性地创建三个状态子类对象。由状态类负责调用Context类的方法setState更新Context类的state变量。Context类根据此变量的状态,决定使用哪个已经创建的状态子类的对象。
在这里插入图片描述
方案2
  由状态超类创建状态子类的对象,并且传递给Context类,Context类使用该对象对状态子类的功能进行调用。由Context类维护一个trafficLight类型的变量light;状态类TrafficLight保持一个Context变量cxt。由状态类根据当前状态,负责创建新的状态子类的对象,并且使用cxt调用Context类的setupstateObj方法更新Context类的light变量。Context类将直接使用该对象对相应的状态子类进行调用。
在这里插入图片描述
方案3
  遵循层次架构的状态模式设计,不存在任何形式的反向调用。由状态类的方法createStateObj()负责创建状态子类对象,并且在Context类中调用该方法,获得对应于当前状态下的状态子类对象。
在这里插入图片描述
纠正:
请添加图片描述

策略模式和状态模式的相似之处
  两种模式在结构上是相同的。策略模式将每个条件分支封装在一个子类中,而状态模式将每个状态封装在一个子类中。

策略模式和状态模式的区别
  策略模式用来处理一组具有相同目的但是实现方法不同的算法,这些算法方案之间一般来说没有状态变迁,并且用户总是从几个算法中间选取一个。
  状态模式则不同,它实现的一个概念可以叫做动态继承,也就是继承的子类都可以发生变化。状态的变化可以由一个状态迁徙图表示。

优点

  • 封装了转换规则。
  • 枚举可能的状态,在枚举状态之前需要确定状态种类。
  • 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
  • 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
  • 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
5.1 练习
  1. 在射击游戏中,游戏角色(Player)存在几种不同的状态(State)和名字属性(name),分别为正常状态(NomalState)、暂停状态(PauseState)、阵亡状态(DeathState)。在不同状态下角色对象的行为不同,行为有暂停(pause)、开始(start)、被射击(beAttacked)、射击(shot)、移动(move)。使用状态模式来设计和实现角色状态的转换。

解析:
请添加图片描述

案例分析

工厂模式案例

  使用工厂方法模式设计一个系统日志记录器,该系统日志记录器要求支持多种日志记录方式,如文件日志记录(FileLog)、数据库日志记录(DatabaseLog)等,每种记录方式均具有writeLog()方法记录数据,客户端(Client)可选择日志记录方式(logType)通过调用工厂类(LogFactory)中的createLog()方法创建日志(Log)对象,
  请按要求画出类图及其关键属性和操作。

解析:
请添加图片描述

生成器模式案例

  使用生成器模式设计一款播放器软件主界面(MainScreen)类图。其中,主界面(MainScreen)有两种显示模式:完整模式(FullModeBuilder),精简模式(SimpleModeBuilder)。
  播放器由以下属性构成:显示菜单(Menu)、播放列表(PlayList)、主窗口(MenuWindow)、控制条(ControlBar)等。
  用户可以在选择界面上(MainGUI),通过系统管理(ScreenModeController),根据不同的显示类型(setModeBuilder),来定制具体的播放器的主界面对象(construct),并返回一个主界面(MainScreen)。
  其中,界面对象是通过调用build方法来对属性进行赋值的(例如,buildMenu方法)。两种类型的界面构建方式均实现了抽象类(ModeBuilder)。
  请按要求画出类图及其关键属性和操作。

解析:
请添加图片描述

单例模式案例

  为了节约系统资源,使用单例模式为联机射击游戏(ShooterGame)设计一个场景管理器(SceneManager),它包含一系列成员对象并可以使用manage()方法管理成员对象,此外还提供getInstance()方法用于创建场景管理器实例,instance用于存储已被创建的场景管理器实例,
请按要求画出类图及其关键属性和操作。

解析:
请添加图片描述

组合模式案例

使用组合模式设计一个类图来表示水果店的物品。
  假设水果店有3种水果,分别是苹果(Apple)、西瓜(Watermelon)和橙子(Orange),另外,水果拼盘(FruitPlatter)是组合对象(Composite),拥有集合类型的fruitList私有成员变量,用于(动态地)添加水果到其数据结构中,所以可以产生由3种水果中的一种或多种组成的水果拼盘,当调用水果拼盘的获取价格操作时,将自动分别调用组合中的所有水果子类的获取价格的操作,得出水果拼盘的价格,而不需要知道水果拼盘的细节。
  三种水果和水果拼盘都继承于Fruit类,都有float类型的价格(price)私有成员变量,且都有添加(add)、删除(remove)和获取价格(getPrice)方法。
请按要求画出类图及其关键属性和操作。

解析:
在这里插入图片描述

适配器模式案例

  某公司欲开发一款儿童玩具汽车(Car),为了更好地吸引小朋友的注意力,该玩具汽车在移动(move)过程中伴随着灯光闪烁(twinkle)和声音提示(sound),在该公司以往的产品(OldProduct)中已经实现了控制灯光闪烁和声音提示的程序,为了重用先前的代码并且使得汽车控制软件具有更好的灵活性和扩展性,使用适配器(CarAdapter)模式设计该系统,
  请按要求画出类图及其关键操作。

解析:
请添加图片描述

外观模式案例

  在金融机构中,当有客户(Client)前来抵押贷款mortgage()时,需通过抵押系统(Mortgage)对客户进行合格性验证isQualified(),只有验证通过后才能进行抵押贷款。
  抵押系统的合格性验证需要三个子系统同时工作:身份验证(Authentication)子系统确认客户身份是否合法isLegal()、信用(Credit)子系统查询其信用是否良好isCredible()以及贷款(Loan)子系统查询客户是否有贷款劣迹hasBadLoans()。只有这三个子系统都通过时才能通过合格性验证。
  使用外观模式模拟该过程,请按要求画出类图及其关键属性和操作。

解析:
请添加图片描述

桥接模式案例

使用桥接模式设计不同品牌的不同家具的对象构造类图。
  宜家(YJ)、业通(YT)和百强(BQ)都是家具制造商,它们都生产沙发(sofa)、茶几(TeaTable)和电脑桌(ComputerTable)。现需要设计一个系统,描述这些家具制造商以及它们所制造的家具,其中家具制造商( FurnitureProducer)具有生产方法produce()和设置家具类型方法setFurniture(Furniture furniture),家具类(Furniture)有装配方法assemble(),
  请按要求画出类图及其关键属性和操作。

解析:
请添加图片描述

迭代器模式案例

  使用迭代器模式设计一个类图来表示对整数矩阵(Matrix)进行不同方式的遍历。其中遍历方式有:按照奇数类型进行遍历(OddIterator)和按照偶数类型进行遍历(EvenIterator)。整数矩阵(Matrix)可以获得该矩阵的整数(getMatrixData);可以选择不同的迭代器进行遍历(createOddIterator和createEvenIterator)。其中,两种迭代器均实现了抽象类NumberIterator的操作方法(hasnext、next、remove、getNumOfItems)。
  请按要求画出类图及其关键属性和操作。

解析:
请添加图片描述

访问者模式案例

使用访问者模式设计高校奖励审批系统。
  某高校奖励审批系统可实现教师奖励的审批和学生奖励的审批,可以参与评选的人员(Person)有教师(Teacher)和学生(Student),审批的奖励项目(AwardCheck)包括科研奖(ScientificAwardCheck)和成绩优秀奖(ExcellenceAwardCheck)。其中,教师或学生的论文(paperAmount)超过规定数目可评选科研奖,教师教学反馈分(feedbackScore)或学生平均分(score)达到规定分数可评选成绩优秀奖。系统中的候选人列表类(CandidateList)定义了一个人员列表(PersonList),用于存放待审核的教师和学生信息,并可以对列表进行添加人员(addPerson)、删除人员(removePerson)和审批人员(accept)等操作。客户端(Client)创建候选人列表对象,通过调用候选人列表类的方法,允许奖励审批对象(AwardCheck)访问参与评选的人员(Person)。
  请按要求画出类图及其关键属性和操作。

解析:
请添加图片描述

中介者模式案例

使用中介者模式设计房屋租赁系统。
  房屋中介(HouseMediator)作为具体中介者类,实现了中介者接口类(Mediator)的方法,能够向参与者发送房产信息(sendMessage),并提供了参与者的注册方法(registerLandlord与registerRenter);
  房东(Landlord)和房客(Renter)作为具体参与者类,实现了参与者接口类(Person)的方法,能够向房屋中介发送需求(sendMessage)及从房屋中介获取房产信息(getMessage)。
  请按要求画出类图及其关键属性和操作。

解析:
请添加图片描述

策略模式案例

  现有一种玩具,它可以模拟三种动物(Animal)的叫声:狗叫(Dog)、猫叫(Cat)和青蛙叫(Frog),每一种动物都有cry()方法用于实现发出叫声的操作。玩具类(Toy) 使用setAnimal()方法设置动物,并使用属性currentAnimal记录玩具当前模拟的动物,使用 run()方法来调用动物的cry()方法使玩具发出叫声。为将来能模拟更多的动物,采用策略模式设计该系统,请按要求画出类图及其关键属性和操作。

解析:
请添加图片描述

状态模式案例

使用状态模式设计一个类图来表示火箭升空的状态改变。
  其中火箭升空的大致状态有:系统启动状态(StartState)、加速状态(AccelerateState)、稳定运行状态(StableState)。
  环境类(Context)通过执行doAction()来改变火箭升空过程中的状态改变。其中三个升空状态类均继承父类RocketState状态类,能够创建升空状态对象(createStateObj),实现了升空状态变化逻辑(changeState),不同升空状态子类分别实现了当前状态下的操作(performTask) 。当火箭运行到稳定状态时候,环境类(Context)一直保持这个状态。
  请按要求画出类图及其关键属性和操作。

解析:
请添加图片描述

软件体系结构

设计模式与软件体系结构复习资料——软件体系结构

  • 4
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值