面试题-设计模式

前言

关于设计模式这篇面试题,我是参考了刘伟老师的书籍设计模式的艺术总结的,刘伟老师的书籍叙述详实,娓娓道来,语言质朴实在,是我很喜欢的国内计算机作者之一。希望总结的面试题能够对大家有所帮助。

系列面试题文章:

面试题-Java基础

面试题-Java集合

面试题-Java多线程基础、实现工具和可见性保证

面试题-线程池和原子变量

面试题-Java虚拟机

面试题-计算机网络1

面试题-计算机网络-HTTP部分

面试题-操作系统-进程

设计模式

设计模式基础

  1. 设计模式的定义

    设计模式来源于建筑工程领域,建筑工程领域首先提出了模式的概念。模式是一种在特定环境下,人们解决某类重复出现问题的一套成功有效的解决方案。

    设计模式,指的是软件工程领域,人们为了解决某类软件设计开发的重复问题而总结出来的代码设计经验。

  2. 设计模式的关键要素

    • 模式名称:通过命名,可以使我们更好的理解设计模式,并方便开发人员沟通交流。

    • 问题:描述了软件开发遇到的某个设计问题,并且指出当时的限定条件

    • 解决方案:描述了设计模式的组成部分,还描述了各个组成部分的相互关系、他们的职责和如何协作,通常使用UML类图和核心代码描述

    • 效果:模式的优缺点

  3. 设计模式的分类

    设计模式可以分为三类,分别为创建型设计模式、结构型设计模式和行为型设计模式。

    创建型设计模式主要关注如何创建对象;结构型设计模式主要关注类或对象的组合;行为型设计模式主要关注类或对象之间的交互和职责分配。

  4. 设计模式的用途

    • 避免重复劳动,提高设计和开发效率
    • 设计模式提供了一套通用的设计语言,方便开发人员沟通交流
    • 斗胆简述已故软件工程大师的一句话:
      • 模式不保证任何东西,但他们会给缺乏经验但是具备才能和创造力的人带来希望。

