Java工程师面试题-设计模式

推荐先阅读Java工程师面试题

说一说设计模式的六大原则

参考答案

单一职责原则

一个类,应当只有一个引起它变化的原因;即一个类应该只有一个职责。

就一个类而言,应该只专注于做一件事和仅有一个引起变化的原因,这就是所谓的单一职责原则。该原则提出了对对象职责的一种理想期望,对象不应该承担太多职责,正如人不应该一心分为二用。唯有专注,才能保证对象的高内聚;唯有单一,才能保证对象的细粒度。对象的高内聚与细粒度有利于对象的重用。一个庞大的对象承担了太多的职责,当客户端需要该对象的某一个职责时,就不得不将所有的职责都包含进来,从而造成冗余代码。

里氏替换原则

在面向对象的语言中,继承是必不可少的、优秀的语言机制,它主要有以下几个优点:

  • 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
  • 提高代码的可重用性;
  • 提高代码的可扩展性;
  • 提高产品或项目的开放性。

相应的,继承也存在缺点,主要体现在以下几个方面:

  • 继承是入侵式的。只要继承,就必须拥有父类的所有属性和方法;
  • 降低代码的灵活性。子类必须拥有父类的属性和方法,使子类受到限制;
  • 增强了耦合性。当父类的常量、变量和方法修改时,必须考虑子类的修改,这种修改可能造成大片的代码需要重构。

从整体上看,继承的“利”大于“弊”,然而如何让继承中“利”的因素发挥最大作用,同时减少“弊”所带来的麻烦,这就需要引入“里氏替换原则”。里氏替换原则的定义有以下两种:

  • 如果对一个类型为S的对象o1,都有类型为T的对象o2,使得以S定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型T是类型S的子类型。
  • 所有引用基类的地方必须能透明地使用其子类对象。清晰明确地说明只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道父类还是子类;但是反过来则不可以,有子类的地方,父类未必就能适应。

依赖倒置原则

依赖倒置原则包括三种含义:

  • 高层模块不应该依赖低层模块,两者都依赖其抽象;
  • 抽象不依赖细节;
  • 细节应该依赖于抽象。

传统的过程性系统的设计办法倾向于高层次的模块依赖于低层次的模块;抽象层次依赖于具体层次。“倒置”原则将这个错误的依赖关系倒置了过来,如下图所示,由此命名为“依赖倒置原则”。

在Java语言中,抽象就是指接口或抽象类,两者都是不能直接被实例化的;细节就是具体的实现类,实现类实现了接口或继承了抽象类,其特点是可以直接被实例化。依赖倒置原则在Java语言中的表现是:

  • 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生;
  • 接口或抽象类不依赖于实现类;
  • 实现类依赖于接口或抽象类。

依赖倒置原则更加精确的定义就是“面向接口编程”——OOD(Object-Oriented Design)的精髓之一。依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。依赖倒置原则是JavaBean、EJB和COM等组件设计模型背后的基本原则。

接口隔离原则

接口隔离原则有如下两种定义:

  1. 客户端不应该依赖它不需要的接口。
  2. 类间的依赖关系应该建立在最小的接口上。

接口隔离原则的具体含义如下:

  • 一个类对另外一个类的依赖性应当是建立在最小的接口上的。
  • 一个接口代表一个角色,不应当将不同的角色都交给一个接口。没有关系的接口合并在一起,形成一个臃肿的大接口,这是对角色和接口的污染。因此使用多个专门的接口比使用单一的总接口要好。
  • 不应该强迫客户依赖于它们不用的方法。接口属于客户,不属于它所在的类层次结构,即不要强迫客户使用它们不用的方法,否则这些客户就会面临由于这些不使用的方法的改变所带来的改变。

迪米特法则

迪米特法则又叫最少知识原则,意思是一个对象应当对其他对象尽可能少的了解。迪米特法则不同于其他的OO设计原则,它具有很多种表述方式,其中具有代表性的是以下几种表述:

  • 只与你直接的朋友们通信;
  • 不要跟“陌生人”说话;
  • 每一个软件单位对其他的单位都只有最少的了解,这些了解仅局限于那些与本单位密切相关的软件单位。

按照迪米特法则,如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用;如果一个类需要调用另一个类的某一个方法,可以通过第三者转发这个调用。

开闭原则

