当前位置: 首页 > news >正文

设计模式复习

设计模式

1、什么是设计模式

一个模式描述了一个在我们周围不断重复发生的问题以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动.

尽管Alexander所指的是城市和建筑模式,但他的思想也同样适用于于面向对象设计模式,只是在面向对象的解决方案里, 我们那个对象和接口代替了墙壁和门窗。两类模式的核心都在于提供了相关问题的解决方案。一般而言,设计模式有四个基本要素

  • 1、模式名称(pattern name):一个助记名,它用一两个词来描述模式的问题、解决方案和效果。
  • 2、问题(problem):描述了应该在何时使用模式。
  • 3、解决方案(solution):描述了设计的组成成分,它们之间的相关关系以及各自的职责和协作方案。
  • 4、效果(consequences):描述了模式应用的效果以及使用模式应该权衡的问题。

总的来说,设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结也就是本来并不存在所谓设计模式,用的人多了,也便成了设计模式。

2、设计模式的七大原则

面向对象的设计模式有七大基本原则:

  • 开闭原则(Open Closed Principle,OCP)
  • 单一职责原则(Single Responsibility Principle, SRP)
  • 里氏代换原则(Liskov Substitution Principle,LSP)
  • 依赖倒转原则(Dependency Inversion Principle,DIP)
  • 接口隔离原则(Interface Segregation Principle,ISP)
  • 合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)
  • 最少知识原则(Least Knowledge Principle,LKP)或者迪米特法则(Law of Demeter,LOD)
设计模式原则名称简单定义
开闭原则对扩展开放,对修改关闭
单一职责原则一个类只负责一个功能领域中的相应职责
里氏代换原则所有引用基类的地方必须能透明地使用其子类的对象
依赖倒转原则依赖于抽象,不能依赖于具体实现
接口隔离原则类之间的依赖关系应该建立在最小的接口上
合成/聚合复用原则尽量使用合成/聚合,而不是通过继承达到复用的目的
迪米特法则一个软件实体应当尽可能少的与其他实体发生相互作用

tips:

高内聚 低耦合

高内聚 是 关于这个方面的 内聚起来 比如都是数学

低耦合 是 相关的两个方面 尽量不要相关 比如 数学和 物理

高内聚就是说相关度比较高的部分尽可能的集中,不要分散

低耦合就是说两个相关的模块尽可以能把依赖的部分降低到最小,不要让两个系统产生强依赖


思维导图

gof设计模式常用的有几种
23个

一、创建型模式:

    单例模式、抽象工厂模式、建造者模式、工厂模式、原型模式。

二、结构型模式:

    适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。

三、行为型模式:

    模版方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式、访问者模式。
创建型模式
工厂模式Factory
抽象工厂模式AbstractFatory
单例模式Singleton
建造者模式Builder
原型模式Prototype
创建型模式
这些设计模式提供了一种在创建对象的同时隐藏创建逻辑的方式,而不是使用 new 运算符直接实例化对象。这使得程序在判断针对某个给定实例需要创建哪些对象时更加灵活。

结构型模式
适配器模式Adapter
桥接模式Bridge
过滤器模式Filter
组合模式Composite
装饰器模式Decorator
外观模式Facade
享元模式Flyweight
代理模式Proxy
结构型模式
这些设计模式关注类和对象的组合。继承的概念被用来组合接口和定义组合对象获得新功能的方式。

行为型模式
责任链模式Chain
命令模式Command
解释器模式Interpreter
迭代器模式Iterator
中介者模式Mediator
备忘录模式Memento
观察者模式Observer
状态模式State
空对象模式Null
策略模Strategy
模板模Template
访问者模式Visitor
行为型模式
这些设计模式特别关注对象之间的通信。

J2EE模式
业务代表模式BusinessDelegate
组合实体模式CompositeEntity
数据访问对象模式DataAccessObject
前端控制器模式FrontController
拦截过滤器模式InterceptingFilter
服务定位器模式ServiceLocator
传输对象模式TransfeObject
J2EE 模式
这些设计模式特别关注表示层。这些模式是由 Sun Java Center 鉴定的。

image

设计模式的六大原则
1、单一原则
1、开闭原则(Open Close Principle) 开闭原则的意思是:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。2、里氏代换原则(Liskov Substitution Principle)里氏代换原则是面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP 是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。3、依赖倒转原则(Dependence Inversion Principle)这个原则是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于具体。依赖倒置原则的核心思想是面向接口编程。4、接口隔离原则(Interface Segregation Principle)这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。它还有另外一个意思是:降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦合。 接口最小功能。5、迪米特法则,又称最少知道原则(Demeter Principle)最少知道原则是指:一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。6、合成复用原则(Composite Reuse Principle)合成复用原则是指:尽量使用合成/聚合的方式,而不是使用继承。依赖倒转
依赖倒置原则在Java语言中的表现是:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或者抽象类产生的;
接口或抽象类不依赖于实现类;
实现类依赖接口或抽象类。

依赖于抽象,不要依赖于具体是依赖倒置原则。 依赖倒置原则(Dependence Inversion Principle)是程序要依赖于抽象接口,不要依赖于具体实现

七大规则解释

2.1、开闭原则

模块在尽量不修改原代码(原来的代码)的情况下进行扩展(开放)。

开闭原则(Open Closed Principle,OCP)的定义是:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。模块应尽量在不修改原(是"原",指原来的代码)代码的情况下进行扩展。

开闭原则的意义:

在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。

如何实现对扩展开放,对修改关闭?

要实现对扩展(抽象类)开放,对修改(原有代码)关闭,即遵循开闭原则,需要对系统进行抽象化设计,抽象可以基于抽象类或者接口。一般来说需要做到几点:

  • 1、通过接口或者抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法,也就是扩展必须添加具体实现而不是改变具体的方法。(继承,再去添加具体实现,后面的方法不要动)
  • 2、参数类型、引用对象尽量使用接口或者抽象类,而不是实现类,这样就能尽量保证抽象层是稳定的。
  • 3、一般抽象模块设计完成(例如接口的方法已经敲定),不允许修改接口或者抽象方法的定义

下面通过一个例子遵循开闭原则进行设计,场景是这样:某系统的后台需要监测业务数据展示图表,如柱状图、折线图等,在未来需要支持图表的着色操作。在开始设计的时候,代码可能是这样的:

public class BarChart {public void draw(){System.out.println("Draw bar chart...");}
}public class LineChart {public void draw(){System.out.println("Draw line chart...");}
}public class App {public void drawChart(String type){if (type.equalsIgnoreCase("line")){new LineChart().draw();}else if (type.equalsIgnoreCase("bar")){new BarChart().draw();}}
}