UML和面向对象设计原则

  1. UML的结构组成你清楚吗? 比如什么是视图、图,了解他们之间的关系吗?

    UML的结构组成如下:

    • 视图:视图用于从不同角度来表示待建模系统,UML视图包括如下:

      • 用户视图:用于描述用户角度的系统需求
      • 结构视图:用于描述系统中静态元素比如包类对象以及他们的关系
      • 行为视图:用于描述系统在运行时,各种元素之间的交互关系
      • 实现视图:用于描述系统中的物理文件以及他们的关系
      • 环境视图:用于描述系统中的硬件设备以及他们之间的关系

      简单来说,从系统需求开始,具象为系统中的静态结构和动态关系,然后具象为系统中的物理文件以及硬件设备,笔者认为是由一定递进的关系的。

    • 图:描述UML视图的图形。

    • 模型元素:UML图中的一些概念,对应于普通面向对象的概念以及这些概念之间的关系。

    • 通用机制:为模型元素提供额外的注释信息等等

  2. 类图中,都有哪些类和类之间的关系?能简单说说吗?

    • 关联关系:标识类于类之间有一定的联系。

      • 聚合:表达了整体与部分的关系,但是部分可以脱离整体存在。

      • 组合:也表达了整体与部分的关系,但是脱离了整体部分不能存在。

    • 依赖关系:表达了一种使用关系,特定事物的改变可能会影响到使用该事物的其他事物。

      • 类作为方法参数
      • 类作为方法的局部变量
      • 在方法中调用类的静态方法
    • 继承关系:父与子的关系。

    • 接口和实现关系

  3. 你都知道哪些设计原则?简单说说

    • 单一职责原则:不要设计大而全的类,而要设计粒度小,功能单一的类。

      简单来说,如果一个类中包含了两个或两个以上业务不相干的功能,那么就应该拆分,实际应用中还需要考虑当前的应用场景和需求背景,同一个类,可能在A场景下是满足单一职责的,但是换做B场景就不符合了。在实际开发中笔者会尽可能的在当前业务场景下设计一个较为粗粒度的类,如果后续场景需求改变,需要拆分,那么就会继续拆分成更细粒度的类。(继续举个例子)我们最近在进行串口开发时,原来的业务类中包含了具体的发送实现,考虑到后续有其他业务也会使用串口发送数据,就把串口相关的方法拿出来形成了一个驱动类。

    • 开闭原则:添加一个新的功能或者修改一个功能时,尽量通过在已有代码的基础上扩展代码,而非修改已有代码。

      理解这个定义时,要注意,扩展或修改一个功能,不可能完全不修改,而是要尽可能避免核心代码的修改。在实际开发时,要根据业务需求和修改成本思考一下扩展点,但也不能因为过度设计而增加了系统的复杂度和降低了可读性。(继续举个例子)串口业务开发时,我们需要对接上层的业务系统传递过来的基础数据,分析一下,数据对接层面可能是未来改动较大的部分,所以在最初设计的时候就把数据对接的功能抽取出来为一个接口,下游业务使用接口调用来实现数据对接,这样就算对接方式修改了,只需要增加一个新的实现即可,进而实现了开闭原则。

    • 里氏替换原则:子类在设计时,要保证在替换父类时,不改变程序原有的逻辑以及不破坏程序的正确性。

      简单来说,子类要在不违背的父类方法设计初衷的基础上扩展父类。举个例子,如果父类的方法定义中说明了不能抛出异常,但是子类却抛出异常,这样如果使用多态调用了子类的方法,可能会引起调用方的异常。接口和实现也是如此。

    • 接口隔离原则:不设计大而全的接口,而是根据不同的客户端定制不同的接口。

      这样做的好处是,客户端不会看见不需要的接口,增加了一定程度的安全性,同时,实现类也不需要实现不需要的方法,避免了大量的无用代码。在实际开发中,设计接口的功能时,要考虑实现或者调用方是否需要,如果出现了大量不需要的方法,那么就需要重构,把客户端需要的方法抽取出来成为一个独立的接口。

    • 依赖倒转原则: 针对接口编程而不是针对实现编程。

      这条原则比较简单,针对接口编程,这样底层修改实现就不会影响客户端的调用者。要实现依赖倒转原则,需要使用依赖注入。

      插播一条小知识:

      控制反转到底是什么意思?王争老师的设计模式之美的第19课就详细的讲述了,我觉得非常好,大家可以参考。我这里也简单总结下,控制指的是对程序流程的控制,反转指的是程序流程控制从程序员转换为了框架,程序员只需要实现框架预留好的扩展点即可利用框架驱动整个流程。

    • 合成复用原则:当想要复用代码时,尽量多用组合少用继承。

      这条原则笔者持怀疑态度,笔者认为,在复用代码时,也需要具体问题具体分析,而不应该一味的使用组合,当A类与B类确实时is-a的关系时,应该使用继承;has-a的关系时,应该使用组合。(继续举个例子)笔者所负责的中间件组平时的一个重要工作就是针对不同硬件实现命令交互协议,虽然有很多命令,但是他们的基础格式是相同的,都包含一些共有的字段,所以笔者使用了BaseMessage的方法抽象出了一个抽象基类,存储公共的字段,并且负责公共字段的解析,具体的命令则负责自己的字段的存储与解析,实现了代码复用。在这种情况下,使用继承就是合理的。

    • 迪米特法则: 设计系统时,尽量减少对象之间不必要的交互,这样一旦发生修改,对其他功能的影响也较小。

      这部分我的理解不深,需要实践后再来填充这部分内容。


创建型模式

单例模式

单例模式要解决的问题

单例模式出现的动机是,在软件开发中,我们希望某些类在系统中只有一个实例,不希望有多个实例,比如配置文件类,各种管理类等等。

单例模式的多种解法

单例模式有很多种解法,每种解法各有优缺点。

饿汉式

