考试题型:选择、简答、设计
类(Class)封装了数据和行为,是面向对象的重要组成部分,它是具有相同属性、操作、关系的对象集合的总称。
在系统中,每个类具有一定的职责,职责指的是类所担任的任务,即类要完成什么样的功能,要承担什么样的义务。一个类可以有多种职责,设计得好的类一般只有一种职责,在定义类的时候,将类的职责分解成为类的属性和操作(即方法)。
类之间的关系:
软件工程和建模大师Peter Coad认为,一个好的系统设计应该具备如下三个性质:
软件的可维护性和可复用性
面向对象设计原则和设计模式也是对系统进行合理重构的指南针。
设计原则名称 | 设计原则简介 | 重要性 |
---|---|---|
单一职责原则 | 类的职责要单一,不能将太多的职责放在一个类中 | ★★★★☆ |
开闭原则 | 软件实体对扩展是开放的,但对修改是关闭的,即在不修改一个软件实体的基础上去扩展其功能 | ★★★★★ |
里氏代换原则 | 在软件系统中,一个可以接受基类对象的地方必然可以接受一个子类对象 | ★★★★☆ |
依赖倒转原则 | 要针对抽象层编程,而不要针对具体类编程 | ★★★★★ |
接口隔离原则 | 使用多个专门的接口来取代一个统一的接口 | ★★☆☆☆ |
合成复用原则 | 在系统中应该尽量多使用组合和聚合关联关系,尽量少使用甚至不使用继承关系 | ★★★★☆ |
迪米特法则 | 一个软件实体对其他实体的引用越少越好,或者说如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,而是通过引入一个第三者发生间接交互 | ★★★☆☆ |
定义:一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中。
一个类(或者大到模块,小到方法)承担的职责越多,它被复用的可能性越小,而且如果一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作。
单一职责原则是实现高内聚、低耦合的指导方针,在很多代码重构手法中都能找到它的存在,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关重构经验。
示例:
某个基于Java的C/S系统的“登录功能”通过如下登录类(Login)实现:
请思考:使用单一职责原则对其进行重构。
解析:
开闭原则由Bertrand Meyer于1988年提出,它是面向对象设计中最重要的原则之一。
开闭原则定义如下:
开闭原则还可以通过一个更加具体的“对可变性封装原则”来描述,
可变性封装原则要求找到系统的可变因素并将其封装起来。
示例:
某图形界面系统提供了各种不同形状的按钮,客户端代码可针对这些按钮进行编程,用户可能会改变需求要求使用不同的按钮,原始设计方案如图所示:
请对该系统进行重构,使之满足开闭原则的要求。
解析:
里氏代换原则由2008年图灵奖得主、美国第一位计算机科学女博士、麻省理工学院教授Barbara Liskov和卡内基.梅隆大学Jeannette Wing教授于1994年提出。
里氏代换原则有两种定义方式,第一种定义方式相对严格,其定义如下:
第二种更容易理解的定义方式如下:
里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
示例:
如果需要更换一个加密算法类或者增加并使用一个新的加密算法类,如将CipherA改为CipherB,则需要修改客户类Client和数据操作类DataOperator的源代码,违背了开闭原则。请使用里氏代换原则对其进行重构,使得系统可以灵活扩展,符合开闭原则。
解析:
依赖倒转原则的定义如下:
另一种表述为:要针对接口编程,不要针对实现编程。
简单来说,依赖倒转原则就是指:
实现开闭原则的关键是抽象化,并且从抽象化导出具体化实现,如果说开闭原则是面向对象设计的目标的话,那么依赖倒转原则就是面向对象设计的主要手段。
依赖倒转原则的常用实现方式之一是在代码中使用抽象类,而将具体类放在配置文件中,即控制反转,其具体实现例如:
示例:
某系统提供一个数据转换模块,可以将来自不同数据源的数据转换成多种格式,如:可以转换来自数据库的数据(DatabaseSource)、也可以转换来自文本文件的数据(TextSource),转换后的格式可以是XML文件(XMLTransformer)、也可以是XLS文件(XLSTransformer)等。
由于需求的变化,该系统可能需要增加新的数据源或者新的文件格式,每增加一个新的类型的数据源或者新的类型的文件格式,客户类MainClass都需要修改源代码,以便使用新的类,但违背了开闭原则。
请使用依赖倒转原则对其进行重构。
解析:
接口隔离原则的定义如下:客户端不应该依赖那些它不需要的接口。
另一种定义方法如下:一旦一个接口太大,则需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。
接口隔离原则是指使用多个专门的接口,而不使用单一的总接口。每一个接口应该承担一种相对独立的角色,不多不少,不干不该干的事,该干的事都要干。
使用接口隔离原则拆分接口时,首先必须满足单一职责原则,将一组相关的操作定义在一个接口
中,且在满足高内聚的前提下,接口中的方法越少越好。
可以在进行系统设计时采用定制服务的方式,即为不同的客户端提供宽窄不同的接口,只提供用户需要的行为,而隐藏用户不需要的行为。
合成复用原则,又称为组合/聚合复用原则,其定义如下:
简言之:要尽量使用组合/聚合关系,少用继承。
在面向对象设计中,可以通过两种基本方法在不同的环境中复用已有的设计和实现,即通过组合/
聚合关系或通过继承。
组合/聚合可以使系统更加灵活,类与类之间的耦合度降低,一个类的变化对其他类造成的影响相对较少,因此一般首选使用组合/聚合来实现复用;
其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。
示例:
某教学管理系统部分数据库访问类设计如图所示。
若需要更换数据库连接方式,例如原来采用JDBC连接数据库,现在采用数据库连接池连接,则需要修改DBUtil类源代码。
如果StudentDAO采用JDBC连接,但是TeacherDAO采用连接池连接,则需要增加一个新的DBUtil类,并修改StudentDAO或TeacherDAO的源代码,使之继承新的数据库连接类,这将违背开闭原则,系统扩展性较差。
请使用合成复用原则对其进行重构。
解析:
迪米特法则又称为最少知识原则,它有多种定义方法,其中几种典型定义如下:
迪米特法则来自于1987年秋美国东北大学(Northeastern University)一个名为“Demeter”的研究项目。
简单地说,迪米特法则就是指一个软件实体应当尽可能少的与其他实体发生相互作用。这样,当一个模块修改时,就会尽量少的影响其他的模块,扩展会相对容易。这是对软件实体之间通信的限制,它要求限制软件实体之间通信的宽度和深度
在迪米特法则中,对于一个对象,其朋友包括以下几类:
任何一个对象,如果满足上面的条件之一,就是当前对象的“朋友”,否则就是“陌生人”。
模式:
根据其目的(模式是用来做什么的),面向对象的领域的设计模式可分为创建型(Creational),结构型(Structural)和行为型(Behavioral)三种:
创建型模式(Creational Pattern)对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离。
为了使软件的结构更加清晰,外界对于这些对象只需要知道它们共同的接口,而不清楚其具体的实现细节,使整个系统的设计更加符合单一职责原则。
创建型模式在创建什么(What),由谁创建(Who),何时创建(When)等方面都为软件设计者提供了尽可能大的灵活性。
创建型模式隐藏了类的实例的创建细节,通过隐藏对象如何被创建和组合在一起 达到使整个系统独立的目的。
创建型模式的目标和内容:
简单工厂模式:又称为静态工厂方法模式。
在简单工厂模式中,可以根据参数的不同返回不同类的实例。
简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。
示例一:
某电视机厂专为各知名电视机品牌代工生产各类电视机,当需要海尔牌电视机时只需要在调用该工厂的工厂方法时传入参数“Haier”,需要海信电视机时只需要传入参数“Hisense”,工厂可以根据传入的不同参数返回不同品牌的电视机。
请使用简单工厂方法模式来模拟该电视机工厂的生产过程。
解析:
优点:
缺点:
适用环境:
在简单工厂模式中,只提供了一个工厂类,该工厂类处于对产品类进行实例化的中心位置,它知道每一个产品对象的创建细节,并决定何时实例化哪一个产品类。
简单工厂模式最大的缺点是当有新产品要加入到系统中时,必须修改工厂类,加入必要的处理逻辑,这违背了“开闭原则”。
在简单工厂模式中,所有的产品都是由同一个工厂创建,工厂类职责较重,业务逻辑较为复杂,具体产品与工厂类之间的耦合度高,严重影响了系统的灵活性和扩展性。
为克服简单工厂方法模式的不足,人们试图改善工厂类结构以解决这一问题。
软件设计者们发现,可以将简单工厂方法模式中单一的工厂类改写为一个层次类来解决这个问题。
示例二:
用工厂方法模式重构上述设计,当需要生产新品牌电视机,原有的工厂无须做任何修改,使得整个系统具有更加的灵活性和可扩展性。
解析:
优点:
在工厂方法模式中,工厂方法用来创建客户所需要的产品,同时还向客户隐藏了哪种具体产品类将被实例化这一细节,用户只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须知道具体产品类的类名。
基于工厂角色和产品角色的多态性设计是工厂方法模式的关键。它能够使工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。工厂方法模式之所以又被称为多态工厂模式,是因为所有的具体工厂类都具有同一抽象父类。
使用工厂方法模式的另一个优点是在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品就可以了。这样,系统的可扩展性也就变得非常好,完全符合“开闭原则”。
缺点:
在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。
由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,且在实现时可能需要用到DOM、反射等技术,增加了系统的实现难度。
简单工厂模式与工厂方法模式的区别:
适用场景
一个类不知道它所需要的对象的类:在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。
一个类通过其子类来指定创建哪个对象:在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。
将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无须关心是哪一个工厂子类创建产品子类,需要时再动态指定,可将具体工厂类的类名存储在配置文件或数据库中。
在工厂方法模式中具体工厂负责生产具体的产品,每一个具体工厂对应一种具体产品,工厂方法也具有唯一性,一般情况下,一个具体工厂中只有一个工厂方法或者一组重载的工厂方法。但是有时候我们需要一个工厂可以提供多个产品对象,而不是单一的产品对象。
产品等级结构:——同一产品类型,不同工厂生产
产品等级结构即产品的继承结构,如一个抽象类是电视机,其子类有海尔电视机、海信电视机、TCL电视机,则抽象电视机与具体品牌的电视机之间构成了一个产品等级结构,抽象电视机是父类,而具体品牌的电视机是其子类。
产品族:——同一工厂生产,不同产品类型
在抽象工厂模式中,产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品,如海尔电器工厂生产的海尔电视机、海尔电冰箱,海尔电视机位于电视机产品等级结构中,海尔电冰箱位于电冰箱产品等级结构中。
抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。
抽象工厂模式又称为Kit模式,属于对象创建型模式。
示例:
一个电器工厂可以产生多种类型的电器,如海尔工厂可以生产海尔电视机、海尔空调等,TCL工厂可以生产TCL电视机、TCL空调等,相同品牌的电器构成一个产品族,而相同类型的电器构成了一个产品等级结构。
请使用抽象工厂模式模拟该场景。
解析:
抽象工厂模式的可扩展性:
增加新的工厂符合开闭原则,增加新类型的产品不符合开闭原则。
优点:
抽象工厂模式隔离了具体类的生成,使得客户并不需要知道什么被创建。由于这种隔离,更换一个具体工厂就变得相对容易。
所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。另外,应用抽象工厂模式可以实现高内聚低耦合的设计目的,因此抽象工厂模式得到了广泛的应用。
当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。这对一些需要根据当前环境来决定其行为的软件系统来说,是一种非常实用的设计模式。
增加新的具体工厂和产品族很方便,无须修改已有系统,符合“开闭原则”。
缺点:
在添加新的产品对象时,难以扩展抽象工厂来生产新种类的产品,这是因为在抽象工厂角色中规定了所有可能被创建的产品集合,要支持新种类的产品就意味着要对该接口进行扩展,而这将涉及到对抽象工厂角色及其所有子类的修改,显然会带来较大的不便。
开闭原则的倾斜性(增加新的工厂和产品族容易,增加新的产品等级结构麻烦)
适用场合:
一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是重要的。
系统中有多于一个的产品族,而每次只使用其中某一产品族。
属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。
系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于具体实现。
抽象工厂模式包含四个角色:
答:简单工厂方法模式一般不符合开闭原则;工厂方法模式符合开闭原则;抽象工厂模式在增加新的工厂和产品族时符合开闭模式,在增加新的产品等级结构时不符合开闭模式。
解:
无论是在现实世界中还是在软件系统中,都存在一些复杂的对象,它们拥有多个组成部分,如:汽车的组成包括底盘(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)的数量上。根据用户的选择,具体选择房屋的各种指标。本程序采用生成器模式,设计类图如下:
优点:
在生成器模式中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
每一个具体生成器都相对独立,而与其他的具体生成器无关,因此可以很方便地替换具体生成器或增加新的具体生成器,用户使用不同的具体生成器即可得到不同的产品对象。
可以更加精细地控制产品的创建过程。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。
增加新的具体生成器无须修改原有类库的代码,指挥者类针对抽象生成器类编程,系统扩展方便,符合“开闭原则”。
缺点:
生成器模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用生成器模式,因此其使用范围受到一定的限制。
如果产品的内部变化复杂,可能会导致需要定义很多具体生成器类来实现这种变化,导致系统变得很庞大。
适用场合:
需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个成员属性。
需要生成的产品对象的属性相互依赖,需要指定其生成顺序。
对象的创建过程独立于创建该对象的类。在生成器模式中引入了指挥者类,将创建过程封装在指挥者类中,而不在生成器类中。
隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品。
生成器模式与抽象工厂模式
与抽象工厂模式相比,生成器模式返回一个组装好的完整产品,而抽象工厂模式返回一系列相关的产品,这些产品位于不同的产品等级结构,构成了一个产品族。
在抽象工厂模式中,客户端实例化工厂类,然后调用工厂方法获取所需产品对象,而在生成器模式中,客户端可以不直接调用生成器的相关方法,而是通过指挥者类来指导如何生成对象,包括对象的组装过程和建造步骤,它侧重于一步步构造一个复杂对象,返回一个完整的对象。
如果将抽象工厂模式看成汽车配件生产工厂,生产一个产品族的产品,那么生成器模式就是一个汽车组装工厂,通过对部件的组装可以返回一辆完整的汽车。
在书本P27页房屋选购系统例子的设计中,添加一个经济型房屋生成器类,命名为EconHouseBuilder。 请绘制新设计的类图。
解:
对于系统中的某些类来说,只有一个实例很重要,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个窗口管理器或文件系统;一个系统只能有一个计时工具或ID(序号)生成器。
单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供一个全局访问点。
单例模式的要点有三个:
单例模式是一种对象创建型模式。单例模式又名单件模式或单态模式。
基本思路:
将构造方法声明为private类型,其他类无法使用该构造方法来创建对象
提供一个可以获得实例的getInstance方法,该方法是静态方法,确保客户程序得到的始终是同一个对象
示例:
考虑一个单位的互联网连接问题。该单位对外的互联网使用一个统一的IP地址,所有的内部用户都使用一个统一的内部服务器。当一个用户要连接到互联网上时,该用户应首先连接到内部服务器上。显然,如果一个用户已经开始了一个连接,该用户不可能再有第二个连接。
单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时有两个线程同时调用创建方法,那么就将导致两个线程各自创建了一个实例,从而违反了单例模式的实例唯一性的初衷。为了改善以上的设计,可以将设计中的getInstance()方法声明为synchronized类型的。
优点:
提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它,并为设计及开发团队提供了共享的概念。
由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
允许可变数目的实例。我们可以基于单例模式进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例。
缺点:
由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失。
适用环境:
系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器,或者需要考虑资源消耗太大而只允许创建一个对象。
客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
在一个系统中要求一个类只有一个实例时才应当使用单例模式。反过来,如果一个类可以有几个实例共存,就需要对单例模式进行改进,使之成为多例模式。
动机:
结构型软件设计模式的主要目的是将不同的类和对象组合在一起,形成更大或者更复杂的结构体。
内容:
动机:
对于树形结构,当容器对象(如文件夹)的某一个方法被调用时,将遍历整个树形结构,寻找也包含这个方法的成员对象(可以是容器对象,也可以是叶子对象,如子文件夹和文件)并调用执行。(递归调用)
由于容器对象和叶子对象在功能上的区别,在使用这些对象的客户端代码中必须有区别地对待容器对象和叶子对象,而实际上大多数情况下客户端希望一致地处理它们,因为对于这些对象的区别对待将会使得程序非常复杂。
组合模式描述了如何将容器对象和叶子对象进行递归组合,使得用户在使用时无须对它们进行区分,可以一致地对待容器对象和叶子对象,这就是组合模式的模式动机。
定义:
组合模式(Composite Pattern):组合多个对象形成树形结构以表示“整体-部分”的结构层次。组合模式对单个对象(即叶子对象)和组合对象(即容器对象)的使用具有一致性。
组合模式又可以称为“整体-部分”(Part-Whole)模式,属于对象的结构模式,它将对象组织到树结构中,可以用来描述整体与部分的关系。
结构:
Component:为组合模式中的对象声明接口
Leaf:在组合模式中表示叶结点对象
Composite:表示组合部件
Client:通过Component接口操纵组合部件的对象。
优点:
可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,使得增加新构件也更容易。
客户端调用简单,客户端可以一致的使用组合结构或其中单个对象。
定义了包含叶子对象和容器对象的类层次结构,叶子对象可以被组合成更复杂的容器对象,而这个容器对象又可以被组合,这样不断递归下去,可以形成复杂的树形结构。
更容易在组合体内加入对象构件,客户端不必因为加入了新的对象构件而更改原有代码。
缺点:
使设计变得更加抽象,对象的业务规则如果很复杂,则实现组合模式具有很大挑战性,而且不是所有的方法都与叶子对象子类都有关联。
增加新构件时可能会产生一些问题,很难对容器中的构件类型进行限制。
适用环境:
需要表示一个对象整体或部分层次,在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,可以一致地对待它们。
让客户能够忽略不同对象层次的变化,客户端可以针对抽象构件编程,无须关心对象层次结构的细节。
对象的结构是动态的并且复杂程度不一样,但客户需要一致地处理它们。
在软件开发中,为了解决接口不一致的问题,两个软件模块之间往往也需要通过一个适配器类Adapter进行“适配”。这样的模式叫做适配器设计模式。
通常情况下,客户端可以通过目标类的接口访问它所提供的服务。
有时,现有的类可以满足客户类的功能需要,但是它所提供的接口不一定是客户类所期望的,这可能是因为现有类中方法名与目标类中定义的方法名不一致等原因所导致的。
在这种情况下,现有的接口需要转化为客户类期望的接口,这样保证了对现有类的重用。如果不进行这样的转化,客户类就不能利用现有类所提供的功能,适配器模式可以完成这样的转化。
适配器提供客户类需要的接口,适配器的实现就是把客户类的请求转化为对适配者的相应接口的调用。
也就是说:当客户类调用适配器的方法时,在适配器类的内部将调用适配者类的方法,而这个过程对客户类是透明的,客户类并不直接访问适配者类。
因此,适配器可以使由于接口不兼容而不能交互的类可以一起工作。
该模式可以分为两种,分别为类适配器模式(Class Adapter Pattern)和对象适配器模式(Object Adapter Pattern)。
示例:
某公司购买了一个用于验证客户信息的离架产品类InfoValidation,但是卖方没有提供源代码。该类可以用于检查客户输入的信息,包含验证姓名、地址、电话区号和手机号码等功能。如果还需要增加一个验证社会安全号(SSN)的功能,则可以使用类适配器模式来实现。
解析:
示例:
使用对象适配器实现字符串序列排序。要求从一个.txt文件读入一些英文字符串,并且对这些字符串进行排序。
这里已有一个类FileInput,其主要功能包含从一个文件中读入字符串;另外,在一个Java类库中有一个Arrays,其中包含功能sort,用于对多个字符串进行排序。
解析:
类适配器模式与对象适配器模式的区别:
在Java语言中,使用对象适配器模式可以把多种不同的源类都适配到同一个Target接口,而使用类的适配器模式是做不到这一点的。
如果一个被适配源类中有大量的方法,使用类适配器模式比较容易,只需要让Adapter类继承被适配的源类即可。而此时使用对象适配器模式则要在Adapter类中明确写出Target角色中的每个方法,并且在每个方法中要调用被适配的源类中的相应的方法。
优点:
将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,而无须修改原有代码。
增加了类的透明性和复用性,将具体的实现封装在适配者类中,对于客户端类来说是透明的,而且提高了适配者的复用性。
灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”。
由于类适配器是适配者类的子类,因此可以在类适配器中置换一些适配者的方法,使得适配器的灵活性更强。
一个对象适配器可以把多个不同的适配者适配到同一个目标,也就是说,同一个适配器可以把适配者类和它的子类都适配到目标接口。
缺点:
对于Java、C#等不支持多重继承的语言,一次最多只能适配一个适配者类,而且目标抽象类只能为抽象类,不能为具体类,其使用有一定的局限性,不能将一个适配者类和它的子类都适配到目标接口。
与类适配器模式相比,要想置换适配者类的方法就不容易。如果一定要置换掉适配者类的一个或多个方法,就只好先做一个适配者类的子类,将适配者类的方法置换掉,然后再把适配者类的子类当做真正的适配者进行适配,实现过程较为复杂。
适用场合:
适配器模式的扩展:
引入外观角色之后,用户只需要直接与外观角色交互,用户与子系统之间的复杂关系由外观角色来实现,从而降低了系统的耦合度。
外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
外观模式又称为门面模式,它是一种对象结构型模式。
外观模式由三个角色组成:
外观角色:外观模式的核心。它被客户角色调用,因此它熟悉子系统的功能。其内部根据客户角色已有的需求预定了几种功能组合。
子系统角色:实现子系统的功能,对它而言,外观角色就和客户角色一样是未知的,它没有任何外观角色的信息和链接。
客户角色:调用外观角色来完成要得到的功能
根据“单一职责原则”,在软件中将一个系统划分为若干个子系统有利于降低整个系统的复杂性,一个常见的设计目标是使子系统间的通信和相互依赖关系达到最小,而达到该目标的途径之一就是引入一个外观对象,它为子系统的访问提供了一个简单而单一的入口。
外观模式也是“迪米特法则”的体现,通过引入一个新的外观类可以降低原有系统的复杂度,同时降低客户类与子系统类的耦合度。
在外观模式中,通常只需要一个外观类,并且此外观类只有一个实例,换言之它是一个单例类。
在很多情况下为了节约系统资源,一般将外观类设计为单例类。当然这并不意味着在整个系统里只能有一个外观类,在一个系统中可以设计多个外观类,每个外观类都负责和一些特定的子系统交互,向用户提供相应的业务功能。
不要通过继承一个外观类在子系统中加入新的行为,这种做法是错误的。
外观模式的用意是为子系统提供一个集中化和简化的沟通渠道,而不是向子系统加入新的行为,
抽象外观类的引入
外观模式最大的缺点在于违背了“开闭原则”,当增加新的子系统或者移除子系统时需要修改外观类,可以通过引入抽象外观类在一定程度上解决该问题,客户端针对抽象外观类进行编程。
对于新的业务需求,不修改原有外观类,而对应增加一个新的具体外观类,由新的具体外观类来关联新的子系统对象,同时通过修改配置文件来达到不修改源代码并更换外观类的目的。
对于有两个变化维度(即两个变化的原因)的系统,采用桥接模式来进行设计系统中类的个数更少,且系统扩展更为方便。
桥接模式将继承关系转换为关联关系,从而降低了类与类之间的耦合,减少了代码编写量。
将抽象部分与它的实现部分分离,使它们都可以独立地变化。
是一种对象结构型模式,又称为柄体(Handle and Body)模式或接口(Interface)模式。
优点:
缺点:
适用环境:
适配器模式与桥接模式的联用
动机:
行为型软件设计模式关心算法和对象之间的责任分配,不仅是描述对象或类模式,更加侧重描述它们之间的通信模式。
内容:
一个聚合对象,如一个列表(List)或者一个集合(Set),应该提供一种方法来让别人可以访问它的元素,而又不需要暴露它的内部结构。
针对不同的需要,可能还要以不同的方式遍历整个聚合对象,但是我们并不希望在聚合对象的抽象层接口中充斥着各种不同遍历的操作。
怎样遍历一个聚合对象,又不需要了解聚合对象的内部结构,还能够提供多种不同的遍历方式?
在迭代器模式中,提供一个外部的迭代器来对聚合对象进行访问和遍历,
迭代器定义了一个访问该聚合元素的接口,并且可以跟踪当前遍历的元素,了解哪些元素已经遍历过而哪些没有。
有了迭代器模式,我们会发现对一个复杂的聚合对象的操作会变得如此简单。
迭代器模式的关键思想是将对列表的访问和遍历从列表对象中分离出来,放入一个独立的迭代对象中。
迭代器模式能够提供一种方法按照顺序访问一个聚合对象元素中的所有元素,而又不需要暴露该对象的内部表示。
Aggregate:聚合接口,其实现子类将创建并且维持一个一种数据类型的聚合体。另外,它还定义了创建相应迭代器对象的接口creaIterator。
ConcreteAggregate:封装了一个数据存储结构,实现一个具体的聚合,如列表、java类型ArrayList等。一个聚合对象包含一些其他的对象。另外,该类提供了创建相应迭代器对象的方法createIterator,该方法返回类型为ConcreteIterator的一个对象。
Iterator:迭代器定义访问和遍历元素的接口。
ConcreteIterator:具体迭代器实现迭代器接口,对该聚合遍历时跟踪当前位置。
示例:
随机生成一个整数矩阵(Matrix)。矩阵可以看做是聚合类型的数据。考虑对该矩阵进行两种不同方式的遍历(OddNumIterator和CircularIterator)。采用迭代器模式设计类图。
缺点:
适用环境:
访问者模式为解决这类问题而诞生。
在实际使用时,对同一集合对象的操作并不是唯一的,对相同的元素对象可能存在多种不同的操作方式。而且这些操作方式并不稳定,可能还需要增加新的操作,以满足新的业务需求。此时,访问者模式就是一个值得考虑的解决方案。
访问者模式的目的是:封装一些施加于某种数据结构元素之上的操作,一旦这些操作需要修改的话,接受这个操作的数据结构可以保持不变。
为不同类型的元素提供多种访问操作方式,且可以在不修改原有系统的情况下增加新的操作方式,这就是访问者模式的模式动机。
访问者模式(Visitor Pattern):表示一个作用于某对象结构中的各元素的操作,它使我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作。访问者模式是一种对象行为型模式。
示例:
假设要实现一个计算机部件销售软件。考虑到计算机部件的种类相对固定,所以使用访问者模式进行设计。计算机部件(ComputerParts)包括Motherboard、Microprocessor、Memory、VideoCard、Monitor等。使用复合对象CompositeStructure类来表示计算机部件的集合,包含有attach、detach、accept三个方法。每个计算机部件类都有一个getPrice()方法、一个getDescription()方法与一个accept()方法;PriceVisitor通过调用getPrice()方法,达到计算价格的目的。类似地,PartsInfoVisitor类负责调用getDescription方法,实现获取部件的具体描述。
优点:
缺点:
适用场合:
在面向对象的软件设计与开发过程中,根据“单一职责原则”,我们应该尽量将对象细化,使其只负责或呈现单一的职责。
对于一个模块,可能由很多对象构成,而且这些对象之间可能存在相互的引用,为了减少对象两两之间复杂的引用关系,使之成为一个松耦合的系统,我们需要使用中介者模式,这就是中介者模式的模式动机。
中介者模式(Mediator Pattern)定义:
用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
中介者模式又称为调停者模式,它是一种对象行为型模式。
设计类图4.36就是以上逻辑图的具体体现,叫做中介者模式设计类图。
该设计类图由两部分组成,一部分是中介者类,另外一部分是参与者类。中介者模式的各组成部分的含义说明如下:
中转作用(结构性):
通过中介者提供的中转作用,各个同事对象就不再需要显式引用其他同事,当需要和其他同事进行通信时,通过中介者即可。
该中转作用属于中介者在结构上的支持。
协调作用(行为性):
中介者可以更进一步的对同事之间的关系进行封装,同事可以一致地和中介者进行交互,而不需要指明中介者需要具体怎么做,中介者根据封装在自身内部的协调逻辑,对同事的请求进行进一步处理,将同事成员之间的关系行为进行分离和封装。
该协调作用属于中介者在行为上的支持。
示例:
在一个小海岛上有一个微型军用飞机场,该机场只有三种类型的飞行:轰炸机(Bomber)、战斗机(BattlePlane)和运输机(Transporter)。
机场的设置包括一个飞机库与一条供飞机起飞与降落的跑道。
飞机库里可以停放飞机,防止飞机在地面被破坏,要求为该机场设计一个软件管理系统。
控制塔(ControlTower)作为中介者类实现对飞机的起飞降落的协调。
轰炸机、战斗机和运输机作为参与者类,实现了参与者接口类(Airplane)的方法,能够对飞机的起飞和降落操作( excuteTakeoff、 excuteLanding )以及进出库进行管理。按照中介者模式设计该系统。
优点:
缺点:
在具体中介者类中包含了同事之间的交互细节,可能会导致具体中介者类非常复杂,使得系统难以维护。
适用环境:
策略模式定义了一系列算法,将每一个算法封装起来,并且使它们之间可以相互替换。策略模式让算法的变化不会影响到使用算法的客户。
策略模式是一个比较容易理解和使用的设计模式,策略模式是对算法的封装,它把算法的责任和算法本身分割开,委派给不同的对象管理。策略模式通常把一个系列的算法封装到一系列的策略类里面,作为一个抽象策略类的子类。用一句话来说,就是“准备一组算法,并将每一个算法封装起来,使得它们可以互换”。
优点:
缺点:
状态模式可以有效地消除在客户程序中的条件语句,并且使得状态转换非常清楚。状态模式将不同状态下的行为封装在一个层次类的不同子类中。下图为状态模式的结构类图。
状态模式描述了对象状态的变化以及对象如何在每一种状态下表现出不同的行为。
状态模式的关键是引入了一个抽象类来专门表示对象的状态,这个类我们叫做抽象状态类,而对象的每一种具体状态类都继承了该类,并在不同具体状态类中实现了不同状态的行为,包括各种状态之间的转换。
示例:
交通信号控制灯的实例
考虑交通信号控制灯,他有红黄绿三个状态,三个状态在不断的转换。首先设计一个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类中调用该方法,获得对应于当前状态下的状态子类对象。
纠正:
策略模式和状态模式的相似之处
两种模式在结构上是相同的。策略模式将每个条件分支封装在一个子类中,而状态模式将每个状态封装在一个子类中。
策略模式和状态模式的区别
策略模式用来处理一组具有相同目的但是实现方法不同的算法,这些算法方案之间一般来说没有状态变迁,并且用户总是从几个算法中间选取一个。
状态模式则不同,它实现的一个概念可以叫做动态继承,也就是继承的子类都可以发生变化。状态的变化可以由一个状态迁徙图表示。
优点:
使用工厂方法模式设计一个系统日志记录器,该系统日志记录器要求支持多种日志记录方式,如文件日志记录(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)一直保持这个状态。
请按要求画出类图及其关键属性和操作。
设计模式与软件体系结构复习资料——软件体系结构