开闭原则的定义是:一个软件实体应当对扩展开放,对修改关闭。这个原则说的是,在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展,即应当可以在不必修改源代码的情况下改变这个模块的行为。

在面向对象的编程中,开闭原则是最基础的原则,起到总的指导作用,其他原则(单一职责、里氏替换、依赖倒置、接口隔离、迪米特法则)都是开闭原则的具体形态,即其他原则都是开闭原则的手段和工具。开闭原则的重要性可以通过以下几个方面来体现。

  • 开闭原则提高复用性。在面向对象的设计中,所有的逻辑都是从原子逻辑组合而来的,而不是在一个类中独立实现一个业务逻辑,代码粒度越小,被复用的可能性就越大,避免相同的逻辑重复增加。开闭原则的设计保证系统是一个在高层次上实现了复用的系统。
  • 开闭原则提高可维护性。一个软件投产后,维护人员的工作不仅仅是对数据进行维护,还可能对程序进行扩展,就是扩展一个类,而不是修改一个类。开闭原则对已有软件模块,特别是最重要的抽象层模块要求不能再修改,这就使变化中的软件系统有一定的稳定性和延续性,便于系统的维护。
  • 开闭原则提高灵活性。所有的软件系统都有一个共同的性质,即对系统的需求都会随时间的推移而发生变化。在软件系统面临新的需求时,系统的设计必须是稳定的。开闭原则可以通过扩展已有的软件系统,提供新的行为,能快速应对变化,以满足对软件新的需求,使变化中的软件系统有一定的适应性和灵活性。
  • 开闭原则易于测试。测试是软件开发过程中必不可少的一个环节。测试代码不仅要保证逻辑的正确性,还要保证苛刻条件(高压力、异常、错误)下不产生“有毒代码”(Poisonous Code),因此当有变化提出时,原有健壮的代码要尽量不修改,而是通过扩展来实现。否则,就需要把原有的测试过程回笼一遍,需要进行单元测试、功能测试、集成测试,甚至是验收测试。开闭原则的使用,保证软件是通过扩展来实现业务逻辑的变化,而不是修改。因此,对于新增加的类,只需新增相应的测试类,编写对应的测试方法,只要保证新增的类是正确的就可以了。

说一下六大原则中的开闭原则

参考答案

开闭原则的定义是:一个软件实体应当对扩展开放,对修改关闭。这个原则说的是,在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展,即应当可以在不必修改源代码的情况下改变这个模块的行为。

在面向对象的编程中,开闭原则是最基础的原则,起到总的指导作用,其他原则(单一职责、里氏替换、依赖倒置、接口隔离、迪米特法则)都是开闭原则的具体形态,即其他原则都是开闭原则的手段和工具。开闭原则的重要性可以通过以下几个方面来体现。

  • 开闭原则提高复用性。在面向对象的设计中,所有的逻辑都是从原子逻辑组合而来的,而不是在一个类中独立实现一个业务逻辑,代码粒度越小,被复用的可能性就越大,避免相同的逻辑重复增加。开闭原则的设计保证系统是一个在高层次上实现了复用的系统。
  • 开闭原则提高可维护性。一个软件投产后,维护人员的工作不仅仅是对数据进行维护,还可能对程序进行扩展,就是扩展一个类,而不是修改一个类。开闭原则对已有软件模块,特别是最重要的抽象层模块要求不能再修改,这就使变化中的软件系统有一定的稳定性和延续性,便于系统的维护。
  • 开闭原则提高灵活性。所有的软件系统都有一个共同的性质,即对系统的需求都会随时间的推移而发生变化。在软件系统面临新的需求时,系统的设计必须是稳定的。开闭原则可以通过扩展已有的软件系统,提供新的行为,能快速应对变化,以满足对软件新的需求,使变化中的软件系统有一定的适应性和灵活性。
  • 开闭原则易于测试。测试是软件开发过程中必不可少的一个环节。测试代码不仅要保证逻辑的正确性,还要保证苛刻条件(高压力、异常、错误)下不产生“有毒代码”(Poisonous Code),因此当有变化提出时,原有健壮的代码要尽量不修改,而是通过扩展来实现。否则,就需要把原有的测试过程回笼一遍,需要进行单元测试、功能测试、集成测试,甚至是验收测试。开闭原则的使用,保证软件是通过扩展来实现业务逻辑的变化,而不是修改。因此,对于新增加的类,只需新增相应的测试类,编写对应的测试方法,只要保证新增的类是正确的就可以了。