饿汉式的代码如下:

class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton{
        
    }
    public static Singleton getInstance() {
        return instance;
    }
}

优点:

  • 饿汉式的代码实现很简洁,并且由jvm保证了线程安全
  • 饿汉式因为在类加载时就会创建实例,假如实例比较占用资源或者初始化耗时长,有人说会浪费资源,但仔细分析却不然,原因如下:
    • 如果初始化耗时很长,最好不要等到用到它时再去初始化,而应该在类加载的时候提前初始化,会提高性能
    • 如果实例占用资源多,我们也希望在类加载的时候就尽快去加载,因为如果资源不足,就可以提前报错,也比用到时再报错要好得多。

缺点:按照上面的逻辑看,饿汉式其实没什么缺点。

懒汉式和双重检查

笔者个人不推崇使用这两种方式,实现起来复杂而且对系统性能提升却没什么意义,所以这里不介绍。

静态内部类

如果一定要追求延迟加载的话,静态内部类是一种很好的实现方式。

静态内部类代码如下:

class Singleton {
     private Singleton{
        
    }
    
    private static class HolderClass{
        private static Singleton instance = new Singleton();
    }
    
    public static Singleton getInstance() {
        return HolderClass.instance;
    }
}

优点:

  • 实现了延迟加载
  • 由JVM保证线程安全

缺点:

  • 代码实现较饿汉式较复杂一些

单例模式的优缺点

这里主要谈谈单例类的缺点:

  • 单例类没有抽象层,不容易扩展
  • 单例类职责过重,同时承担了创建对象和对象的业务功能两个职责。

简单工厂模式

简单工厂模式要解决的问题

对象的创建和使用如果耦合在一个核心的业务类中,当对象的构造函数改变或者需要增加子类对象,就需要修改核心业务类,不符合开闭原则。所以将对象的创建和使用分离就是必须要做的事情了。引入简单工厂,把创建对象的逻辑从核心业务类中剥离,这样有利于代码的复用,并且当对象的构造函数改变或者需要增加新的子类时,核心业务类代码不需要修改。

  • 有利于复用
  • 有利于维护

简单工厂模式的解法

简单工厂代码的UML类图如下:

简单工厂模式的优缺点

这里也主要谈谈缺点:

  • 工厂类的负责所有产品的创建逻辑,职责很重,有单点问题
  • 系统扩展困难,当有新产品需要添加,还是需要修改工厂类的代码
  • 基于静态方法实现,无法使用继承的等级结构

工厂方法模式

工厂方法模式要解决的问题

工厂方法模式主要想解决简单工厂模式的几个缺点。如上,这里就不再赘述了。

工厂方法模式的解法

工厂方法模式通过引入一个抽象的工厂层来解决简单工厂的问题。具体工厂类实现了抽象工厂的方法,不同的产品由不同的具体工厂类创建,客户端只需要知道工具体工厂相关的信息即可。

工厂方法模式的缺点

工厂方法模式虽然解决了简单工厂的缺点,但是也引入了新的复杂度。

  • 当需要引入一个新的产品时,除了需要创建具体产品类,还需要创建对应的具体工厂类,系统中的类是成对增加的。

这里简单说说抽象工厂模式,抽象工厂模式就是为了解决工厂方法模式中产品和工厂类暴增的缺点的,主要解决方式是,分析各个产品,把同一个产品族的产品们放到同一个具体工厂中生产,而不是每一个产品对应一个工厂,这这样大大降低了具体工厂类的数量。


结构型模式

装饰器模式

装饰器模式要解决的问题

当需要扩展某个类的功能时,并且需要扩展的类很多,扩展功能也很多,如果用继承,排列组合下来会产生很多子类。比如有ABC三个待扩展的类,和DEF三个扩展功能,假如每个待扩展的类只需要扩展一个功能时,会产生3*3=9个子类,当需要扩展的类越多,功能越多时,需要的子类就越多,无法维护。

装饰器模式的解法

