关于设计模式的问答主要是三个方向:
- 你的项目中用到了哪些设计模式,如何使用
- 知道常用设计模式的优缺点
- 能画出常用设计模式的UML图
前言
1、设计模式分类(5+7+11=23)
2、UML类图中的五种关系:
1,依赖关系(Dependency)
单向,表示一个类依赖于另一个类的定义,其中一个类的变化将影响另外一个类,是一种“use a”关系
如果A依赖于B,则B表现为A的局部变量(不是成员变量),方法参数,静态方法调用等
[java] view plain copy
- public class Person {
- public void doSomething(){
- Card card = new Card();//局部变量
- ….
- }
- }
[java] view plain copy
- public class Person {
- public void doSomething(Card card){//方法参数
- ….
- }
- }
[java] view plain copy
- public class Person {
- public void doSomething(){
- int id = Card.getId();//静态方法调用
- …
- }
- }
2,关联关系(Association)
单向或双向(通常我们需要避免使用双向关联关系),是一种”has a”关系,如果A单向关联B,则可以说A has a B,通常表现为全局变量(成员变量)
[java] view plain copy
- public class Person {
- public Phone phone;
- public void setPhone(Phone phone){
- this.phone = phone;
- }
- public Phone getPhone(){
- return phone;
- }
- }
3,聚合关系(Aggregation)
单向,关联关系的一种,与关联关系之间的区别是语义上的,关联的两个对象通常是平等的,聚合则一般不平等,有一种整体和局部的感觉,实现上区别不大
Team由Person组成,其生命周期不同,整体不存在了,部分依然存在,当前Team解散了,人还在,还可以加入别的组
[java] view plain copy
- public class Team {
- public Person person;
- public Team(Person person){
- this.person = person;
- }
- }
4,组合关系(Composition)
单向,是一种强依赖的特殊聚合关系
Head,Body,Arm和Leg组合成People,其生命周期相同,如果整体不存在了,部分也将消亡
[java] view plain copy
- public class Person {
- public Head head;
- public Body body;
- public Arm arm;
- public Leg leg;
- public Person(){
- head = new Head();
- body = new Body();
- arm = new Arm();
- leg = new Leg();
- }
- }
5,继承关系(Inheritance)
类实现接口,类继承抽象类,类继承父类都属于这种关系
可以分得更细:
实现(Realization):类实现接口属于这种关系。
泛化(Generalization):即”is a”关系,类继承抽象类,类继承父类都属于这种关系
一、单例模式(创建型)
1.概念
Singleton是一种创建型模式,指某个类采用Singleton模式,则在这个类被创建后,只可能产生一个实例供外部访问,并且提供一个全局的访问点。单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。
2. 核心知识点如下:
(1) 将采用单例设计模式的类的构造方法私有化(采用private修饰)。
(2) 在其内部产生该类的实例化对象,并将其封装成private static类型。
(3) 定义一个静态方法返回该类的实例。
3.优缺点
优点:
(1)提供了对唯一实例的受控访问
(2)由于在系统内存中只存在一个对象,因此可以节约系统资源,当需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。
(3)避免对共享资源的多重占用。
缺点:
(1)由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
(2)单例类的职责过重,在一定程度上违背了“单一职责原则”。
(3)不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
4.使用场景:
单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。如:
(1).需要频繁实例化然后销毁的对象。
(2).创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
(3).有状态的工具类对象。
(4).频繁访问数据库或文件的对象。
5.单例模式的实现
(1)懒汉,线程不安全
(2)懒汉,线程安全
这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下不需要同步。
(3)饿汉方式
这种方式基于classloder机制(静态变量在准备阶段附初始值)避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。
(4)饿汉,变种
跟第三种方式差不多,都是在类初始化就实例化instance。
(5)静态内部类
利用了classloder的机制来保证初始化instance时只有一个线程,它跟第三种和第四种方式不同的是(很细微的差别):第三种和第四种方式是只要Singleton类被装载了,那么instance就会被实例化(没有达到 lazy loading 效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为Instance类没有被主动使用,只有显示调用getInstance方法时,才会显示装载Instance类,从而实例化instance。
(6)枚举(饿汉模式)
直接使用 Singleton.INSTANCE 得到一个实例,然后可以访问内部的方法。
enum有且仅有private的构造器,防止外部的额外构造,这恰好和单例模式吻合,也为保证单例性做了一个铺垫。枚举会被编译成如下形式:
枚举量的实现其实是public static final T 类型的未初始化变量,之后,会在静态代码中对枚举量进行初始化。使用枚举其实和使用静态类内部加载方法原理类似。
(7)双重校验锁
publicclass SingletonTest implements Serializable{ // 定义一个私有构造方法 private SingletonTest() {
} //定义一个静态私有变量(不初始化,不使用final关键字,使用volatile保证了多线程访问时instance变量的可见性,避免了instance初始化时其他变量属性还没赋值完时,被另外线程调用) privatestatic volatile SingletonTest instance; //定义一个共有的静态方法,返回该类型实例 publicstatic SingletonTest getIstance() {
// 对象实例化时与否判断(不使用同步代码块,instance不等于null时,直接返回对象,提高运行效率) if (instance == null) {
//同步代码块(对象未初始化时,使用同步代码块,保证多线程访问时对象在第一次创建后,不再重复被创建) synchronized (SingletonTest.class) {
//未初始化,则初始instance变量 if (instance == null) { instance = new SingletonTest(); } } } return instance; }
private Object readResolve() {
return instance; //防止反序列化破坏单例模式
}
}
序列化和反序列化会破坏单例模式,反序列化时会通过反射调用被反序列化的类的readResolve方法,如果没有实现这个方法,则会调用无参数的构造方法创建一个新的对象。
二、责任链模式(行为型)
1.概念
创建多个对象,使这些对象形成一条链,并沿着这条链传递请求,直到链上的某一个对象决定处理此请求。
责任链模式涉及到的角色如下:
● 抽象处理者(Handler)角色:定义出一个处理请求的接口(Handler)。如果需要,接口可以定义 出一个方法以设定和返回对下家的引用。这个角色通常由一个Java抽象类或者Java接口实现。上图中Handler类的聚合关系给出了具体子类对下家的引用,抽象方法handleRequest()规范了子类处理请求的操作。
● 具体处理者(ConcreteHandler)角色:具体处理者接到请求后,可以选择将请求处理掉,或者将请求传给下家(具体处理者持有对下家的引用)。
2.特点
1)接收请求的对象连接成一条链,对象之间存在层级关系。
2)这些对象可处理请求,也可传递请求,直到有对象处理该请求。
3.优缺点和总结
优点
1)降低耦合度:客户端不需要知道请求由哪个处理者处理,而处理者也不需要知道处理者之间的传递关系,由系统灵活的组织和分配。
2)良好的扩展性:增加处理者的实现很简单,只需重写处理请求业务逻辑的方法。
缺点
1)请求会从链头发出,直到有处理者响应,在责任链比较长的时候会影响系统性能。
2)请求递归,调试排错比较麻烦。
总结:
责任链模式在实际项目中可以用到的地方还是比较多的,比如会员等级系统,会员等级之间构成一条链,用户发起一个请求,系统只要把请求分发到责任链模式的入口,直到传递到与用户会员匹配的等级,这样各个会员等级的业务逻辑就会变成很清晰。
三、工厂模式(创建型)
工厂模式(Factory Pattern)是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
工厂模式一般可以分为三类:
1)简单工厂模式(Simple Factory):不利于产生系列产品;
2)工厂方法模式(Factory Method):又称为多形性工厂;
3)抽象工厂模式(Abstract Factory):又称为工具箱,产生产品族,但不利于产生新的产品;
这三种模式从上到下逐步抽象,并且更具一般性。
3.1 简单工厂
简单工厂模式又称静态工厂模式。从命名上就可以看出这个模式一定很简单。它存在的目的很简单:定义一个用于创建对象的接口。
在简单工厂模式中,一个工厂类处于对产品类实例化调用的中心位置上,它决定那一个产品类应当被实例化, 如同一个交通警察站在来往的车辆流中,决定放行那一个方向的车辆向那一个方向流动一样。
先来看看它的组成:
1) 抽象产品角色:它一般是具体产品继承的父类或者实现的接口。在java中由接口或者抽象类来实现。
2) 具体产品角色:工厂类所创建的对象就是此角色的实例。在java中由一个具体类实现。<br /> 3) 工厂类角色:这是本模式的核心,含有一定的商业逻辑和判断逻辑。在java中它往往由一个具体类实现。<br />**3.2 工厂方法**<br /><br />工厂方法模式是简单工厂模式的进一步抽象化和推广,工厂方法模式里不再只由一个工厂类决定那一个产品类应当被实例化,这个决定被交给抽象工厂的子类去做。<br /> 来看下它的组成:<br /> 1)抽象工厂角色: 这是工厂方法模式的核心,它与应用程序无关。是具体工厂角色必须实现的接口或者必须继承的父类。在java中它由抽象类或者接口来实现。<br /> 2)具体工厂角色:它含有和具体业务逻辑有关的代码。由应用程序调用以创建对应的具体产品的对象。<br /> 3)抽象产品角色:它是具体产品继承的父类或者是实现的接口。在java中一般有抽象类或者接口来实现。<br /> 4)具体产品角色:具体工厂角色所创建的对象就是此角色的实例。在java中由具体的类来实现。<br /> 工厂方法模式使用继承自抽象工厂角色的多个子类来代替简单工厂模式中的“上帝类”。正如上面所说,这样便分担了对象承受的压力;而且这样使得结构变得灵活 起来——当有新的产品(即暴发户的汽车)产生时,只要按照抽象产品角色、抽象工厂角色提供的合同来生成,那么就可以被客户使用,而不必去修改任何已有的代码。可以看出工厂角色的结构也是符合开闭原则的!<br />**3.3 抽象工厂**<br />在抽象工厂模式中,抽象产品 (AbstractProduct) 可能是一个或多个,从而构成一个或多个产品族(Product Family)。 在只有一个产品族的情况下,抽象工厂模式实际上退化到工厂方法模式。<br />**3.4 总结**<br />(1)简单工厂模式是由一个具体的类去创建其他类的实例,父类是相同的。<br />(2)工厂方法模式是有一个抽象的父类定义公共接口,子类负责生成具体的对象,这样做的目的是将类的实例化操作延迟到子类中完成。<br />(3)抽象工厂模式提供一个创建一系列相关或相互依赖对象的接口,而无须指定他们具体的类。它针对的是有多个产品的等级结构。而工厂方法模式针对的是一个产品的等级结构。<br />四、装饰器模式(结构型)<br />