手写一个单例模式

参考答案

饿汉式单例模式:

public class Singleton {    
private static Singleton instance = new Singleton();
// 私有构造方法,保证外界无法直接实例化。
private Singleton() {}
// 通过公有的静态方法获取对象实例
public static Singleton getInstace() {
return instance;
}
}

懒汉式单例模式:

public class Singleton {   
private static Singleton instance = null;
// 私有构造方法,保证外界无法直接实例化。
private Singleton() {}
// 通过公有的静态方法获取对象实例
public static Singleton getInstace() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

手写一个线程安全的单例模式

参考答案

在懒汉式单例模式基础上实现线程同步:

public class Singleton {    
private static Singleton instance = null;
// 私有构造方法,保证外界无法直接实例化。
private Singleton() {}
// 通过公有的静态方法获取对象实例
synchronized public static Singleton getInstace() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

上述代码对静态方法 getInstance()进行同步,以确保多线程环境下只创建一个实例。如果getInstance()方法未被同步,并且线程A和线程B同时调用此方法,则执行if (instance == null)语句时都为真,那么线程A和线程B都会创建一个对象,在内存中就会出现两个对象,这样就违反了单例模式。而使用synchronized关键字进行同步后,则不会出现此种情况。

说一说你对工厂模式的理解

参考答案

工厂模式的用意是定义一个创建产品对象的工厂接口,将实际创建性工作推迟到子类中。工厂模式可分为简单工厂、工厂方法和抽象工厂模式。注意,我们常说的23种经典设计模式,包含了工程方法模式和抽象工厂模式,而并未包含简单工厂模式。另外,我们平时说的工厂模式,一般默认是指工厂方法模式。

简单工厂

简单工厂模式其实并不算是一种设计模式,更多的时候是一种编程习惯。简单工厂的实现思路是,定义一个工厂类,根据传入的参数不同返回不同的实例,被创建的实例具有共同的父类或接口。简单工厂的适用场景是:

  • 需要创建的对象较少。
  • 客户端不关心对象的创建过程。

示例:

创建一个可以绘制不同形状的绘图工具,可以绘制圆形,正方形,三角形,每个图形都会有一个draw()方法用于绘图,不看代码先考虑一下如何通过该模式设计完成此功能。

由题可知圆形,正方形,三角形都属于一种图形,并且都具有draw方法,所以首先可以定义一个接口或者抽象类,作为这三个图像的公共父类,并在其中声明一个公共的draw方法:

public interface Shape {     
void draw();
}

下面就是编写具体的图形,每种图形都实现Shape接口:

// 圆形 
class CircleShape implements Shape {
public CircleShape() {
System.out.println("CircleShape: created");
}
@Override
public void draw() {
System.out.println("draw: CircleShape");
}
}

// 正方形
class RectShape implements Shape {
public RectShape() {
System.out.println("RectShape: created");
}
@Override
public void draw() {
System.out.println("draw: RectShape");
}
}

// 三角形
public class TriangleShape implements Shape {
public TriangleShape() {
System.out.println("TriangleShape: created");
}
@Override
public void draw() {
System.out.println("draw: TriangleShape");
}
}

下面是工厂类的具体实现:

class ShapeFactory {         
public static Shape getShape(String type) {
Shape shape = null;
if (type.equalsIgnoreCase("circle")) {
shape = new CircleShape();
} else if (type.equalsIgnoreCase("rect")) {
shape = new RectShape();
} else if (type.equalsIgnoreCase("triangle")) {
shape = new TriangleShape();
}
return shape;
}
}

为工厂类传入不同的type可以new不同的形状,返回结果为Shape 类型,这个就是简单工厂核心的地方了。

工厂方法

工厂方法模式是简单工厂的仅一步深化, 在工厂方法模式中,我们不再提供一个统一的工厂类来创建所有的对象,而是针对不同的对象提供不同的工厂。也就是说每个对象都有一个与之对应的工厂。

工厂方法的实现思路是,定义一个用于创建对象的接口,让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类。

示例:

现在需要设计一个这样的图片加载类,它具有多个图片加载器,用来加载jpg,png,gif格式的图片,每个加载器都有一个read()方法,用于读取图片。下面我们完成这个图片加载类。

首先完成图片加载器的设计,编写一个加载器的公共接口:

public interface Reader {    
void read();
}

然后完成各个图片加载器的代码:

// jpg图片加载器 
class JpgReader implements Reader {
@Override
public void read() {
System.out.print("read jpg");
}
}

// png图片加载器
class PngReader implements Reader {
@Override
public void read() {
System.out.print("read png");
}
}

// gif图片加载器
class GifReader implements Reader {
@Override
public void read() {
System.out.print("read gif");
}
}

现在我们按照定义所说定义一个抽象的工厂接口ReaderFactory:

interface ReaderFactory {
Reader getReader();
}

里面有一个getReader()方法返回我们的Reader 类,接下来我们把上面定义好的每个图片加载器都提供一个工厂类,这些工厂类实现了ReaderFactory 。

// jpg加载器工厂
class JpgReaderFactory implements ReaderFactory {
@Override
public Reader getReader() {
return new JpgReader();
}
}

// png加载器工厂
class PngReaderFactory implements ReaderFactory {
@Override
public Reader getReader() {
return new PngReader();
}
}

// gif加载器工厂
class GifReaderFactory implements ReaderFactory {
@Override
public Reader getReader() {
return new GifReader();
}
}

在每个工厂类中我们都通过重写的getReader()方法返回各自的图片加载器对象。

和简单工厂对比一下,最根本的区别在于,简单工厂只有一个统一的工厂类,而工厂方法是针对每个要创建的对象都会提供一个工厂类,这些工厂类都实现了一个工厂基类。

下面总结一下工厂方法的适用场景:

  • 客户端不需要知道它所创建的对象的类。
  • 客户端可以通过子类来指定创建对应的对象。

抽象工厂

这个模式最不好理解,而且在实际应用中局限性也蛮大的,因为这个模式并不符合开闭原则。实际开发还需要做好权衡。抽象工厂模式是工厂方法的仅一步深化,在这个模式中的工厂类不单单可以创建一个对象,而是可以创建一组对象。这是和工厂方法最大的不同点。

抽象工厂的实现思路是,提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂和工厂方法一样可以划分为4大部分:

  • AbstractFactory(抽象工厂):声明了一组用于创建对象的方法,注意是一组。
  • ConcreteFactory(具体工厂):它实现了在抽象工厂中声明的创建对象的方法,生成一组具体对象。
  • AbstractProduct(抽象产品):它为每种对象声明接口,在其中声明了对象所具有的业务方法。
  • ConcreteProduct(具体产品):它定义具体工厂生产的具体对象。

示例:

现在需要做一款跨平台的游戏,需要兼容Android,Ios,Wp三个移动操作系统,该游戏针对每个系统都设计了一套操作控制器(OperationController)和界面控制器(UIController),下面通过抽闲工厂方式完成这款游戏的架构设计。

由题可知,游戏里边的各个平台的UIController和OperationController应该是我们最终生产的具体产品。所以新建两个抽象产品接口。

抽象操作控制器:

interface OperationController {
void control();
}

抽象界面控制器:

interface UIController {
void display();
}

然后完成各个系统平台的具体操作控制器和界面控制器。

Android:

class AndroidOperationController implements OperationController {
@Override
public void control() {
System.out.println("AndroidOperationController");
}
}

class AndroidUIController implements UIController {
@Override
public void display() {
System.out.println("AndroidInterfaceController");
}
}

IOS:

class IosOperationController implements OperationController {
@Override
public void control() {
System.out.println("IosOperationController");
}
}

class IosUIController implements UIController {
@Override
public void display() {
System.out.println("IosInterfaceController");
}
}

WP:

class WpOperationController implements OperationController {
@Override
public void control() {
System.out.println("WpOperationController");
}
}

class WpUIController implements UIController {
@Override
public void display() {
System.out.println("WpInterfaceController");
}
}

下面定义一个抽闲工厂,该工厂需要可以创建OperationController和UIController。

public interface SystemFactory {
public OperationController createOperationController();
public UIController createInterfaceController();
}

在各平台具体的工厂类中完成操作控制器和界面控制器的创建过程。

Android:

public class AndroidFactory implements SystemFactory {
@Override
public OperationController createOperationController() {
return new AndroidOperationController();
}
@Override
public UIController createInterfaceController() {
return new AndroidUIController();
}
}

IOS:

public class IosFactory implements SystemFactory {
@Override
public OperationController createOperationController() {
return new IosOperationController();
}
@Override
public UIController createInterfaceController() {
return new IosUIController();
}
}

WP:

public class WpFactory implements SystemFactory {
@Override
public OperationController createOperationController() {
return new WpOperationController();
}
@Override
public UIController createInterfaceController() {
return new WpUIController();
}
}

下面总结一下抽象工厂的适用场景:

  • 和工厂方法一样客户端不需要知道它所创建的对象的类。
  • 需要一组对象共同完成某种功能时。并且可能存在多组对象完成不同功能的情况。
  • 系统结构稳定,不会频繁的增加对象。

简单工厂模式和抽象工厂模式有什么区别?

参考答案

简单工厂模式其实并不算是一种设计模式,更多的时候是一种编程习惯。简单工厂的实现思路是,定义一个工厂类,根据传入的参数不同返回不同的实例,被创建的实例具有共同的父类或接口。

工厂方法模式是简单工厂的仅一步深化, 在工厂方法模式中,我们不再提供一个统一的工厂类来创建所有的对象,而是针对不同的对象提供不同的工厂。也就是说每个对象都有一个与之对应的工厂。工厂方法的实现思路是,定义一个用于创建对象的接口,让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类。

抽象工厂模式是工厂方法的仅一步深化,在这个模式中的工厂类不单单可以创建一个对象,而是可以创建一组对象。这是和工厂方法最大的不同点。抽象工厂的实现思路是,提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。

如何实现工厂模式?

参考答案

简单工厂

示例:

创建一个可以绘制不同形状的绘图工具,可以绘制圆形,正方形,三角形,每个图形都会有一个draw()方法用于绘图,不看代码先考虑一下如何通过该模式设计完成此功能。

由题可知圆形,正方形,三角形都属于一种图形,并且都具有draw方法,所以首先可以定义一个接口或者抽象类,作为这三个图像的公共父类,并在其中声明一个公共的draw方法:

public interface Shape {
void draw();
}

下面就是编写具体的图形,每种图形都实现Shape接口:

// 圆形
class CircleShape implements Shape {
public CircleShape() {
System.out.println("CircleShape: created");
}
@Override
public void draw() {
System.out.println("draw: CircleShape");
}
}

// 正方形
class RectShape implements Shape {
public RectShape() {
System.out.println("RectShape: created");
}
@Override
public void draw() {
System.out.println("draw: RectShape");
}
}

// 三角形
public class TriangleShape implements Shape {
public TriangleShape() {
System.out.println("TriangleShape: created");
}
@Override
public void draw() {
System.out.println("draw: TriangleShape");
}
}

下面是工厂类的具体实现:

class ShapeFactory {
public static Shape getShape(String type) {
Shape shape = null;
if (type.equalsIgnoreCase("circle")) {
shape = new CircleShape();
} else if (type.equalsIgnoreCase("rect")) {
shape = new RectShape();
} else if (type.equalsIgnoreCase("triangle")) {
shape = new TriangleShape();
}
return shape;
}
}

为工厂类传入不同的type可以new不同的形状,返回结果为Shape 类型,这个就是简单工厂核心的地方了。

工厂方法

示例:

现在需要设计一个这样的图片加载类,它具有多个图片加载器,用来加载jpg,png,gif格式的图片,每个加载器都有一个read()方法,用于读取图片。下面我们完成这个图片加载类。

首先完成图片加载器的设计,编写一个加载器的公共接口:

public interface Reader {
void read();
}

然后完成各个图片加载器的代码:

// jpg图片加载器
class JpgReader implements Reader {
@Override
public void read() {
System.out.print("read jpg");
}
}
// png图片加载器
class PngReader implements Reader {
@Override
public void read() {
System.out.print("read png");
}
}
// gif图片加载器
class GifReader implements Reader {
@Override
public void read() {
System.out.print("read gif");
}
}

现在我们按照定义所说定义一个抽象的工厂接口ReaderFactory:

interface ReaderFactory {
Reader getReader();
}

里面有一个getReader()方法返回我们的Reader 类,接下来我们把上面定义好的每个图片加载器都提供一个工厂类,这些工厂类实现了ReaderFactory 。

// jpg加载器工厂
class JpgReaderFactory implements ReaderFactory {
@Override
public Reader getReader() {
return new JpgReader();
}
}
// png加载器工厂
class PngReaderFactory implements ReaderFactory {
@Override
public Reader getReader() {
return new PngReader();
}
}
// gif加载器工厂
class GifReaderFactory implements ReaderFactory {
@Override
public Reader getReader() {
return new GifReader();
}
}

在每个工厂类中我们都通过重写的getReader()方法返回各自的图片加载器对象。

抽象工厂

示例:

现在需要做一款跨平台的游戏,需要兼容Android,Ios,Wp三个移动操作系统,该游戏针对每个系统都设计了一套操作控制器(OperationController)和界面控制器(UIController),下面通过抽闲工厂方式完成这款游戏的架构设计。

由题可知,游戏里边的各个平台的UIController和OperationController应该是我们最终生产的具体产品。所以新建两个抽象产品接口。

抽象操作控制器:

interface OperationController {
void control();
}

抽象界面控制器:

interface UIController {
void display();
}

然后完成各个系统平台的具体操作控制器和界面控制器。

Android:

class AndroidOperationController implements OperationController {
@Override
public void control() {
System.out.println("AndroidOperationController");
}
}
class AndroidUIController implements UIController {
@Override
public void display() {
System.out.println("AndroidInterfaceController");
}
}

IOS:

class IosOperationController implements OperationController {
@Override
public void control() {
System.out.println("IosOperationController");
}
}
class IosUIController implements UIController {
@Override
public void display() {
System.out.println("IosInterfaceController");
}
}

WP:

class WpOperationController implements OperationController {
@Override
public void control() {
System.out.println("WpOperationController");
}
}
class WpUIController implements UIController {
@Override
public void display() {
System.out.println("WpInterfaceController");
}
}

下面定义一个抽闲工厂,该工厂需要可以创建OperationController和UIController。

public interface SystemFactory {
public OperationController createOperationController();
public UIController createInterfaceController();
}

在各平台具体的工厂类中完成操作控制器和界面控制器的创建过程。

Android:

public class AndroidFactory implements SystemFactory {
@Override
public OperationController createOperationController() {
return new AndroidOperationController();
}
@Override
public UIController createInterfaceController() {
return new AndroidUIController();
}
}

IOS:

public class IosFactory implements SystemFactory {
@Override
public OperationController createOperationController() {
return new IosOperationController();
}
@Override
public UIController createInterfaceController() {
return new IosUIController();
}
}

WP:

public class WpFactory implements SystemFactory {
@Override
public OperationController createOperationController() {
return new WpOperationController();
}
@Override
public UIController createInterfaceController() {
return new WpUIController();
}
}

说一说你策略模式的理解

参考答案

策略模式(Strategy Pattern)也叫政策模式,是一种比较简单的模式。它的目的是定义一组算法,将每个算法都封装起来,并且使它们之间可以互换。其用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换,使得算法可以在不影响到客户端的情况下发生变化。

策略模式的通用类图如下图所示:

策略模式涉及以下3个角色:

  • 环境(Context)角色:该角色也叫上下文角色,起到承上启下的作用,屏蔽高层模块对策略、算法的直接访问,它持有一个Strategy类的引用。
  • 抽象策略(Strategy)角色:该角色对策略、算法进行抽象,通常定义每个策略或算法必须具有的方法和属性。
  • 具体策略(Concrete Strategy)角色:该角色实现抽象策略中的具体操作,含有具体的算法。

抽象策略Strategy的代码如下所示:

public abstract class Strategy {
public abstract void strategyInterface();
}

具体策略ConcreteStrategy的代码如下所示:

public class ConcreteStrategy extends Strategy {
public void strategyInterface() {
...
}
}

环境角色Context的代码如下所示:

public class Context {
private Strategy strategy = null;
public Context(Strategy strategy) {
this.strategy = strategy;
}
public void contextInterface() {
this.strategy.strategyInterface();
}
}

策略模式包括如下优点:

  • 策略模式提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族,恰当地使用继承可以把公共的代码移到父类中,从而避免代码重复。
  • 策略模式提供了可以替换继承关系的办法。继承可以处理多种算法或行为,如果不用策略模式,那么使用算法或行为的环境类就可能会有一些子类,每一个子类提供一个不同的算法或行为。但是,这样算法或行为的使用者就和算法本身混在一起,从而不可能再独立演化。
  • 使用策略模式可以避免使用多重条件转移语句。多重转移语句不易维护,它把采取哪一种算法或采取哪一种行为的逻辑与算法或行为的逻辑混合在一起,统统列在一个多重转移语句里面,这比使用继承的办法还要原始和落后。

策略模式包括如下缺点:

  • 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法类,即策略模式只适用于客户端知道所有的算法或行为的情况。
  • 策略模式造成很多的策略类。有时候可以通过把依赖于环境的状态保持到客户端里面,而将策略类设计成可共享的,这样策略类实例可以被不同客户端使用。可以使用享元模式来减少对象的数量。

策略模式有如下几个应用场景:

  • 多个类只是在算法或行为上稍有不同的场景。
  • 算法需要自由切换的场景。
  • 需要屏蔽算法规则的场景。

说一说你对观察者模式的了解

参考答案

观察者模式(Observer Pattern)也称发布订阅模式,它的目的是定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新。

观察者模式的类图如下图所:

观察者模式具有以下4个角色:

  • 抽象主题(Subject)角色:该角色又称为“被观察者”,可以增加和删除观察者对象。
  • 抽象观察者(Observer)角色:该角色为所有的具体观察者定义一个接口,在得到主题的通知时更新自己。
  • 具体主题(Concrete Subject)角色:该角色又称为“具体被观察者”,它将有关状态存入具体观察者对象,在具体主题的内部状态改变时,给所有登记过的观察者发出通知。
  • 具体观察者(Concrete Observer)角色:该角色实现抽象观察者所要求的更新接口,以便使自身的状态与主题的状态相协调。

上述类图所涉及的代码如下所示:

interface Subject {
// 登记一个新的观察者
public void attach(Observer obs);
// 删除一个登记过的观察者
public void detach(Observer obs);
// 通知所有登记过的观察者对象
public void notifyObserver();
}
interface Observer {
// 更新方法
public void update();
}
class ConcreteSubject implements Subject {
private Vector<Observer> obsVector = new Vector<Observer>();
// 登记一个新的观察者
public void attach(Observer obs) {
obsVector.add(obs);
}
// 删除一个登记过的观察者
public void detach(Observer obs) {
obsVector.remove(obs);
}
// 通知所有登记过的观察者对象
public void notifyObserver() {
for (Observer e : obsVector) {
e.update();
}
}
// 返回观察者集合的Enumeration对象
public Enumeration<Observer> observers() {
return obsVector.elements();
}
// 业务方法,改变状态
public void change() {
this.notifyObserver();
}
}
class ConcreteObserver implements Observer {
// 实现更新方法
public void update() {
System.out.println("收到通知,并进行处理!");
}
}

观察者模式具有以下几个优点:

  • 观察者和被观察者之间是抽象耦合。被观察者角色所知道的只是一个具体观察者集合,每一个具体观察者都符合一个抽象观察者的接口。被观察者并不认识任何一个具体的观察者,它只知道它们都有一个共同的接口。由于被观察者和观察者没有紧密的耦合在一起,因此它们可以属于不同的抽象化层次,且都非常容易扩展。
  • 支持广播通信。被观察者会向所有登记过的观察者发出通知,这就是一个触发机制,形成一个触发链。

观察模式的缺点如下:

  • 如果一个主题有多个直接或间接的观察者,则通知所有的观察者会花费很多时间,且开发和调试都比较复杂。
  • 如果在主题之间有循环依赖,被观察者会触发它们之间进行循环调用,导致系统崩溃。在使用观察者模式时要特别注意这一点。
  • 如果对观察者的通知是通过另外的线程进行异步投递,系统必须保证投递的顺序执行。
  • 虽然观察者模式可以随时使观察者知道所观察的对象发生了变化,但是观察者模式没有提供相应的机制使观察者知道所观察的对象是如何发生变化。

观察者模式的应用场景如下:

  • 关联行为场景。
  • 事件多级触发场景。
  • 跨系统的消息交换场景,如消息队列的处理机制。

说一说你对责任链模式的了解

参考答案

责任链模式(Chain of Responsibility Pattern)是一种常见的行为模式,它的目的是使多个对象都有机会处理请求,从而避免了请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。

责任链模式的重点是在“链”上,由一条链去处理相似的请求,在链中决定谁来处理这个请求,并返回相应的结果。责任链模式的类图如下图所示:

责任链模式涉及以下两个角色:

  • 抽象处理者(Handler)角色:该角色对请求进行抽象,并定义一个方法以设定和返回对下一个处理者的引用。
  • 具体处理者(Concrete Handler)角色:该角色接到请求后,可以选择将请求处理掉,或者将请求传给下一个处理者。由于具体处理者持有对下一个处理者的引用,因此,如果需要,具体处理者可以访问下一个处理者。

上述类图所涉及的代码如下所示:

abstract class Handler {
private Handler successor;
public abstract void handleRequest();
public Handler getSuccessor() {
return successor;
}
public void setSuccessor(Handler successor) {
this.successor = successor;
}
}
class ConcreteHandler extends Handler {
// 处理请求
public void handleRequest() {
if (getSuccessor() != null) {
System.out.println("请求传递给" + getSuccessor());
getSuccessor().handleRequest();
} else {
System.out.println("请求处理");
}
}
}

责任链模式的优点如下:

  • 责任链模式将请求和处理分开,请求者不知道是谁处理的,处理者可以不用知道请求的全貌。
  • 提高系统的灵活性。

责任链模式的缺点如下:

  • 降低程序的性能,每个请求都是从链头遍历到链尾,当链比较长的时候,性能会大幅下降。
  • 不易于调试,由于采用了类似递归的方式,调试的时候逻辑比较复杂。

责任链模式的应用场景如下:

  • 一个请求需要一系列的处理工作。
  • 业务流的处理,例如,文件审批。
  • 对系统进行补充扩展。

说一说装饰器模式和适配器模式的区别

参考答案

装饰器的目的是动态地给一个对象添加一些额外的职责,这个对象的类型不会发生变化,但是行为却发生了改变。

适配器的目的是将一个类的接口变换成客户端所期待的另一种接口,就是可以将一个对象包装成另外的一个接口。

Spring框架中用到了哪些设计模式?

Spring框架在实现时运用了大量的设计模式,常见的有如下几种:

  1. 简单工厂

    Spring中的BeanFactory就是简单工厂模式的体现,根据传入一个唯一的标识来获得Bean对象,但是否是在传入参数后创建还是传入参数前创建这个要根据具体情况来定。

  2. 工厂方法

    实现了FactoryBean接口的bean是一类叫做factory的bean。其特点是,spring会在使用getBean()调用获得该bean时,会自动调用该bean的getObject()方法,所以返回的不是factory这个bean,而是这个bean的getOjbect()方法的返回值。

  3. 单例模式

    Spring依赖注入Bean实例默认是单例的。Spring的依赖注入(包括lazy-init方式)都是发生在AbstractBeanFactory的getBean里。getBean的doGetBean方法调用getSingleton进行bean的创建。

  4. 适配器模式

    SpringMVC中的适配器HandlerAdatper,它会根据Handler规则执行不同的Handler。即DispatcherServlet根据HandlerMapping返回的handler,向HandlerAdatper发起请求处理Handler。HandlerAdapter根据规则找到对应的Handler并让其执行,执行完毕后Handler会向HandlerAdapter返回一个ModelAndView,最后由HandlerAdapter向DispatchServelet返回一个ModelAndView。

  5. 装饰器模式

    Spring中用到的装饰器模式在类名上有两种表现:一种是类名中含有Wrapper,另一种是类名中含有Decorator。

  6. 代理模式

    AOP底层就是动态代理模式的实现。即:切面在应用运行的时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象创建动态的创建一个代理对象。SpringAOP就是以这种方式织入切面的。

  7. 观察者模式

    Spring的事件驱动模型使用的是观察者模式,Spring中Observer模式常用的地方是listener的实现。

  8. 策略模式

    Spring框架的资源访问Resource接口。该接口提供了更强的资源访问能力,Spring 框架本身大量使用了 Resource 接口来访问底层资源。Resource 接口是具体资源访问策略的抽象,也是所有资源访问类所实现的接口。

  9. 模板方法模式

    Spring模板方法模式的实质,是模板方法模式和回调模式的结合,是Template Method不需要继承的另一种实现方式。Spring几乎所有的外接扩展都采用这种模式。