来源:《图说设计模式》
结构型模式(Structural Pattern)描述如何将类或者对象结合在一起形成更大的结构,就像搭积木,可以通过 简单积木的组合形成复杂的、功能更为强大的结构。
结构型模式可以分为类结构型模式和对象结构型模式:
类结构型模式关心类的组合,一般只存在继承关系和实现关系。
对象结构型模式关心类与对象的组合,一般关系为关联关系。 根据“合成复用原则”,在系统中尽量使用关联关系来替代继承关系,因此大部分类结构型模式都是对象结构型模式。
包含模式
适配器模式(Adapter)
重要程度:4桥接模式(Bridge)
重要程度:3组合模式(Composite)
重要程度:4装饰模式(Decorator)
重要程度:3外观模式(Facade)
重要程度:5享元模式(Flyweight)
重要程度:1代理模式(Proxy)
重要程度:4
一、适配器模式(Adapter)
模式动机
适配器可以使由于接口不兼容而不能交互的类可以一起工作。类似于电源适配器的设计。
模式结构
适配器模式包含如下角色:
Target:目标抽象类
Adapter:适配器类
Adaptee:适配者类
Client:客户类
适配器模式有对象适配器和类适配器两种实现:
-
类适配器:
类适配器,Adapter 类既继承了 Adaptee (被适配类),也实现了 Target 接口
-
对象适配器:
对象适配器,它不是使用多继承或继承再实现的方式,而是使用直接关联,或者称为委托的方式。
从类图中我们也可以看到 对象适配器比起类适配器 需要修改的只不过就是 Adapter 类的内部结构,即 Adapter 自身必须先拥有一个被适配类的对象,再把具体的特殊功能委托给这个对象来实现。使用对象适配器模式,可以使得 Adapter 类(适配类)根据传入的 Adaptee 对象达到适配多个不同被适配类的功能,当然,此时我们可以为多个被适配类提取出一个接口或抽象类。这样看起来的话,似乎对象适配器模式更加灵活一点。
代码分析
典型的类适配器代码
// 已存在的、具有特殊功能、但不符合我们既有的标准接口的类
class Adaptee {
public void specificRequest() {
System.out.println("被适配类具有 特殊功能...");
}
}
// 目标接口,或称为标准接口
interface Target {
public void request();
}
// 具体目标类,只提供普通功能
class ConcreteTarget implements Target {
public void request() {
System.out.println("普通类 具有 普通功能...");
}
}
// 适配器类,继承了被适配类,同时实现标准接口
class Adapter extends Adaptee implements Target{
public void request() {
super.specificRequest();
}
}
典型的对象适配器代码
// 适配器类,直接关联被适配类,同时实现标准接口
class Adapter implements Target{
// 直接关联被适配类
private Adaptee adaptee;
// 可以通过构造函数传入具体需要适配的被适配类对象
public Adapter (Adaptee adaptee) {
this.adaptee = adaptee;
}
public void request() {
// 这里是使用委托的方式完成特殊功能
this.adaptee.specificRequest();
}
}
两种方式的区别 :
- 对象适配器模式可以直接使用一个已有的Adaptee的实例来转换接口。
- 类适配器继承了Adaptee,所以可以通过覆写来扩展SpecificRequest()
- 类适配器模式因为是继承所以相对静态,而对象适配器模式是包含是组合相对灵活(可以通过写adaptee子类扩展功能)
优点
将目标类和适配者类解耦,增加了类的透明性和复用性,灵活性和扩展性都非常好,完全符合“开闭原则”。
缺点
类适配器一次最多只能适配一个适配者类,使用有一定的局限性。(不能多继承)
而对象适配器虽然可以把适配者类和它的子类都适配到目标接口,但是更改适配者的方法十分麻烦,既需要更改适配器,也需要更改适配者
应用
JDBC驱动软件就是一个介于JDBC接口和数据库引擎接口之间的适配器软件。
扩展
当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求,它适用于一个接口不想使用其所有的方法的情况。因此也称为单接口适配器模式。
二、桥接模式(Bridge)
模式动机
设想如果要绘制矩形、圆形、椭圆、正方形,我们至少需要4个形状类,但是如果绘制的图形需要具有不同的颜色,如红色、绿色、蓝色等,此时至少有如下两种设计方案:
第一种设计方案是为每一种形状都提供一套各种颜色的版本。
第二种设计方案是根据实际需要对形状和颜色进行组合
对于有两个变化维度的系统,采用方案二来进行设计系统中类的个数更少,且系统扩展更为方便。设计方案二即是桥接模式的应用。桥接模式将继承关系转换为关联关系,从而降低了类与类之间的耦合,减少了代码编写量。
模式定义
桥接模式(Bridge Pattern):将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式。
模式结构
桥接模式包含如下角色:
Abstraction:抽象类
RefinedAbstraction:扩充抽象类
Implementor:实现类接口
ConcreteImplementor:具体实现类
代码分析
interface Implementor{
void operationImpl();
}
abstract class Abstraction{
protected Implementor implementor;
public Abstraction(Implementor implementor){
this.implementor = implementor;
}
public void operation(){
implementor.operationImpl();
}
}
class ConcreteImplementorA implements Implementor {
@Override
public void operationImpl() {
System.out.println("具体实现A");
}
}
class ConcreteImplementorB implements Implementor {
@Override
public void operationImpl() {
System.out.println("具体实现B");
}
}
class RefinedAbstraction extends Abstraction{
public RefinedAbstraction(Implementor implementor) {
super(implementor);
}
public void otherOperation(){
System.out.println("其他操作");
}
}
public class MainTest {
public static void main(String arg[]) {
Implementor implementor = new ConcreteImplementorA();
RefinedAbstraction abstraction = new RefinedAbstraction(implementor);
abstraction.operation();
abstraction.otherOperation();
}
}
扩展
- 桥接模式和适配器模式的区别
很多时候经常容易把桥接模式和适配器模式弄混。那什么时候用桥接,什么时候用适配器呢 ?
共同点:
桥接和适配器都是让两个东西配合工作
不同点:
适配器:改变已有的两个接口,让他们相容。
桥接模式:分离抽象化和实现,使两者的接口可以不同,目的是分离。
所以说,如果你拿到两个已有模块,想让他们同时工作,那么你使用的适配器。 如果你还什么都没有,但是想分开实现,那么桥接是一个选择。
桥接是先有桥,才有两端的东西 适配是先有两边的东西,才有适配器 。
桥模式并不同于适配器模式,适配器模式其实是一个事后诸葛亮,当发现以前的东西不适用了才去做一个弥补的措施。桥模式相对来说所做的改变比适配器模式早,它可以适用于有两个甚至两个以上维度的变化。
桥接模式将继承关系转换为关联关系,从而降低了类与类之间的耦合,减少了代码编写量。
三、组合模式(Composite)
模式动机
将对象组合成树形结构以表示‘部分-整体’的层次结构。组合模式使得用户对耽搁对象和组合对象的使用具有一致性。
模式结构
代码分析
public abstract class Company {
private String name;
public Company(String name) {
this.name = name;
}
public Company() {
}
protected abstract void add(Company company);
protected abstract void remove(Company company);
protected abstract void display(int depth);
}
public class ConcreteCompany extends Company {
private List<Company> cList;
public ConcreteCompany() {
cList = new ArrayList<Company>();
}
public ConcreteCompany(String name) {
super(name);
cList = new ArrayList<Company>() ;
}
@Override
protected void add(Company company) {
cList.add(company);
}
@Override
protected void display(int depth) {
StringBuilder sb = new StringBuilder("");
for (int i = 0; i < depth; i++) {
sb.append("-");
}
System.out.println(new String(sb) + this.getName());
for (Company c : cList) {
c.display(depth + 2);
}
}
@Override
protected void remove(Company company) {
cList.remove(company);
}
}
public class FinanceDepartment extends Company {
public FinanceDepartment(){
}
public FinanceDepartment(String name){
super(name);
}
@Override
protected void add(Company company) {
}
@Override
protected void display(int depth) {
StringBuilder sb = new StringBuilder("");
for (int i = 0; i < depth; i++) {
sb.append("-");
}
System.out.println(new String(sb) + this.getName() ) ;
}
@Override
protected void remove(Company company) {
}
}
public class HRDepartment extends Company {
public HRDepartment(){
}
public HRDepartment(String name){
super(name);
}
@Override
protected void add(Company company) {
}
@Override
protected void display(int depth) {
StringBuilder sb = new StringBuilder("");
for (int i = 0; i < depth; i++) {
sb.append("-");
}
System.out.println(new String(sb) + this.getName() ) ;
}
@Override
protected void remove(Company company) {
}
}
public class Client {
public static void main(String[] args) {
Company root = new ConcreteCompany();
root.setName("北京总公司");
root.add(new HRDepartment("总公司人力资源部"));
root.add(new FinanceDepartment("总公司财务部"));
Company shandongCom = new ConcreteCompany("山东分公司");
shandongCom.add(new HRDepartment("山东分公司人力资源部"));
shandongCom.add(new FinanceDepartment("山东分公司账务部"));
Company jinanCom = new ConcreteCompany("济南办事处");
jinanCom.add(new FinanceDepartment("济南办事处财务部"));
jinanCom.add(new HRDepartment("济南办事处人力资源部"));
shandongCom.add(jinanCom);
root.add(shandongCom);
root.display(0);
}
}
类似于树
四、装饰模式(Decorator)
模式动机
一般有两种方式可以实现给一个类或对象增加行为:
- 继承机制
- 关联机制,即将一个类的对象嵌入另一个对象中,由另一个对象来决定是否调用嵌入对象的行为以便扩展自己的行为,我们称这个嵌入的对象为装饰器(Decorator)
装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。这就是装饰模式的模式动机。
模式定义
装饰模式(Decorator Pattern) :动态地给一个对象增加一些额外的职责(Responsibility),它是一种对象结构型模式。
模式结构
装饰模式包含如下角色:
Component: 抽象构件
ConcreteComponent: 具体构件
Decorator: 抽象装饰类
ConcreteDecorator: 具体装饰类
代码分析
现在需要一个汉堡,主体是鸡腿堡,可以选择添加生菜、酱、辣椒等等许多其他的配料,这种情况下就可以使用装饰者模式。
//汉堡基类(被装饰者)
public abstract class Humburger {
protected String name ;
public String getName(){
return name;
}
public abstract double getPrice();
}
//鸡腿堡类(被装饰者的初始状态,有些自己的简单装饰)
public class ChickenBurger extends Humburger {
public ChickenBurger(){
name = "鸡腿堡";
}
@Override
public double getPrice() {
return 10;
}
}
//配料的基类(装饰者,用来对汉堡进行多层装饰,每层装饰增加一些配料)
public abstract class Condiment extends Humburger {
public abstract String getName();
}
//生菜(装饰者的第一层)
public class Lettuce extends Condiment {
Humburger humburger;
public Lettuce(Humburger humburger){
this.humburger = humburger;
}
@Override
public String getName() {
return humburger.getName()+" 加生菜";
}
@Override
public double getPrice() {
return humburger.getPrice()+1.5;
}
}
//辣椒(装饰者的第二层)
public class Chilli extends Condiment {
Humburger humburger;
public Chilli(Humburger humburger){
this.humburger = humburger;
}
@Override
public String getName() {
return humburger.getName()+" 加辣椒";
}
@Override
public double getPrice() {
return humburger.getPrice(); //辣椒是免费的哦
}
}
//测试类
public static void main(String[] args) {
Humburger humburger = new ChickenBurger();
System.out.println(humburger.getName()+" 价钱:"+humburger.getPrice());
Lettuce lettuce = new Lettuce(humburger);
System.out.println(lettuce.getName()+" 价钱:"+lettuce.getPrice());
Chilli chilli = new Chilli(humburger);
System.out.println(chilli.getName()+" 价钱:"+chilli.getPrice());
Chilli chilli2 = new Chilli(lettuce);
System.out.println(chilli2.getName()+" 价钱:"+chilli2.getPrice());
}
应用
Java 的I/O API就是使用装饰模式实现的
五、外观模式(Facade)
模式定义
外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得子系统更加容易使用。外观模式又称为门面模式,它是一种对象结构型模式。
模式结构
外观模式包含如下角色:
Facade: 外观角色
SubSystem:子系统角色
代码分析
int main()
{
Facade fa;
fa.wrapOpration();
return 0;
}
class Facade
{
public:
Facade();
virtual ~Facade();
void wrapOpration();
private:
SystemC *m_SystemC;
SystemA *m_SystemA;
SystemB *m_SystemB;
};
Facade::Facade(){
m_SystemA = new SystemA();
m_SystemB = new SystemB();
m_SystemC = new SystemC();
}
void Facade::wrapOpration(){
m_SystemA->operationA();
m_SystemB->operationB();
m_SystemC->opeartionC();
}
模式分析
据“单一职责原则”,在软件中将一个系统划分为若干个子系统有利于降低整个系统的复杂性,一个常见的设计目标是使子系统间的通信和相互依赖关系达到最小,而达到该目标的途径之一就是引入一个外观对象,它为子系统的访问提供了一个简单而单一的入口。
外观模式也是“迪米特法则”的体现,通过引入一个新的外观类可以降低原有系统的复杂度,同时降低客户类与子系统类的耦合度。
外观类将客户端与子系统的内部复杂性分隔开,使得客户端只需要与外观对象打交道,而不需要与子系统内部的很多对象打交道。
优点
对客户屏蔽子系统组件,实现了子系统与客户之间的松耦合关系
缺点
在不引入抽象外观类的情况下,增加新的子系统可能需要修改外观类的源代码,违背了“开闭原则”。
扩展
一个系统有多个外观类
在一个系统中可以设计多个外观类,每个外观类都负责和一些特定的子系统交互,向用户提供相应的业务功能。不要通过继承一个外观类在子系统中加入新的行为
外观模式的用意是为子系统提供一个集中化和简化的沟通渠道,而不是向子系统加入新的行为,新的行为的增加应该通过修改原有子系统类或增加新的子系统类来实现,不能通过外观类来实现。外观模式与迪米特法则(最少知道原则)
外观类充当了客户类与子系统类之间的“第三者”,降低了客户类与子系统类之间的耦合度,外观模式就是实现代码重构以便达到“迪米特法则”要求的一个强有力的武器。抽象外观类的引入
外观模式最大的缺点在于违背了“开闭原则”,当增加新的子系统或者移除子系统时需要修改外观类,可以通过引入抽象外观类在一定程度上解决该问题,客户端针对抽象外观类进行编程。对于新的业务需求,不修改原有外观类,而对应增加一个新的具体外观类,由新的具体外观类来关联新的子系统对象,同时可以通过修改配置文件来达到不修改源代码并更换外观类的目的。
六、享元模式(Flyweight)
模式定义
对象的多次复用
模式结构
享元模式包含如下角色:
Flyweight: 抽象享元类
ConcreteFlyweight: 具体享元类
UnsharedConcreteFlyweight: 非共享具体享元类
FlyweightFactory: 享元工厂类
优点
可以极大减少内存中对象的数量,使得相同对象或相似对象在内存中只保存一份。
缺点
享元模式需要将享元对象的状态外部化,而读取外部状态使得运行时间变长。
应用
享元模式在编辑器软件中大量使用,如在一个文档中多次出现相同的图片,则只需要创建一个图片对象,通过在应用程序中设置该图片出现的位置,可以实现该图片在不同地方多次重复显示。
七、代理模式(Proxy)
模式动机
在某些情况下,客户不想或者不能直接引用一个对 象,此时可以通过一个称之为“代理”的第三者来实现 间接引用。代理对象可以在客户端和目标对象之间起到 中介的作用,并且可以通过代理对象去掉客户不能看到 的内容和服务或者添加客户需要的额外服务。
模式定义
给某一个对象提供一个代 理,并由代理对象控制对原对象的引用。它是一种对象结构型模式。
模式结构
代理模式包含如下角色:
Subject: 抽象主题角色
Proxy: 代理主题角色
RealSubject: 真实主题角色
扩展
几种常用的代理模式
- 图片代理
一个很常见的代理模式的应用实例就是对大图浏览的控制。
用户通过浏览器访问网页时先不加载真实的大图,而是通过代理对象的方法来进行处理,在代理对象的方法中,先使用一个线程向客户端浏览器加载一个小图片,然后在后台使用另一个线程来调用大图片的加载方法将大图片加载到客户端。当需要浏览大图片时,再将大图片在新网页中显示。如果用户在浏览大图时加载工作还没有完成,可以再启动一个线程来显示相应的提示信息。通过代理技术结合多线程编程将真实图片的加载放到后台来操作,不影响前台图片的浏览。
远程代理:远程代理可以将网络的细节隐藏起来,使得客户端不必考虑网络的存在。客户完全可以认为被代理的远程业务对象是局域的而不是远程的,而远程代理对象承担了大部分的网络通信工作。
虚拟代理:当一个对象的加载十分耗费资源的时候,虚拟代理的优势就非常明显地体现出来了。虚拟代理模式是一种内存节省技术,那些占用大量内存或处理复杂的对象将推迟到使用它的时候才创建。
动态代理
较为高级的代理模式,它的典型应用就是Spring AOP。