1. 概念
装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。
2. 优缺点
2.1 优点:
装饰类和被装饰类可以独立发展,不会相互耦合。
装饰模式可以动态扩展一个实现类的功能,动态的将责任(装饰)附加到对象(被装饰者)身上。
2.2 缺点:
多层装饰比较复杂。
3. 应用场景
- 扩展一个类的功能。
- 动态增加功能,动态撤销。
- Java 中 IO使用了装饰者模式和适配器模式。
4.代码示例
public interface Drink {float cost();String getDescription();}public class Coffee implements Drink {private String description = "coffee";@Overridepublic float cost() {return 10;}@Overridepublic String getDescription() {return description;}}public class Decorator implements Drink{protected Drink drink;public Decorator(Drink drink) {this.drink = drink;}@Overridepublic float cost() {return drink.cost();}@Overridepublic String getDescription() {return drink.getDescription();}}public class Milk extends Decorator {public Milk(Drink drink) {super(drink);}@Overridepublic float cost() {return super.cost() + 2;}@Overridepublic String getDescription() {return super.getDescription() + " milk";}}public class Sugar extends Decorator {public Sugar(Drink drink) {super(drink);}@Overridepublic float cost() {return super.cost() + 1;}@Overridepublic String getDescription() {return super.getDescription() + " sugar";}}public class DecoratorTest {public static void main(String[] args) {Drink drink = new Coffee();System.out.println(drink.getDescription() + ":" + drink.cost());//加牛奶进行装饰drink = new Milk(drink);System.out.println(drink.getDescription() + ":" + drink.cost());//加糖装饰drink = new Sugar(drink);System.out.println(drink.getDescription() + ":" + drink.cost());}}
五、代理模式(结构型)
1.概念
为其他对象提供一种代理,通过代理控制对这个对象的访问。而对一个对象进行访问控制的一个原因是为了只有在我们确实需要这个对象时才对它进行创建和初始化。它是给某一个对象提供一个替代者(占位者),使之在client对象和subject对象之间编码更有效率。代理可以提供延迟实例化(lazy instantiation),控制访问,等等。
2.优缺点
2.1 优点:
1、代理模式能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。
2、职责清晰。
3、高扩展性。
2.2 缺点:
1、由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
2、实现代理模式需要额外的工作,有些代理模式的实现非常复杂。
3.应用场景
在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。
1) 远程代理(Remote Proxy)为一个位于不同的地址空间的对象提供一个本地的代理对象。这个不同的地址空间可以是在同一台主机中,也可是在另一台主机中,远程代理又叫做大使(Ambassador)
2) 虚拟代理(Virtual Proxy)根据需要创建开销很大的对象。如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。
3) 保护代理(Protection Proxy)控制对原始对象的访问。保护代理用于对象应该有不同的访问权限的时候。
4) 智能指引(Smart Reference)取代了简单的指针,它在访问对象时执行一些附加操作。
5) Copy-on-Write代理:它是虚拟代理的一种,把复制(克隆)操作延迟到只有在客户端真正需要时才执行。一般来说,对象的深克隆是一个开销较大的操作,Copy-on-Write代理可以让这个操作延迟,只有对象被用到的时候才被克隆。
4.总结
和装饰器模式的区别:装饰器模式为了增强功能,而代理模式是为了加以控制。
六、策略模式
优点:
1、算法可以自由切换(具体策略是等价的)。
2、避免使用多重条件判断。
3、扩展性良好,只需要增加策略类就可以。
缺点:
1、策略类会增多,增加了对象的数目。
2、所有策略类都需要对外暴露,只有让用户知道了所有策略的功能,才可以根据需要需要选择对应的策略。
七、模板方法模式
模版方法:在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模版方法使得子类可以在不改变算法结构的情况下,重新定义算法中某些步骤。其目的就是对流程进行封装,封装不可变的,扩展可变的。
优点:
1、封装不变部分,扩展可变部分。
2、提取公共代码,便于维护。
3、行为由父类控制,子类实现。
缺点:每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。
模版方法和策略模式的对比
- 模版方法:控制算法内部。
- 策略模式:不同算法的管理。
八、过滤器模式(JavaWeb中Filter使用的设计模式)
九、观察者模式
当一个对象被修改时,则会自动通知它的依赖对象。观察者模式属于行为型模式。
优点:
1、观察者和被观察者是抽象耦合的。
2、建立一套触发机制。
缺点:
1、如果一个被观察者对象有很多的观察者的话,将所有的观察者都通知到会花费很多时间。
2、观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
应用场景:
一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用。
十、适配器模式
适配器就是一种适配中间件,它存在于不匹配的二者之间,用于连接二者,将不匹配变得匹配,简单点理解就是平常所见的转接头,转换器之类的存在。
优点:
1、可以让任何两个没有关联的类一起运行。
2、提高了类的复用。
3、增加了类的透明度。
4、灵活性好。
缺点:
1、过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。 (适配器不是在详细设计时添加的,而是解决正在服役的项目的问题)
2、由于 JAVA 至多继承一个类,所以至多只能适配一个适配者类,而且目标类(被适配的对象)必须是抽象类。
使用场景:有动机地修改一个正常运行的系统的接口,这时应该考虑使用适配器模式。