这样做在初期是能满足业务需要的,开发效率也十分高,但是当后面需要新增一个饼状图的时候,既要添加一个饼状图的类,原来的客户端App类的drawChart方法也要新增一个if分支,这样做就是修改了原有客户端类库的方法,是十分不合理的。如果这个时候,在图中加入一个颜色属性,复杂性也大大提高。基于此,需要引入一个抽象Chart类AbstractChart,App类在画图的时候总是把相关的操作委托到具体的AbstractChart的派生类实例,这样的话App类的代码就不用修改:

public abstract class AbstractChart {public abstract void draw();
}public class BarChart extends AbstractChart{@Overridepublic void draw() {System.out.println("Draw bar chart...");}
}public class LineChart extends AbstractChart {@Overridepublic void draw() {System.out.println("Draw line chart...");}
}public class App {public void drawChart(AbstractChart chart){chart.draw();}
}

如果新加一种图,只需要新增一个AbstractChart的子类即可。客户端类App不需要改变原来的逻辑。修改后的设计符合开闭原则,因为整个系统在扩展时原有的代码没有做任何修改。


2.2、单一职责原则

单一职责原则(Single Responsibility Principle, SRP)的定义是:指一个类或者模块应该有且只有一个改变的原因。如果一个类承担的职责过多,就等于把这些职责耦合在一起了。一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当发生变化时,设计会遭受到意想不到的破坏。而如果想要避免这种现象的发生,就要尽可能的遵守单一职责原则。此原则的核心就是解耦和增强内聚性

单一职责原则的意义:

单一职责原则告诉我们:一个类不能做太多的东西。在软件系统中,一个类(一个模块、或者一个方法)承担的职责越多,那么其被单一职责原则的意义:

单一职责原则告诉我们:一个类不能做太多的东西。在软件系统中,一个类(一个模块、或者一个方法)承担的职责越多,那么其被复用的可能性就会越低。一个很典型的例子就是万能类。其实可以说一句大实话:任何一个常规的MVC项目,在极端的情况下,可以用一个类(甚至一个方法)完成所有的功能。但是这样做就会严重耦合,甚至牵一发动全身。一个类承(一个模块、或者一个方法)担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作,因此要将这些职责进行分离,将不同的职责封装在不同的类中,即将不同的变化原因封装在不同的类中,如果多个职责总是同时发生改变则可将它们封装在同一类中。

不过说实话,其实有的时候很难去衡量一个类的职责,主要是很难确定职责的粒度。这一点不仅仅体现在一个类或者一个模块中,也体现在采用微服务的分布式系统中。这也就是为什么我们在实施微服务拆分的时候经常会撕逼:"这个功能不应该发在A服务中,它不做这个领域的东西,应该放在B服务中"诸如此类的争论。存在争论是合理的,不过最好不要不了了之,而应该按照领域定义好每个服务的职责(职责的粒度最好找业务和架构专家咨询),得出相对合理的职责分配。(有争议是合理的。要商量 提出问题和建议)

下面通过一个很简单的实例说明一下单一职责原则:

在一个项目系统代码编写的时候,由于历史原因和人为的不规范,导致项目没有分层,一个Service类的伪代码是这样的:

public class Service {public UserDTO findUser(String name){Connection connection = getConnection();PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM t_user WHERE name = ?");preparedStatement.setObject(1, name);User user = //处理结果UserDTO dto = new UserDTO();//entity值拷贝到dtoreturn dto;}
}

这里出现一个问题,Service做了太多东西,包括数据库连接的管理,Sql的执行这些业务层不应该接触到的逻辑,更可怕的是,例如到时候如果数据库换成了Oracle,这个方法将会大改。因此,拆分出新的DataBaseUtils类用于专门管理数据库资源,Dao类用于专门执行查询和查询结果封装,改造后Service类的伪代码如下: 把功能划分出来

public class Service {private Dao dao;public UserDTO findUser(String name){User user =  dao.findUserByName(name);UserDTO dto = new UserDTO();//entity值拷贝到dtoreturn dto;}
}public class Dao{public User findUserByName(String name){Connection connection = DataBaseUtils.getConnnection();PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM t_user WHERE name = ?");preparedStatement.setObject(1, name);User user = //处理结果return user;}
}

现在,如果有查询封装的变动只需要修改Dao类,数据库相关变动只需要修改DataBaseUtils类,每个类的职责分明。这个时候,如果我们要把底层的存储结构缓成Redis或者MongoDB怎么办,这样显然要重建整个Dao类,这种情况下,需要进行接口隔离,下面分析接口隔离原则的时候再详细分析。

2.3、里氏代换原则

爸爸能做的,儿子也能做

里氏代换原则(Liskov Substitution Principle,LSP)的定义是:所有引用基类的地方必须能透明地使用其子类的对象,也可以简单理解为任何基类可以出现的地方,子类一定可以出现。

里氏代换原则的意义:

只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对"开-闭"原则的补充。实现"开-闭"原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。当然,如果反过来,软件单位使用的是一个子类对象的话,那么它不一定能够使用基类对象。举个很简单的例子说明这个问题:如果一个方法接收Map类型参数,那么它一定可以接收Map的子类参数例如HashMap、LinkedHashMap、ConcurrentHashMap类型的参数;但是返过来,如果另一个方法只接收HashMap类型的参数,那么它一定不能接收所有Map类型的参数,否则它可以接收LinkedHashMap、ConcurrentHashMap类型的参数。

开闭原则的延续,开闭原则就是希望子类对父类应用,而不影响原来的代码。里氏代换更像是具体实现出来后的东西,表达的意思还是很相似。

子类为什么可以替换基类的位置?

其实原因很简单,只要存在继承关系,基类的所有非私有属性或者方法,子类都可以通过继承获得(白箱复用),反过来不成立,因为子类很有可能扩充自身的非私有属性或者方法,这个时候不能用基类获取子类新增的这些属性或者方法。

里氏代换原则是实现开闭原则的基础,它告诉我们在设计程序的时候进可能使用基类进行对象的定义和引用,在运行时再决定基类的具体子类型

举个简单的例子,假设一种会呼吸的动物作为父类,子类猪和鸟也有自身的呼吸方式:

public abstract class Animal {protected abstract void breathe();
}public class Bird extends Animal {@Overridepublic void breathe() {System.out.println("Bird breathes...");}
}public class Pig extends Animal {@Overridepublic void breathe() {System.out.println("Pig breathes...");}
}public class App {public static void main(String[] args) throws Exception {Animal bird = new Bird();bird.breathe();Animal pig = new Pig();pig.breathe();}
}    

2.4、依赖倒转原则

java来看就是 面向接口(抽象)编程 为了用最高层 不依赖 不耦合

依赖倒转原则的意义:

​ 依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。为了确保该原则的应用,一个具体类应当只实现接口或抽象类中声明过的方法,而不要给出多余的方法,否则将无法调用到在子类中增加的新方法。在引入抽象层后,系统将具有很好的灵活性,在程序中尽量使用抽象层进行编程,而将具体类写在配置文件中,这样一来,如果系统行为发生变化,只需要对抽象层进行扩展,并修改配置文件,而无须修改原有系统的源代码,在不修改的情况下来扩展系统的功能,满足开闭原则的要求

依赖倒转原则的注意事项:

  • 高层模块不应该依赖低层模块,高层模块和低层模块都应该依赖于抽象。
  • 抽象不应该依赖于具体,具体应该依赖于抽象。

在实现依赖倒转原则时,我们需要针对抽象层编程,而将具体类的对象通过依赖注入(DependencyInjection, DI)的方式注入到其他对象中,依赖注入是指当一个对象要与其他对象发生依赖关系时,通过抽象来注入所依赖的对象。常用的注入方式有三种,分别是:构造注入,设值注入(Setter注入)和接口注入。Spring的IOC是此实现的典范。

从Java角度看待依赖倒转原则的本质就是:面向接口(抽象)编程。

  • 每个具体的类都应该有其接口或者基类,或者两者都具备。
  • 类中的引用对象应该是接口或者基类。
  • 任何具体类都不应该派生出子类。
  • 尽量不要覆写基类中的方法。
  • 结合里氏代换原则使用。

遵循依赖倒转原则的一个例子,场景是司机开车:

(感觉真的好有生活感啊)

public interface Driver {void drive();void setCar(Car car);
}public interface Car {void run();
}public class DefaultDriver implements Driver {private Car car;@Overridepublic void drive() {car.run();}@Overridepublic void setCar(Car car) {this.car = car;}
}public class Bmw implements Car {@Overridepublic void run() {System.out.println("Bmw runs...");}
}public class Benz implements Car {@Overridepublic void run() {System.out.println("Benz runs...");}
}public class App {public static void main(String[] args) throws Exception {Driver driver = new DefaultDriver();Car car = new Benz();driver.setCar(car);driver.drive();car = new Bmw();driver.setCar(car);driver.drive();}
}

这样实现了一个司机可以开各种类型的车,如果还有其他类型的车,只需要新加一个Car的实现即可。


2.5、接口隔离原则

与单一职责有一些矛盾

当一个接口事务过多,粒度需要变的更小

接口隔离原则(Interface Segregation Principle,ISP)的定义是客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上。简单来说就是建立单一的接口,不要建立臃肿庞大的接口。也就是接口尽量细化,同时接口中的方法尽量少。

如何看待接口隔离原则和单一职责原则?

单一职责原则注重的是类和接口的职责单一,这里职责是从业务逻辑上划分的,但是在接口隔离原则要求当一个接口太大时,我们需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。也就是说,我们在设计接口的时候有可能满足单一职责原则但是不满足接口隔离原则。

意思是单一职责,要小。

但是接口又要隔离,确实是,那我们把他归成一个类不就行了吗。

接口隔离原则的规范:

  • 使用接口隔离原则前首先需要满足单一职责原则。
  • 接口需要高内聚,也就是提高接口、类、模块的处理能力,少对外发布public的方法。
  • 定制服务,就是单独为一个个体提供优良的服务,简单来说就是拆分接口,对特定接口进行定制。
  • 接口设计是有限度的,接口的设计粒度越小,系统越灵活,但是值得注意不能过小,否则变成"字节码编程"。
public interface ValueOperations<K, V> {void set(K key, V value);void set(K key, V value, long timeout, TimeUnit unit);//....
}

如果有用过spring-data-redis的人就知道,RedisTemplate中持有一些列的基类,分别是ValueOperations(处理K-V)、ListOperations(处理Hash)、SetOperations(处理集合)等等。

不同接口处理不同事物


2.6、合成/聚合复用原则

更多的去理解。

拥有和组成的概念去 设计接口或者抽象类 然后去实现。

我拥有的(指外界) 和我身上的(内部) 是不一样的。

Has A or Is A

部分整体。

合成-聚合复用原则(Composite/Aggregate Reuse Principle),简称CARP也可称为合成复用原则。

合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)一般也叫合成复用原则(Composite Reuse Principle, CRP),定义是:尽量使用合成/聚合,而不是通过继承达到复用的目的

合成/聚合复用原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向内部持有的这些对象的委派达到复用已有功能的目的,而不是通过继承来获得已有的功能。

**聚合(Aggregate)的概念:**弱概念 群

聚合表示一种弱的"拥有"关系,一般表现为松散的整体和部分的关系,其实,所谓整体和部分也可以是完全不相关的。例如A对象持有B对象,B对象并不是A对象的一部分,也就是B对象的生命周期是B对象自身管理,和A对象不相关。

**合成(Composite)的概念:**强概念 主体的东西

合成表示一种强的"拥有"关系,一般表现为严格的整体和部分的关系,部分和整体的生命周期是一样的。

聚合和合成的关系:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

继承复用破坏包装,因为继承将基类的实现细节暴露给派生类,基类的内部细节通常对子类来说是可见的,这种复用也称为"白箱复用"。这里有一个明显的问题是:派生类继承自基类,如果基类的实现发生改变,将会影响到所有派生类的实现;如果从基类继承而来的实现是静态的,不可能在运行时发生改变,不够灵活。

由于合成或聚合关系可以将已有的对象,一般叫成员对象,纳入到新对象中,使之成为新对象的一部分,因此新对象可以调用已有对象的功能,这样做可以使得成员对象的内部实现细节对于新对象不可见,所以这种复用又称为**"黑箱"复用**,相对继承关系而言,其耦合度相对较低,成员对象的变化对新对象的影响不大,可以在新对象中根据实际需要有选择性地调用成员对象的操作;合成/聚合复用可以在运行时动态进行,新对象可以动态地引用与成员对象类型相同的其他对象。

如果有阅读过《Effective Java 2nd》的同学就知道,此书也建议慎用继承。一般情况下,只有明确知道派生类和基类满IS A的时候才选用继承,当满足HAS A或者不能判断的情况下应该选用合成/聚合。

下面举个很极端的例子说明一下如果在非IS A的情况下使用继承会出现什么问题:

先定义一个抽象手,手有一个摇摆的方法,然后定义左右手继承抽象手,实现摇摆方法:

public abstract class AbstractHand {protected abstract void swing();
}public class LeftHand extends AbstractHand {@Overridepublic void swing() {System.out.println("Left hand swings...");}
}public class RightHand extends AbstractHand {@Overridepublic void swing() {System.out.println("Right hand swings...");}

这里使用了合成,说明了Person和AbstractHand实例的生命周期是一致的。

肯定是要分开 生命周期


2.7、迪米特法则(最少知识原则)

迪米特法则(Law of Demeter,LOD),有时候也叫做最少知识原则(Least Knowledge Principle,LKP),它的定义是:一个软件实体应当尽可能少地与其他实体发生相互作用。每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。迪米特法则的初衷在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。迪米特法则不希望类之间建立直接的联系。如果真的有需要建立联系,也希望能通过它的友元类(中间类或者跳转类)来转达

接口隔离。也只是指接口。

这个是指类。

慢慢向下延申了

迪米特法则的规则:

  • Only talk to your immediate friends(只与直接的朋友通讯),一个对象的"朋友"包括他本身(this)、它持有的成员对象、入参对象、它所创建的对象。
  • 尽量少发布public的变量和方法,一旦公开的属性和方法越多,修改的时候影响的范围越大。
  • “是自己的就是自己的”,如果一个方法放在本类中,既不产生新的类间依赖,也不造成负面的影响,那么次方法就应该放在本类中。

迪米特法则的意义:

迪米特法则的核心观念就是类间解耦,也就降低类之间的耦合,只有类处于弱耦合状态,类的复用率才会提高。所谓降低类间耦合,实际上就是尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。但是这样会引发一个问题,有可能产生大量的中间类或者跳转类,导致系统的复杂性提高,可维护性降低。如果一味追求极度解耦,那么最终有可能变成面向字节码编程甚至是面向二进制的0和1编程。

举个很简单的例子,体育老师要知道班里面女生的人数,他委托体育课代表点清女生的人数:

public class Girl {}public class GroupLeader {private final List<Girl> girls;public GroupLeader(List<Girl> girls) {this.girls = girls;}public void countGirls() {System.out.println("The sum of girls is " + girls.size());}
}public class Teacher {public void command(GroupLeader leader){leader.countGirls();}
}public class App {public static void main(String[] args) throws Exception {Teacher teacher = new Teacher();GroupLeader groupLeader = new GroupLeader(Arrays.asList(new Girl(), new Girl()));teacher.command(groupLeader);}
}

这个例子中,体育课代表就是中间类,体育课代表对于体育老师来说就是"直接的朋友",如果去掉体育课代表这个中间类,体育老师必须亲自清点女生的人数(实际上就数人数这个功能,体育老师是不必要获取所有女生的对象列表),这样做会违反迪米特法则。

两个不相关的。

可以新弄一个类,符合迪米特原则,最少知识原则,完成友好调用。

设计模式中

门面模式(Facade )

中介模式(Mediator)

应用迪米特法则的例子

设计模式

创建者模式

工厂方法模式(简单)

工厂方法模式

一个工厂定义属性,还有一些必要的多个创建方式。

一个具体的厂子。

优点

  • 创建者和具体产品,减少耦合
  • 单一职责
  • 开闭原则

缺点

  • 需要引入许多新的子类。

    最好的情况是将该模式引入创建者类的现有层次结构中。

与其他模式的关系

  • 许多设计工作初期,都会使用工厂方法模式。只是简单的抽象。在我看来

随后演化,抽象工厂模式,原型模式或者生成器模式

(更灵活也更加复杂)

  • 抽象工厂模式,通常基于一组工厂方法,你也可以使用原型模式来生成这些类的方法。

  • 使用工厂方法+迭代器模式 让子类集合返回不同的类型的迭代器。并使得迭代器与集合相匹配。???

  • 原型并不基于继承, 因此没有继承的缺点。 另一方面, 原型需要对被复制对象进行复杂的初始化。 工厂方法基于继承, 但是它不需要初始化步骤。

  • 工厂方法是模板方法模式的一种特殊形式。 同时, 工厂方法可以作为一个大型模板方法中的一个步骤。

抽象工厂模式

抽象工厂模式

一个抽象的工厂,可以变成多个具体工厂。

客户端无需了解其所调用工厂的具体类信息。

生成器模式(复杂内部构造)

生成器设计模式

生成器模式是一种创建型设计模式, 使你能够分步骤创建复杂对象。 该模式允许你使用相同的创建代码生成不同类型和形式的对象。

一步一步来。

当你希望使用代码创建不同形式的产品 例如石头或木头房屋 **,** 可使用生成器模式**。**

伪代码

// 只有当产品较为复杂且需要详细配置时,使用生成器模式才有意义。下面的两个
// 产品尽管没有同样的接口,但却相互关联。
class Car is// 一辆汽车可能配备有 GPS 设备、行车电脑和几个座位。不同型号的汽车(// 运动型轿车、SUV 和敞篷车)可能会安装或启用不同的功能。class Manual is// 用户使用手册应该根据汽车配置进行编制,并介绍汽车的所有功能。// 生成器接口声明了创建产品对象不同部件的方法。
interface Builder ismethod reset()method setSeats(...)method setEngine(...)method setTripComputer(...)method setGPS(...)// 具体生成器类将遵循生成器接口并提供生成步骤的具体实现。你的程序中可能会
// 有多个以不同方式实现的生成器变体。
class CarBuilder implements Builder isprivate field car:Car// 一个新的生成器实例必须包含一个在后续组装过程中使用的空产品对象。constructor CarBuilder() isthis.reset()// reset(重置)方法可清除正在生成的对象。method reset() isthis.car = new Car()// 所有生成步骤都会与同一个产品实例进行交互。method setSeats(...) is// 设置汽车座位的数量。method setEngine(...) is// 安装指定的引擎。method setTripComputer(...) is// 安装行车电脑。method setGPS(...) is// 安装全球定位系统。// 具体生成器需要自行提供获取结果的方法。这是因为不同类型的生成器可能// 会创建不遵循相同接口的、完全不同的产品。所以也就无法在生成器接口中// 声明这些方法(至少在静态类型的编程语言中是这样的)。//// 通常在生成器实例将结果返回给客户端后,它们应该做好生成另一个产品的// 准备。因此生成器实例通常会在 `getProduct(获取产品)`方法主体末尾// 调用重置方法。但是该行为并不是必需的,你也可让生成器等待客户端明确// 调用重置方法后再去处理之前的结果。method getProduct():Car isproduct = this.carthis.reset()return product// 生成器与其他创建型模式的不同之处在于:它让你能创建不遵循相同接口的产品。
class CarManualBuilder implements Builder isprivate field manual:Manualconstructor CarManualBuilder() isthis.reset()method reset() isthis.manual = new Manual()method setSeats(...) is// 添加关于汽车座椅功能的文档。method setEngine(...) is// 添加关于引擎的介绍。method setTripComputer(...) is// 添加关于行车电脑的介绍。method setGPS(...) is// 添加关于 GPS 的介绍。method getProduct():Manual is// 返回使用手册并重置生成器。// 主管只负责按照特定顺序执行生成步骤。其在根据特定步骤或配置来生成产品时
// 会很有帮助。由于客户端可以直接控制生成器,所以严格意义上来说,主管类并
// 不是必需的。
class Director isprivate field builder:Builder// 主管可同由客户端代码传递给自身的任何生成器实例进行交互。客户端可通// 过这种方式改变最新组装完毕的产品的最终类型。method setBuilder(builder:Builder)this.builder = builder// 主管可使用同样的生成步骤创建多个产品变体。method constructSportsCar(builder: Builder) isbuilder.reset()builder.setSeats(2)builder.setEngine(new SportEngine())builder.setTripComputer(true)builder.setGPS(true)method constructSUV(builder: Builder) is// ...// 客户端代码会创建生成器对象并将其传递给主管,然后执行构造过程。最终结果
// 将需要从生成器对象中获取。
class Application ismethod makeCar() isdirector = new Director()CarBuilder builder = new CarBuilder()director.constructSportsCar(builder)Car car = builder.getProduct()CarManualBuilder builder = new CarManualBuilder()director.constructSportsCar(builder)// 最终产品通常需要从生成器对象中获取,因为主管不知晓具体生成器和// 产品的存在,也不会对其产生依赖。Manual manual = builder.getProduct()
车子:
SPORTS_CAR
手册:
Type of car: SPORTS_CAR
Count of seats: 2
Engine: volume - 3.0; mileage - 0.0
Transmission: SEMI_AUTOMATIC
Trip Computer: Functional
GPS Navigator: Functional

生成器模式优缺点

  • 你可以分步创建对象, 暂缓创建步骤或递归运行创建步骤。

  • 生成不同形式的产品时, 你可以复用相同的制造代码。

  • 单一职责原则。 你可以将复杂构造代码从产品的业务逻辑中分离出来。

  • 由于该模式需要新增多个类, 因此代码整体复杂程度会有所增加。

原型模式

原型模式是一种创建型设计模式, 使你能够复制已有对象, 而又无需使代码依赖它们所属的类。

由于工业原型并不是真正意义上的自我复制, 因此细胞有丝分裂 (还记得生物学知识吗?) 或许是更恰当的类比。 有丝分裂会产生一对完全相同的细胞。 原始细胞就是一个原型, 它在复制体的生成过程中起到了推动作用。

  • 你可以克隆对象, 而无需与它们所属的具体类相耦合。

  • 你可以克隆预生成原型, 避免反复运行初始化代码。

  • 你可以更方便地生成复杂对象。

  • 你可以用继承以外的方式来处理复杂对象的不同配置。

  • 克隆包含循环引用的复杂对象可能会非常麻烦。

所有形状类都遵循同一个提供克隆方法的接口。 在复制自身成员变量值到结果对象前, 子类可调用其父类的克隆方法。// 基础原型。
abstract class Shape isfield X: intfield Y: intfield color: string// 常规构造函数。constructor Shape() is// ...// 原型构造函数。使用已有对象的数值来初始化一个新对象。constructor Shape(source: Shape) isthis()this.X = source.Xthis.Y = source.Ythis.color = source.color// clone(克隆)操作会返回一个形状子类。abstract method clone():Shape// 具体原型。克隆方法会创建一个新对象并将其传递给构造函数。直到构造函数运
// 行完成前,它都拥有指向新克隆对象的引用。因此,任何人都无法访问未完全生
// 成的克隆对象。这可以保持克隆结果的一致。
class Rectangle extends Shape isfield width: intfield height: intconstructor Rectangle(source: Rectangle) is// 需要调用父构造函数来复制父类中定义的私有成员变量。super(source)this.width = source.widththis.height = source.heightmethod clone():Shape isreturn new Rectangle(this)class Circle extends Shape isfield radius: intconstructor Circle(source: Circle) issuper(source)this.radius = source.radiusmethod clone():Shape isreturn new Circle(this)// 客户端代码中的某个位置。
class Application isfield shapes: array of Shapeconstructor Application() isCircle circle = new Circle()circle.X = 10circle.Y = 10circle.radius = 20shapes.add(circle)Circle anotherCircle = circle.clone()shapes.add(anotherCircle)// 变量 `anotherCircle(另一个圆)`与 `circle(圆)`对象的内// 容完全一样。Rectangle rectangle = new Rectangle()rectangle.width = 10rectangle.height = 20shapes.add(rectangle)method businessLogic() is// 原型是很强大的东西,因为它能在不知晓对象类型的情况下生成一个与// 其完全相同的复制品。Array shapesCopy = new Array of Shapes.// 例如,我们不知晓形状数组中元素的具体类型,只知道它们都是形状。// 但在多态机制的帮助下,当我们在某个形状上调用 `clone(克隆)`// 方法时,程序会检查其所属的类并调用其中所定义的克隆方法。这样,// 我们将获得一个正确的复制品,而不是一组简单的形状对象。foreach (s in shapes) doshapesCopy.add(s.clone())// `shapesCopy(形状副本)`数组中包含 `shape(形状)`数组所有// 子元素的复制品。

构造方法加一个return new

单例模式

单例模式

单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。

伪代码

在本例中, 数据库连接类即是一个单例

该类不提供公有构造函数, 因此获取该对象的唯一方式是调用 获取实例方法。 该方法将缓存首次生成的对象, 并为所有后续调用返回该对象。

// 数据库类会对`getInstance(获取实例)`方法进行定义以让客户端在程序各处
// 都能访问相同的数据库连接实例。
class Database is// 保存单例实例的成员变量必须被声明为静态类型。private static field instance: Database// 单例的构造函数必须永远是私有类型,以防止使用`new`运算符直接调用构// 造方法。private constructor Database() is// 部分初始化代码(例如到数据库服务器的实际连接)。// ...// 用于控制对单例实例的访问权限的静态方法。public static method getInstance() isif (Database.instance == null) thenacquireThreadLock() and then// 确保在该线程等待解锁时,其他线程没有初始化该实例。if (Database.instance == null) thenDatabase.instance = new Database()return Database.instance// 最后,任何单例都必须定义一些可在其实例上执行的业务逻辑。public method query(sql) is// 比如应用的所有数据库查询请求都需要通过该方法进行。因此,你可以// 在这里添加限流或缓冲逻辑。// ...class Application ismethod main() isDatabase foo = Database.getInstance()foo.query("SELECT ...")// ...Database bar = Database.getInstance()bar.query("SELECT ...")// 变量 `bar` 和 `foo` 中将包含同一个对象。

单例模式与全局变量不同, 它保证类只存在一个实例。 除了单例类自己以外, 无法通过任何方式替换缓存的实例。

请注意, 你可以随时调整限制并设定生成单例实例的数量, 只需修改 获取实例方法, 即 getInstance 中的代码即可实现。

java.lang.Runtime#getRuntime()
java.awt.Desktop#getDesktop()
java.lang.System#getSecurityManager()

结构型模式

定义:

结构型模式介绍如何将对象和类组装成较大的结构, 并同时保持结构的灵活和高效。

image-20211026160457180

适配器模式

也称: 封装器模式、Wrapper、Adapter

定义

适配器模式是一种结构型设计模式, 它能使接口不兼容的对象能够相互合作。

image-20211026160605793

适配器假扮成一个圆钉 (Round­Peg), 其半径等于方钉 (Square­Peg) 横截面对角线的一半 (即能够容纳方钉的最小外接圆的半径)。

// 假设你有两个接口相互兼容的类:圆孔(Round­Hole)和圆钉(Round­Peg)。
class RoundHole isconstructor RoundHole(radius) { ... }method getRadius() is// 返回孔的半径。method fits(peg: RoundPeg) isreturn this.getRadius() >= peg.getRadius()class RoundPeg isconstructor RoundPeg(radius) { ... }method getRadius() is// 返回钉子的半径。// 但还有一个不兼容的类:方钉(Square­Peg)。
class SquarePeg isconstructor SquarePeg(width) { ... }method getWidth() is// 返回方钉的宽度。// 适配器类让你能够将方钉放入圆孔中。它会对 RoundPeg 类进行扩展,以接收适
// 配器对象作为圆钉。
class SquarePegAdapter extends RoundPeg is// 在实际情况中,适配器中会包含一个 SquarePeg 类的实例。private field peg: SquarePegconstructor SquarePegAdapter(peg: SquarePeg) isthis.peg = pegmethod getRadius() is// 适配器会假扮为一个圆钉,// 其半径刚好能与适配器实际封装的方钉搭配起来。return peg.getWidth() * Math.sqrt(2) / 2// 客户端代码中的某个位置。
hole = new RoundHole(5)
rpeg = new RoundPeg(5)
hole.fits(rpeg) // truesmall_sqpeg = new SquarePeg(5)
large_sqpeg = new SquarePeg(10)
hole.fits(small_sqpeg) // 此处无法编译(类型不一致)。small_sqpeg_adapter = new SquarePegAdapter(small_sqpeg)
large_sqpeg_adapter = new SquarePegAdapter(large_sqpeg)
hole.fits(small_sqpeg_adapter) // true
hole.fits(large_sqpeg_adapter) // false

实现方式

  1. 确保至少有两个类的接口不兼容:
    • 一个无法修改 (通常是第三方、 遗留系统或者存在众多已有依赖的类) 的功能性服务类。
    • 一个或多个将受益于使用服务类的客户端类。
  2. 声明客户端接口, 描述客户端如何与服务交互。
  3. 创建遵循客户端接口的适配器类。 所有方法暂时都为空。
  4. 在适配器类中添加一个成员变量用于保存对于服务对象的引用。 通常情况下会通过构造函数对该成员变量进行初始化, 但有时在调用其方法时将该变量传递给适配器会更方便。
  5. 依次实现适配器类客户端接口的所有方法。 适配器会将实际工作委派给服务对象, 自身只负责接口或数据格式的转换。
  6. 客户端必须通过客户端接口使用适配器。 这样一来, 你就可以在不影响客户端代码的情况下修改或扩展适配器。

适配器模式优缺点

  • _单一职责原则_你可以将接口或数据转换代码从程序主要业务逻辑中分离。

  • 开闭原则。 只要客户端代码通过客户端接口与适配器进行交互, 你就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器。

  • 代码整体复杂度增加, 因为你需要新增一系列接口和类。 有时直接更改服务类使其与其他代码兼容会更简单。

与其他模式的关系

  • 桥接模式通常会于开发前期进行设计, 使你能够将程序的各个部分独立开来以便开发。 另一方面, 适配器模式通常在已有程序中使用, 让相互不兼容的类能很好地合作。
  • 适配器可以对已有对象的接口进行修改, 装饰模式则能在不改变对象接口的前提下强化对象功能。 此外, 装饰还支持递归组合, 适配器则无法实现。
  • 适配器能为被封装对象提供不同的接口, 代理模式能为对象提供相同的接口, 装饰则能为对象提供加强的接口。
  • 外观模式为现有对象定义了一个新接口, 适配器则会试图运用已有的接口。 适配器通常只封装一个对象, 外观通常会作用于整个对象子系统上。
  • 桥接、 状态模式和策略模式 (在某种程度上包括适配器) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。

Java 核心程序库中有一些标准的适配器:

  • java.util.Arrays#asList()
  • java.util.Collections#list()
  • java.util.Collections#enumeration()
  • java.io.InputStreamReader(InputStream) (返回 Reader对象)
  • java.io.OutputStreamWriter(OutputStream) (返回 Writer对象)
  • javax.xml.bind.annotation.adapters.XmlAdapter#marshal()#unmarshal()

识别方法: 适配器可以通过以不同抽象或接口类型实例为参数的构造函数来识别。 当适配器的任何方法被调用时, 它会将参数转换为合适的格式, 然后将调用定向到其封装对象中的一个或多个方法。

桥接模式

桥接模式是一种结构型设计模式, 可将一个大类或一系列紧密相关的类拆分为抽象和实现两个独立的层次结构, 从而能在开发时分别使用。

image-20211026161559124

实例:

  • 抽象部分: 程序的 GUI 层。
  • 实现部分: 操作系统的 API。

image-20211026161713694

示例演示了桥接模式如何拆分程序中同时管理设备及其遥控器的庞杂代码。 设备Device类作为实现部分, 而 遥控器Remote类则作为抽象部分。

桥接模式适合应用场景

如果你想要拆分或重组一个具有多重功能的庞杂类 (例如能与多个数据库服务器进行交互的类), 可以使用桥接模式。

类的代码行数越多, 弄清其运作方式就越困难, 对其进行修改所花费的时间就越长。 一个功能上的变化可能需要在整个类范围内进行修改, 而且常常会产生错误, 甚至还会有一些严重的副作用。

桥接模式可以将庞杂类拆分为几个类层次结构。 此后, 你可以修改任意一个类层次结构而不会影响到其他类层次结构。 这种方法可以简化代码的维护工作, 并将修改已有代码的风险降到最低。

如果你希望在几个独立维度上扩展一个类, 可使用该模式。

桥接建议将每个维度抽取为独立的类层次。 初始类将相关工作委派给属于对应类层次的对象, 无需自己完成所有工作。

如果你需要在运行时切换不同实现方法, 可使用桥接模式。

当然并不是说一定要实现这一点, 桥接模式可替换抽象部分中的实现对象, 具体操作就和给成员变量赋新值一样简单。

顺便提一句, 最后一点是很多人混淆桥接模式和策略模式的主要原因。 记住, 设计模式并不仅是一种对类进行组织的方式, 它还能用于沟通意图和解决问题。

实现方式

  1. 明确类中独立的维度。 独立的概念可能是: 抽象/平台, 域/基础设施, 前端/后端或接口/实现。
  2. 了解客户端的业务需求, 并在抽象基类中定义它们。
  3. 确定在所有平台上都可执行的业务。 并在通用实现接口中声明抽象部分所需的业务。
  4. 为你域内的所有平台创建实现类, 但需确保它们遵循实现部分的接口。
  5. 在抽象类中添加指向实现类型的引用成员变量。 抽象部分会将大部分工作委派给该成员变量所指向的实现对象。
  6. 如果你的高层逻辑有多个变体, 则可通过扩展抽象基类为每个变体创建一个精确抽象。
  7. 客户端代码必须将实现对象传递给抽象部分的构造函数才能使其能够相互关联。 此后, 客户端只需与抽象对象进行交互, 无需和实现对象打交道。

桥接模式优缺点

  • 你可以创建与平台无关的类和程序。

  • 客户端代码仅与高层抽象部分进行互动, 不会接触到平台的详细信息。

  • 开闭原则。 你可以新增抽象部分和实现部分, 且它们之间不会相互影响。

  • 单一职责原则。 抽象部分专注于处理高层逻辑, 实现部分处理平台细节。

  • 对高内聚的类使用该模式可能会让代码更加复杂。

与其他模式的关系

  • 桥接模式通常会于开发前期进行设计, 使你能够将程序的各个部分独立开来以便开发。 另一方面, 适配器模式通常在已有程序中使用, 让相互不兼容的类能很好地合作。
  • 桥接、 状态模式和策略模式 (在某种程度上包括适配器) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。
  • 你可以将抽象工厂模式和桥接搭配使用。 如果由桥接定义的抽象只能与特定实现合作, 这一模式搭配就非常有用。 在这种情况下, 抽象工厂可以对这些关系进行封装, 并且对客户端代码隐藏其复杂性。
  • 你可以结合使用生成器模式和桥接模式: 主管类负责抽象工作, 各种不同的生成器负责实现工作。

复杂的东西 组合 桥接

简单的东西 组合 适配

在 Java 中使用模式

复杂度:

流行度:

使用示例: 桥接模式在处理跨平台应用、 支持多种类型的数据库服务器或与多个特定种类 (例如云平台和社交网络等) 的 API 供应商协作时会特别有用。

识别方法: 桥接可以通过一些控制实体及其所依赖的多个不同平台之间的明确区别来进行识别。

组合模式

亦称: 对象树、Object Tree、Composite

image-20211026164357299

组合模式是一种结构型设计模式, 你可以使用它将对象组合成树状结构, 并且能像使用独立对象一样使用它们。

image-20211026164551286

组合模式适合应用场景

如果你需要实现树状对象结构, 可以使用组合模式。

组合模式为你提供了两种共享公共接口的基本元素类型: 简单叶节点和复杂容器。 容器中可以包含叶节点和其他容器。 这使得你可以构建树状嵌套递归对象结构。

如果你希望客户端代码以相同方式处理简单和复杂元素, 可以使用该模式。

组合模式中定义的所有元素共用同一个接口。 在这一接口的帮助下, 客户端不必在意其所使用的对象的具体类。

实现方式

  1. 确保应用的核心模型能够以树状结构表示。 尝试将其分解为简单元素和容器。 记住, 容器必须能够同时包含简单元素和其他容器。

  2. 声明组件接口及其一系列方法, 这些方法对简单和复杂元素都有意义。

  3. 创建一个叶节点类表示简单元素。 程序中可以有多个不同的叶节点类。

  4. 创建一个容器类表示复杂元素。 在该类中, 创建一个数组成员变量来存储对于其子元素的引用。 该数组必须能够同时保存叶节点和容器, 因此请确保将其声明为组合接口类型。

    实现组件接口方法时, 记住容器应该将大部分工作交给其子元素来完成。

  5. 最后, 在容器中定义添加和删除子元素的方法。

    记住, 这些操作可在组件接口中声明。 这将会违反_接口隔离原则_, 因为叶节点类中的这些方法为空。 但是, 这可以让客户端无差别地访问所有元素, 即使是组成树状结构的元素。

组合模式优缺点

  • 你可以利用多态和递归机制更方便地使用复杂树结构。

  • 开闭原则。 无需更改现有代码, 你就可以在应用中添加新元素, 使其成为对象树的一部分。

  • 对于功能差异较大的类, 提供公共接口或许会有困难。 在特定情况下, 你需要过度一般化组件接口, 使其变得令人难以理解。

与其他模式的关系

  • 桥接模式、 状态模式和策略模式 (在某种程度上包括适配器模式) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。

  • 你可以在创建复杂组合树时使用生成器模式, 因为这可使其构造步骤以递归的方式运行。

  • 责任链模式通常和组合模式结合使用。 在这种情况下, 叶组件接收到请求后, 可以将请求沿包含全体父组件的链一直传递至对象树的底部。

  • 你可以使用迭代器模式来遍历组合树。

  • 你可以使用访问者模式对整个组合树执行操作。

  • 你可以使用享元模式实现组合树的共享叶节点以节省内存。

  • 组合和装饰模式的结构图很相似, 因为两者都依赖递归组合来组织无限数量的对象。

    装饰类似于组合, 但其只有一个子组件。 此外还有一个明显不同: 装饰为被封装对象添加了额外的职责, 组合仅对其子节点的结果进行了 “求和”。

    但是, 模式也可以相互合作: 你可以使用装饰来扩展组合树中特定对象的行为。

  • 大量使用组合和装饰的设计通常可从对于原型模式的使用中获益。 你可以通过该模式来复制复杂结构, 而非从零开始重新构造。

组合有组合嵌套 。用组合

在 Java 中使用模式

复杂度:

流行度:

使用实例: 组合模式在 Java 代码中很常见,常用于表示与图形打交道的用户界面组件或代码的层次结构。

下面是一些来自 Java 标准程序库中的组合示例:

  • java.awt.Container#add(Component) (几乎广泛存在于 Swing 组件中)
  • javax.faces.component.UIComponent#getChildren() (几乎广泛存在于 JSF UI 组件中)

识别方法: 组合可以通过将同一抽象或接口类型的实例放入树状结构的行为方法来轻松识别。

装饰模式

亦称: 装饰者模式、装饰器模式、Wrapper、Decorator

装饰模式是一种结构型设计模式, 允许你通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。

image-20211026164835095

image-20211026165006598

装饰 不改变原有事物的形态。

与其他模式的关系

  • 适配器模式可以对已有对象的接口进行修改, 装饰模式则能在不改变对象接口的前提下强化对象功能。 此外, 装饰还支持递归组合, 适配器则无法实现。

  • 适配器能为被封装对象提供不同的接口, 代理模式能为对象提供相同的接口, 装饰则能为对象提供加强的接口。

  • 责任链模式和装饰模式的类结构非常相似。 两者都依赖递归组合将需要执行的操作传递给一系列对象。 但是, 两者有几点重要的不同之处。

    责任链的管理者可以相互独立地执行一切操作, 还可以随时停止传递请求。 另一方面, 各种装饰可以在遵循基本接口的情况下扩展对象的行为。 此外, 装饰无法中断请求的传递。

  • 组合模式和装饰的结构图很相似, 因为两者都依赖递归组合来组织无限数量的对象。

    装饰类似于组合, 但其只有一个子组件。 此外还有一个明显不同: 装饰为被封装对象添加了额外的职责, 组合仅对其子节点的结果进行了 “求和”。

    但是, 模式也可以相互合作: 你可以使用装饰来扩展组合树中特定对象的行为。

  • 大量使用组合和装饰的设计通常可从对于原型模式的使用中获益。 你可以通过该模式来复制复杂结构, 而非从零开始重新构造。

  • 装饰可让你更改对象的外表, 策略模式则让你能够改变其本质。

  • 装饰和代理有着相似的结构, 但是其意图却非常不同。 这两个模式的构建都基于组合原则, 也就是说一个对象应该将部分工作委派给另一个对象。 两者之间的不同之处在于代理通常自行管理其服务对象的生命周期, 而装饰的生成则总是由客户端进行控制。

在 Java 中使用模式

复杂度:

流行度:

使用示例: 装饰在 Java 代码中可谓是标准配置, 尤其是在与流式加载相关的代码中。

Java 核心程序库中有一些关于装饰的示例:

  • java.io.InputStreamOutput­StreamReaderWriter 的所有代码都有以自身类型的对象作为参数的构造函数。
  • java.util.Collectionschecked­XXX()synchronized­XXX()unmodifiable­XXX() 方法。
  • javax.servlet.http.HttpServletRequestWrapperHttp­Servlet­Response­Wrapper

识别方法: 装饰可通过以当前类或对象为参数的创建方法或构造函数来识别。

外观模式

亦称: 门面模式、Facade

外观模式是一种结构型设计模式, 能为程序库、 框架或其他复杂类提供一个简单的接口。

image-20211026165250127

相关文章:

  • Java架构师之路四、分布式系统:分布式架构、分布式数据存储、分布式事务、分布式锁、分布式缓存、分布式消息中间件、分布式存储等。
  • WooCommerce商品采集与发布插件
  • 缩小ppt文件大小的办法
  • C#_各式各样的参数(引用参数、输出参数、数组参数、具名参数、可选参数)
  • (提供数据集下载)基于大语言模型LangChain与ChatGLM3-6B本地知识库调优:数据集优化、参数调整、Prompt提示词优化实战
  • 航空领域中气象常识笔记
  • 什么是跨模态
  • HTML和CSS是前端开发中最基础的两个技术[入门级]
  • 游戏平台如何定制开发?
  • Spring之AOP源码解析(上)
  • 鸿蒙原生应用元服务实战-发布时多设备选择注意事项
  • 9、内网安全-横向移动Exchange服务有账户CVE漏洞无账户口令爆破
  • MacBook的nginx出现13: Permission denied 的问题分析和解决办法
  • 蓝桥杯备赛系列——倒计时50天!
  • Neo4j导入数据之JAVA JDBC
  • CSS实用技巧
  • Docker 笔记(1):介绍、镜像、容器及其基本操作
  • Gradle 5.0 正式版发布
  • gulp 教程
  • javascript从右向左截取指定位数字符的3种方法
  • KMP算法及优化
  • LeetCode18.四数之和 JavaScript
  • 回顾 Swift 多平台移植进度 #2
  • 如何实现 font-size 的响应式
  • 如何学习JavaEE,项目又该如何做?
  • 一个6年java程序员的工作感悟,写给还在迷茫的你
  • 译米田引理
  • 用Visual Studio开发以太坊智能合约
  • 原生JS动态加载JS、CSS文件及代码脚本
  • ​学习一下,什么是预包装食品?​
  • ​一些不规范的GTID使用场景
  • (12)目标检测_SSD基于pytorch搭建代码
  • (C++17) std算法之执行策略 execution
  • (Python第六天)文件处理
  • (二开)Flink 修改源码拓展 SQL 语法
  • (十二)springboot实战——SSE服务推送事件案例实现
  • (十五)使用Nexus创建Maven私服
  • (十一)c52学习之旅-动态数码管
  • (十一)JAVA springboot ssm b2b2c多用户商城系统源码:服务网关Zuul高级篇
  • (转)Android中使用ormlite实现持久化(一)--HelloOrmLite
  • (转)微软牛津计划介绍——屌爆了的自然数据处理解决方案(人脸/语音识别,计算机视觉与语言理解)...
  • .NET Core IdentityServer4实战-开篇介绍与规划
  • .net 程序发生了一个不可捕获的异常
  • .Net 代码性能 - (1)
  • .NET分布式缓存Memcached从入门到实战
  • .NET中的十进制浮点类型,徐汇区网站设计
  • @SuppressWarnings注解
  • [ IO.File ] FileSystemWatcher
  • [ 环境搭建篇 ] 安装 java 环境并配置环境变量(附 JDK1.8 安装包)
  • [2013][note]通过石墨烯调谐用于开关、传感的动态可重构Fano超——
  • [android] 切换界面的通用处理
  • [Android]如何调试Native memory crash issue
  • [C#][opencvsharp]opencvsharp sift和surf特征点匹配
  • [CSDN首发]鱿鱼游戏的具体玩法详细介绍
  • [C语言]——函数递归