第 12 章组合模式(Composite Pattern)
12.1 使用场景
一个学校院系展示需求:编写程序展示一个学校院系结构:需求是这样,要在一个页面中展示出学校的院系组成,一个学校有多个学院, 一个学院有多个系。如图:
12.2 传统方案解决学校院系展示
-
传统方案解决学校院系展示存在的问题分析
(1)将学院看做是学校的子类,系是学院的子类,这样实际上是站在组织大小来进行分层次的。
(2)实际上我们的要求是 :在一个页面中展示出学校的院系组成,一个学校有多个学院,一个学院有多个系, 因此这种方案,不能很好实现的管理的操作,比如对学院、系的添加,删除,遍历等 -
解决方案
解决方案:组合模式 :把学校、院、系都看做是组织结构,他们之间没有继承的关系,而是一个树形结构,可以更好的实现管理操作。
12.3 组合模式
- 介绍
(1)组合模式(Composite Pattern),又叫部分整体模式,它创建了对象组的树形结构,将对象组合成树状结构以 表示**“整体-部分”的层次关系。
(2)组合模式依据树形结构来组合对象**,用来表示部分以及整体层次。
(3)这种类型的设计模式属于结构型模式。
(4)组合模式使得用户对单个对象和组合对象的访问具有一致性,即:组合能让客户以一致的方式处理个别对象以及组合对象 - 组合模式解决的问题
组合模式解决这样的问题,当我们的要处理的对象可以生成一颗树形结构,而 我们要对树上的节点和叶子进行操作时,它能够提供一致的方式,而不用考虑 它是节点还是叶子。 - 类图
(1)Component :这是组合中对象声明接口,在适当情况下,实现所有类共有的接口默认行为,用于访问和管理 Component 子部件, Component 可以是抽象类或者接口。
(2) Leaf : 在组合中表示叶子节点,叶子节点没有子节点
(3)Composite :非叶子节点, 用于存储子部件, 在 Component 接口中实现 子部件的相关操作,比如增加(add),删除。 - 组合模式的注意事项和细节
(1)简化客户端操作。客户端只需要面对一致的对象而不用考虑整体部分或者节点叶子的问题。
(2)具有较强的扩展性。当我们要更改组合对象时,我们只需要调整内部的层次关系,客户端不用做出任何改动.
(3)方便创建出复杂的层次结构。客户端不用理会组合里面的组成细节,容易添加节点或者叶子从而创建出复杂的树形结构
(4)需要遍历组织机构,或者处理的对象具有树形结构时, 非常适合使用组合模式.
(5)要求较高的抽象性,如果节点和叶子有很多差异性的话,比如很多方法和属性都不一样,不适合使用组合模式
12.4 组合模式的应用案例:院校展示
- 类图
- 代码实现
(1)抽象类:组织
public abstract class OrganizationComponent {
private String name; // 名字
private String des; // 说明
protected void add(OrganizationComponent organizationComponent) {
throw new UnsupportedOperationException(); //默认实现;不支持操作
}
protected void remove(OrganizationComponent organizationComponent) {
throw new UnsupportedOperationException(); //默认实现
}
//构造器
public OrganizationComponent(String name, String des) {
super();
this.name = name;
this.des = des;
}
//成员的get和set方法
//方法print, 做成抽象的, 子类都需要实现
protected abstract void print();
}
(2)非叶子结点:University类
//University 就是 Composite , 可以管理College
public class University extends OrganizationComponent {
List<OrganizationComponent> organizationComponents = new ArrayList<OrganizationComponent>();
// 构造器
public University(String name, String des) {
super(name, des);
}
// 重写add
@Override
protected void add(OrganizationComponent organizationComponent) {
organizationComponents.add(organizationComponent);
}
// 重写remove
@Override
protected void remove(OrganizationComponent organizationComponent) {
organizationComponents.remove(organizationComponent);
}
@Override
public String getName() {
return super.getName();
}
@Override
public String getDes() {
return super.getDes();
}
// print方法,就是输出University 包含的学院
@Override
protected void print() {
System.out.println("--------------" + getName() + "--------------");
//遍历 organizationComponents
for (OrganizationComponent organizationComponent : organizationComponents) {
organizationComponent.print();
}
}
}
College类类似,不一定相同
(3)叶子结点:Department类
public class Department extends OrganizationComponent {
//没有集合
public Department(String name, String des) {
super(name, des);
}
//add , remove 就不用写了,因为他是叶子节点
@Override
public String getName() {
return super.getName();
}
@Override
public String getDes() {
return super.getDes();
}
@Override
protected void print() {
System.out.println(getName());
}
}
(4)客户端
public class Client {
public static void main(String[] args) {
//从大到小创建对象 学校
OrganizationComponent university = new University("清华大学", " 中国顶级大学 ");
//创建 学院
OrganizationComponent computerCollege = new College("计算机学院", " 计算机学院 ");
OrganizationComponent infoEngineercollege = new College("信息工程学院", " 信息工程学院 ");
//创建各个学院下面的系(专业)
computerCollege.add(new Department("软件工程", " 软件工程不错 "));
computerCollege.add(new Department("网络工程", " 网络工程不错 "));
computerCollege.add(new Department("计算机科学与技术", " 计算机科学与技术是老牌的专业 "));
infoEngineercollege.add(new Department("通信工程", " 通信工程不好学 "));
infoEngineercollege.add(new Department("信息工程", " 信息工程好学 "));
//将学院加入到 学校
university.add(computerCollege);
university.add(infoEngineercollege);
//university.print();
infoEngineercollege.print();
}
}
12.5 组合模式在 JDK 集合的源码分析
Java 的集合类-HashMap 就使用了组合模式
第13章 外观模式
13.1 使用场景
DVD 播放器、投影仪、自动屏幕、环绕立体声、爆米花机,要求完成使用家庭影院的功能。
(1)直接用遥控器:统筹各设备开关
(2)开爆米花机、放下屏幕、开投影仪、开音响、开 DVD,选 dvd、去拿爆米花、调暗灯光、、播放
(3)观影结束后,关闭各种设备
13.2 传统方式解决影院管理
- 说明
ClientTest类会直接依赖各个设备,以及其中的方法。
代码不灵活,只有有一个设备发生变化,客户端就要发生变化。 - 传统方式问题分析
(1)在 ClientTest 的 main 方法中,创建各个子系统的对象,并直接去调用子系统(对象)相关方法,会造成调用过程混乱,没有清晰的过程
- 不利于在 ClientTest 中,去维护对子系统的操作
- 解决思路:定义一个高层接口,给子系统中的一组接口提供一个一致的界面(比如在高层接口提供四个方法 ready, play, pause, end ),用来访问子系统中的一群接口。
也就是说 就是通过定义一个一致的接口(界面类),用以屏蔽内部子系统的细节,使得调用端只需跟这个接口发生调用,而无需关心这个子系统的内部细节 => 外观模式
13.3 外观模式(过程模式)
1 基本介绍
(1)外观模式就是解决多个复杂接口带来的使用困难,起到简化用户操作的作用 。外观模式为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
(2)外观模式通过定义一个一致的接口,用以屏蔽内部子系统的细节,使得调用端只需跟这个接口发生调用,而无需关心这个子系统的内部细节。
- 类图
(1)外观类(Facade): 为调用端提供统一的调用接口, 外观类知道哪些子系统负责处理请求,从而将调用端的请求代理给适当子系统对象
(2)调用者(Client): 外观接口的调用者
(3)子系统的集合:指模块或者子系统,处理 Facade 对象指派的任务,他是功能的实际提供者。 - 外观模式的使用的注意事项
(1)外观模式对外屏蔽了子系统的细节,因此外观模式降低了客户端对子系统使用的复杂性
(2)外观模式对客户端与子系统解耦,让子系统内部的模块更易维护和扩展
(3)通过合理的使用外观模式,可以帮我们更好的划分访问的层次
(4)当系统需要进行分层设计时,可以考虑使用 Facade 模式
(5)在维护一个遗留的大型系统时,可能这个系统已经变得非常难以维护和扩展,此时可以考虑为新系统开发一个Facade 类,来提供遗留系统的比较清晰简单的接口,让新系统与 Facade 类交互,提高复用性
(6)不能过多的或者不合理的使用外观模式,使用外观模式好,还是直接调用模块好。要以让系统有层次,利于维护为目的。
13.4 外观模式应用案例:影院管理
- 类图
- 代码实现
(1)设备管理类:DVDPlayer
public class DVDPlayer {
//使用单例模式, 使用饿汉式
private static DVDPlayer instance = new DVDPlayer();
public static DVDPlayer getInstanc() {
return instance;
}
public void on() {
System.out.println(" dvd on ");
}
public void off() {
System.out.println(" dvd off ");
}
public void play() {
System.out.println(" dvd is playing ");
}
//....
public void pause() {
System.out.println(" dvd pause ..");
}
}
Popcorn(爆米花)、Screen(屏幕)、Projector(投影仪)、Stereo(立体声)、TheaterLight(灯光)类 与DVDPlayer类类似。
(2)统一调用接口:HomeTheaterFacade类
public class HomeTheaterFacade {
//定义各个子系统对象
private TheaterLight theaterLight;
private Popcorn popcorn;
private Stereo stereo;
private Projector projector;
private Screen screen;
private DVDPlayer dVDPlayer;
//构造器
public HomeTheaterFacade() {
super();
this.theaterLight = TheaterLight.getInstance();
this.popcorn = Popcorn.getInstance();
this.stereo = Stereo.getInstance();
this.projector = Projector.getInstance();
this.screen = Screen.getInstance();
this.dVDPlayer = DVDPlayer.getInstanc();
}
//操作分成 4 步
public void ready() {
popcorn.on(); //爆米花机打开
popcorn.pop(); //爆米花机开始产生爆米花
screen.down(); //屏幕放下来
projector.on(); //投影仪打开
stereo.on(); //立体声打开
dVDPlayer.on(); //DVD打开
theaterLight.dim(); //灯光打开
}
public void play() {
dVDPlayer.play();
}
public void pause() {
dVDPlayer.pause();
}
public void end() {
popcorn.off();
theaterLight.bright();
screen.up();
projector.off();
stereo.off();
dVDPlayer.off();
}
}
(3)客户端
public class Client {
public static void main(String[] args) {
// TODO Auto-generated method stub
//这里直接调用。。 很麻烦
HomeTheaterFacade homeTheaterFacade = new HomeTheaterFacade();
homeTheaterFacade.ready();
homeTheaterFacade.play();
homeTheaterFacade.end();
}
}
13.5 外观模式在 MyBatis 框架应用的源码分析
- MyBatis 中的 Configuration 去创建 MetaObject 对象使用到外观模式
- 类图
第14章 享元模式
14.1 使用场景:网站展示
对于网站
(1)有客户要求以新闻的形式发布
(2)有客户人要求以博客的形式发布
(3)有客户希望以微信公众号的形式发布
14.2 传统方式解决网站展示
- 传统方式
(1)直接复制粘贴一份,然后根据客户不同要求,进行定制修改
(2)给每个网站租用一个空间 - 示意图
- 问题分析
需要的网站结构相似度很高,而且都不是高访问量网站,如果分成多个虚拟空间来处理,相当于一个相同网站的实例对象很多,造成服务器的资源浪费 - 解决思路:
整合到一个网站中,共享其相关的代码和数据,对于硬盘、内存、CPU、数据库空间等服务器资源都可以达成共享,减少服务器资源。=>享元模式
14.3 享元模式(Flyweight Pattern)
- 基本介绍
(1)也叫作蝇量模式: 运用共享技术有效地支持大量细粒度的对象
(2)常用于系统底层开发,解决系统的性能问题。像数据库连接池,里面都是创建好的连接对象,在这些连接对象中有我们需要的则直接拿来用,避免重新创建,如果没有我们需要的,则创建一个
(3)享元模式能够解决重复对象的内存浪费的问题,当系统中有大量相似对象,需要缓冲池时。不需总是创建新对象,可以从缓冲池里拿。这样可以降低系统内存,同时提高效率
(4)享元模式经典的应用场景就是池技术了,String 常量池、数据库连接池、缓冲池等等都是享元模式的应用,享元模式是池技术的重要实现方式 - 原理类图
说明
(1)FlyWeight 是抽象的享元角色, 他是产品的抽象类, 同时定义出对象的外部状态和内部状态(后面介绍) 的接口或实现
(2)ConcreteFlyWeight 是具体的享元角色,是具体的产品类,实现抽象角色定义相关业务
(3) UnSharedConcreteFlyWeight 是不可共享的角色,一般不会出现在享元工厂。
(4)FlyWeightFactory 享元工厂类,用于构建一个**池容器(**集合), 同时提供从池中获取对象方法。 - 内部状态和外部状态
(1)享元模式提出了两个要求:细粒度和共享对象。这里就涉及到内部状态和外部状态了,即将对象的信息分为两个部分:内部状态和外部状态
(2)内部状态指对象共享出来的信息,存储在享元对象内部且不会随环境的改变而改变
(3)外部状态指对象得以依赖的一个标记,是随环境改变而改变的、不可共享的状态。
比如围棋、五子棋、跳棋,它们都有大量的棋子对象,围棋和五子棋只有黑白两色,跳棋颜色多一点,所以棋子颜 色就是棋子的内部状态;而各个棋子之间的差别就是位置的不同,当我们落子后,落子颜色是定的,但位置是变化 的,所以棋子坐标就是棋子的外部状态。
举个例子:围棋理论上有 361 个空位可以放棋子,每盘棋都有可能有两三百个棋子对象产生,因为内存空间有限,一台服务器很难支持更多的玩家玩围棋游戏,如果用享元模式来处理棋子,那么棋子对象就可以减少到只 有两个实例,这样就很好的解决了对象的开销问题。
4. 享元模式的注意事项
(1)在享元模式这样理解,“享”就表示共享,“元”表示对象
(2)系统中有大量对象,这些对象消耗大量内存,并且对象的状态大部分可以外部化时,我们就可以考虑选用享元模式
(3)用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象,用 HashMap/HashTable 存储
(4)享元模式提高了系统的复杂度。需要分离出内部状态和外部状态,而外部状态具有固化特性,不应该随着内部状态的改变而改变,这是我们使用享元模式需要注意的地方.
(5)使用享元模式时,注意划分内部状态和外部状态,并且需要有一个工厂类加以控制。
(6)享元模式经典的应用场景是需要缓冲池的场景,比如 String 常量池、数据库连接池
14.4 享元模式应用案例:网站展示
- 类图
- 代码实现
(1)抽象类:WebSite类
public abstract class WebSite {
public abstract void use(User user);//抽象方法
}
(2)网站的具体实现类:ConcreteWebSite类。
此处具体的网站就是具体的状态。
//具体网站
public class ConcreteWebSite extends WebSite {
//共享的部分,内部状态
private String type = ""; //网站发布的形式(类型)
//构造器
public ConcreteWebSite(String type) {
this.type = type;
}
@Override
public void use(User user) {
System.out.println("网站的发布形式为:" + type + " 在使用中 .. 使用者是" + user.getName());
}
}
(3)网站工厂类:WebSiteFactory
// 网站工厂类,根据需要返回压一个网站
public class WebSiteFactory {
//集合, 充当池的作用
private HashMap<String, ConcreteWebSite> pool = new HashMap<>();
//根据网站的类型,返回一个网站, 如果没有就创建一个网站,并放入到池中,并返回
public WebSite getWebSiteCategory(String type) {
if(!pool.containsKey(type)) {
//就创建一个网站,并放入到池中
pool.put(type, new ConcreteWebSite(type));
}
return (WebSite)pool.get(type);
}
//获取网站分类的总数 (池中有多少个网站类型)
public int getWebSiteCount() {
return pool.size();
}
}
(4)外部状态
public class User {
private String name;
public User(String name) {
super();
this.name = name;
}
//name的set和get方法
}
(5)客户端
public class Client {
public static void main(String[] args) {
// TODO Auto-generated method stub
// 创建一个工厂类
WebSiteFactory factory = new WebSiteFactory();
// 客户要一个以新闻形式发布的网站
WebSite webSite1 = factory.getWebSiteCategory("新闻");
webSite1.use(new User("tom")); //外部状态的使用
// 客户要一个以博客形式发布的网站
WebSite webSite2 = factory.getWebSiteCategory("博客");
webSite2.use(new User("jack"));
// 客户要一个以博客形式发布的网站
WebSite webSite3 = factory.getWebSiteCategory("博客");
webSite3.use(new User("smith"));
// 客户要一个以博客形式发布的网站
WebSite webSite4 = factory.getWebSiteCategory("博客");
webSite4.use(new User("king"));
System.out.println("网站的分类共=" + factory.getWebSiteCount());
}
}