设计模式的常见应用
1、Java 集合框架
装饰器模式
通过阅读源码发现 TreeSet 和 HashSet 的实现其实是对 TreeMap 和 HashMap 的包装,set 在 map 的基础上删减了一些功能并增加了一些特性。
迭代器模式:
Java 集合框架中的各种迭代器。
策略模式:
Sort 中的 Comparator这个接口。多线程的实现。
2、Java IO
装饰器模式
有一些流处理器可以对另一些流处理器起到装饰作用,形成新的、具有改善了的功能的流处理器。
BufferedOutputStream 对一个输出流进行装饰,使得流的写出操作使用缓冲机制。在使用缓冲机制后,不会对每一次的流的写入操作都产生一 个物理的写动作,从而提高的程序的效率。
适配器模式
有一些流处理器是对其他类型的流处理器的适配。
FileOutputStream 继承 OutputStream,同时持有一个对 FileDescriptor 对象的引用,这是一个将 FileDescriptor 适配成 OutputStream 接口的对象形式的适配器模式。
3、Tomcat
Tomcat 对组件生命周期的控制和监听可以说都是通过观察者模式实现的。观察者模式 jdk中有类库实现它,但 Tomcat使用了自己的实现。
责任链模式:
Tomcat 的初始化过程和启动过程都是责任链模式实现的。
模板方法模式:
Tomcat 源码中到处充满着模板方法模式,每个组件的实现控制都用到了模板方法模式。
门面(外观)模式:
Tomcat 内部 Request 和 Response 对象通过门面模式转化为 servlet 中的 Request和 Response 对象。