装饰器模式通过把不同的功能编写为不同的装饰器,使装饰器和待扩展的类都实现同一个接口,装饰器内部引用待扩展的类,来实现装饰新功能。这样做,对客户端来说,引用方法不变,还可以通过多重装饰灵活扩展功能。

装饰器模式的UML类图:

装饰器模式的缺点

  • 如果功能点很多,装饰器模式会产生很多装饰器类,从而增加系统复杂度

代理模式

代理模式要解决的问题

代理模式也是为了给对象增加某些功能而出现的,所以代理模式有点类似上面提到的装饰者模式,代理对象和原始对象都实现同一个接口,代理对象持有原始对象的一个引用,客户端调用代理对象的接口方法时,代理对象会增加一些功能然后调用真正的原始对象。那么他们有什么不同呢?书中总结的不同如下:

  • 问题域的差别。代理模式要增加的问题域和原始对象的问题域不同;装饰者模式的问题域和原始对象的问题域相同。比如:代理模式的应用中有一种记录日志的代理或者增加访问权限的代理,这种日志和访问权限的需求很多业务类都需要实现,所以这种属于系统需求,不属于业务需求。装饰者模式却不同,待装饰的功能就是实际的业务需求。

代理模式的解法

代理模式的问UML类图如下:

动态代理

关于动态代理,网络中已经有很多优秀的文章,这里笔者就不再赘述具体的实现细节。只讲一下动态代理和静态代理的区别。

上面介绍的代理模式其实是静态代理,何为静态?假如我们需要给A业务类和B业务类都增加记录日志的功能,那么我们就需要编写两个代理类,在代理类中编写相同的日志记录代码(当然优化下也可以把日志记录代码抽取出来复用代码),然后编译运行,如果运行中我们还想把日志记录的功能用于C类,那么就需要编写C类的代理类。这么做其实有点麻烦,我们需要给每个需要代理的类编写代理类。后来出现的动态代理解决了这个问题。

动态代理中,我们只需要实现一个handler,把日志记录的逻辑写到里面,实际运行时,java框架层面就可以根据我们的设置动态创建代理类。和之前的方式相比,我们不需要自己编写代理类了,只需要增加两行代码,让Java帮我们创建。这也是一种控制反转呢。😀

代理模式的缺点

  • 有些模式的代理会造成一些性能问题,比如增加了额外的权限校验代理。

  • 代理模式也增加了系统的复杂度。


行为型模式

责任链模式

责任链模式要解决的问题

最典型的是审批系统,一个请求需要被审批链条上的多个领导层层审批,并且调用方想要动态的定制审批顺序或者增删审批链条上的领导时,就需要使用责任链模式。如果不用责任链模式,审批业务代码会变得很冗长,并且因为代码写死,所以无法动态灵活的更改审批链条。

责任链模式的解法

增加一个抽象层和客户端对接,抽象层中包含链条中下一个审批者的引用。链条中的具体审批者继承或者实现这个抽象层即可。使用时需要在客户端构建审批链,然后请求就会依次传递下去。

责任链模式的缺点

  • 请求链如果过长,系统性能也会收到一定影响。
  • 如果建链不当,可能会造成系统死循环。

策略模式

策略模式要解决的问题

软件开发中,我们不可避免要编写各种策略(策略其实就是业务逻辑),不同的条件对应着不同的业务逻辑,当一个方法中的业务逻辑判断太多时,维护性自然就变差,并且当需要增加或者删除业务逻辑分支时,也需要修改源码,不符合开闭原则。同样的,这里的策略也无法应用到其他的相似的业务逻辑中。

策略模式的解法

策略模式中会有一个抽象的策略类,每一个if-else逻辑分支下的业务逻辑可以抽取出来形成一个具体策略类,形成一个业务领域下的算法族。结合工厂模式,使用策略工厂获取对应的策略交由客户端使用。

策略模式的缺点

未完待续...

posted @ 2020-10-01 16:33  Ging  阅读(367)  评论(0)    收藏  举报