Java开发大厂面试第28讲:你知道哪些设计模式?分别对应的应用场景有哪些?它们有哪些优缺点?

70 篇文章 0 订阅

上一课时我们讲了单例模式的 8 种实现方式以及它的优缺点,可见设计模式的内容是非常丰富且非常有趣。我们在一些优秀的框架中都能找到设计模式的具体使用,比如前面 MyBatis 中(第 13 课时)讲的那些设计模式以及具体的使用场景,但由于设计模式的内容比较多,有些常用的设计模式在 MyBatis 课时中并没有讲到。因此本课时我们就以全局的视角,来重点学习一下这些常用设计模式。

今天我们分享的面试题是,你知道哪些设计模式?它的使用场景有哪些?它们有哪些优缺点?

设计模式是在软件开发中解决常见问题的最佳实践。以下是一些常见的设计模式及其使用场景、优点和缺点:

1. 单例模式(Singleton Pattern)

使用场景

  • 当类只能有一个实例,并且客户可以从一个众所周知的访问点访问它时。
  • 例如,数据库连接池、配置管理类、线程池等。

优点

  • 提供了对唯一实例的受控访问。
  • 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。

缺点

  • 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
  • 单例类的职责过重,在一定程度上违背了“单一职责原则”。
  • 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。

2. 工厂模式(Factory Pattern)

使用场景

  • 当一个类不知道它所必须创建的对象的类的时候。
  • 当一个类希望由它的子类来指定它所创建的对象的时候。
  • 当类将创建对象的职责委托给多个帮助子类中的某一个,并且你希望将哪一个帮助子类是代理者这一信息局部化的时候。

优点

  • 一个调用者想创建一个对象,只要知道其名称就可以了。
  • 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
  • 屏蔽产品的具体实现,调用者只关心产品的接口。

缺点

  • 每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。

3. 建造者模式(Builder Pattern)

使用场景

  • 当一个复杂对象的构建与它的表示分离以使得同样的构建过程可以创建不同的表示时。
  • 当构建过程必须允许被构建的对象有不同的表示时。

优点

  • 建造者独立,易扩展。
  • 便于控制细节风险。

缺点

  • 产品的组成部分必须相同,这限制了其使用范围。
  • 如果产品的内部变化复杂,该模式会增加很多的建造类。

4. 原型模式(Prototype Pattern)

使用场景

  • 当一个系统应该独立于它的产品创建、构成和表示时。
  • 当要实例化的类是在运行时刻指定时,例如,通过动态加载。
  • 为了避免创建一个与现有产品类似的新对象而需要复制现有产品时。

优点

  • 性能优良。
  • 逃避构造函数的约束。

缺点

  • 需要为每一个类配备一个克隆方法。
  • 对已有的类进行修改需要增加相应的克隆方法,增加了额外的开发量。

5. 适配器模式(Adapter Pattern)

使用场景

  • 当你想使用一个已经存在的类,而它的接口不符合你的需求时。
  • 当你想要创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类(即那些接口可能不一定兼容的类)一起工作时。

优点

  • 更好的复用性。
  • 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,而无须修改原有代码。

缺点

  • 过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是A接口,其实内部被适配成了B接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。

每种设计模式都有其特定的使用场景和优缺点,在实际应用中需要根据具体情况进行选择和权衡。

典型回答

设计模式从大的维度来说,可以分为三大类:创建型模式、结构型模式及行为型模式,这三大类下又有很多小分类。

创建型模式是指提供了一种对象创建的功能,并把对象创建的过程进行封装隐藏,让使用者只关注具体的使用而并非对象的创建过程。它包含的设计模式有单例模式、工厂模式、抽象工厂模式、建造者模式及原型模式。

结构型模式关注的是对象的结构,它是使用组合的方式将类结合起来,从而可以用它来实现新的功能。它包含的设计模式是代理模式、组合模式、装饰模式及外观模式。

行为型模式关注的是对象的行为,它是把对象之间的关系进行梳理划分和归类。它包含的设计模式有模板方法模式、命令模式、策略模式和责任链模式。

下面我们来看看那些比较常见的设计模式的定义和具体的应用场景。

1. 单例模式

单例模式是指一个类在运行期间始终只有一个实例,我们把它称之为单例模式

单例模式的典型应用场景是 Spring 中 Bean 实例,它默认就是 singleton 单例模式。

单例模式的优点很明显,可以有效地节约内存,并提高对象的访问速度,同时避免重复创建和销毁对象所带来的性能消耗,尤其是对频繁创建和销毁对象的业务场景来说优势更明显。然而单例模式一般不会实现接口,因此它的扩展性不是很好,并且单例模式违背了单一职责原则,因为单例类在一个方法中既创建了类又提供类对象的复合操作,这样就违背了单一职责原则,这也是单例模式的缺点所在。

2. 原型模式

原型模式属于创建型模式,它是指通过“克隆”来产生一个新的对象。所以它的核心方法是 clone(),我们通过该方法就可以复制出一个新的对象。

在 Java 语言中我们只需要实现 Cloneable 接口,并重写 clone() 方法就可以实现克隆了,实现代码如下:

public class CloneTest {
    public static void main(String[] args) throws CloneNotSupportedException {
        // 创建一个新对象
        People p1 = new People();
        p1.setId(1);
        p1.setName("Java");
        // 克隆对象
        People p2 = (People) p1.clone();
        // 输出新对象的名称
        System.out.println("People 2:" + p2.getName());
    }
    static class People implements Cloneable {
        private Integer id;
        private String name;
        /**
         * 重写 clone 方法
         */
        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
        public Integer getId() {
            return id;
        }
        public void setId(Integer id) {
            this.id = id;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
    }
}

程序的执行结果为:

People 2:Java

但需要注意的是,以上代码为浅克隆的实现方式,如果要实现深克隆(对所有属性无论是基本类型还是引用类型的克隆)可以通过以下手段实现:

  • 所有对象都实现克隆方法;
  • 通过构造方法实现深克隆;
  • 使用 JDK 自带的字节流实现深克隆;
  • 使用第三方工具实现深克隆,比如 Apache Commons Lang;
  • 使用 JSON 工具类实现深克隆,比如 Gson、FastJSON 等。

具体的实现代码可以参考我们第 07 课时的内容。

原型模式的典型使用场景是 Java 语言中的 Object.clone() 方法,它的优点是性能比较高,因为它是通过直接拷贝内存中的二进制流实现的复制,因此具备很好的性能。它的缺点是在对象层级嵌套比较深时,复制的代码实现难度比较大。

3. 命令模式

命令模式属于行为模式的一种,它是指将一个请求封装成一个对象,并且提供命令的撤销和恢复功能。说得简单一点就是将发送者、接收者和调用命令封装成独立的对象,以供客户端来调用,它的具体实现代码如下。

接收者的示例代码:

// 接收者
class Receiver {
    public void doSomething() {
        System.out.println("执行业务逻辑");
    }
}

命令对象的示例代码:

// 命令接口
interface Command {
    void execute();
}
// 具体命令类
class ConcreteCommand implements Command {
    private Receiver receiver;
    public ConcreteCommand(Receiver receiver) {
        this.receiver = receiver;
    }
    public void execute() {
        this.receiver.doSomething();
    }
}

请求者的示例代码:

// 请求者类
class Invoker {
    // 持有命令对象
    private Command command;
    public Invoker(Command command) {
        this.command = command;
    }
    // 请求方法
    public void action() {
        this.command.execute();
    }
}

客户端的示例代码:

// 客户端
class Client {
    public static void main(String[] args) {
        // 创建接收者
        Receiver receiver = new Receiver();
        // 创建命令对象,设定接收者
        Command command = new ConcreteCommand(receiver);
        // 创建请求者,把命令对象设置进去
        Invoker invoker = new Invoker(command);
        // 执行方法
        invoker.action();
    }
}

Spring 框架中的 JdbcTemplate 使用的就是命令模式,它的优点是降低了系统的耦合度,新增的命令可以很容易地添加到系统中;其缺点是如果命令很多就会造成命令类的代码很长,增加了维护的复杂性。

考点分析

对于设计模式的掌握程度来说,一般面试官都不会要求你要精通所有的设计模式,但需要对几个比较常用的设计模式有所理解和掌握才行。本课时介绍了 3 种设计模式加上 MyBatis 那一课时介绍的 7 种设计模式,足以应对日常的工作和一般性面试了。

和此知识点相关的面试题还有,软件中的六大设计原则是什么?这也是面试中经常会问的面试题,同时也是优秀程序设计的指导思想。

知识扩展:六大设计原则

六大设计原则包括:单一职责原则、里氏替换原则、依赖倒置原则、接口隔离原则、迪米特法则、开闭原则,接下来我们一一来看看它们分别是什么。

1. 单一职责原则

单一职责是指一个类只负责一个职责。比如现在比较流行的微服务,就是将之前很复杂耦合性很高的业务,分成多个独立的功能单一的简单接口,然后通过服务编排组装的方式实现不同的业务需求,而这种细粒度的独立接口就是符合单一职责原则的具体实践。

2. 开闭原则

开闭原则指的是对拓展开放、对修改关闭。它是说我们在实现一个新功能时,首先应该想到的是扩展原来的功能,而不是修改之前的功能。

这个设计思想非常重要,也是一名优秀工程师所必备的设计思想。至于为什么要这样做?其实非常简单,我们团队在开发后端接口时遵循的也是这个理念。

随着软件越做越大,对应的客户端版本也越来越多,而这些客户端都是安装在用户的手机上。因此我们不能保证所有用户手中的 App(客户端)都一直是最新版本的,并且也不能每次都强制用户进行升级或者是协助用户去升级,那么我们在开发新功能时,就强制要求团队人员不允许直接修改原来的老接口,而是要在原有的接口上进行扩展升级。

因为直接修改老接口带来的隐患是老版本的 App 将不能使用,这显然不符合我们的要求。那么此时在老接口上进行扩展无疑是最好的解决方案,因为这样我们既可以满足新业务也不用担心新加的代码会影响到老版本的使用。

3. 里氏替换原则

里氏替换原则是面向对象(OOP)编程的实现基础,它指的是所有引用了父类的地方都能被子类所替代,并且使用子类替代不会引发任何异常或者是错误的出现。

比如,如果把鸵鸟归为了“鸟”类,那么鸵鸟就是“鸟”的子类,但是鸟类会飞,而鸵鸟不会飞,那么鸵鸟就违背了里氏替换原则。

4. 依赖倒置原则

依赖倒置原则指的是要针对接口编程,而不是面向具体的实现编程。也就说高层模块不应该依赖底层模块,因为底层模块的职责通常更单一,不足以应对高层模块的变动,因此我们在实现时,应该依赖高层模块而非底层模块。

比如我们要从 A 地点去往 B 地点,此时应该掏出手机预约一个“车”,而这个“车”就是一个顶级的接口,它的实现类可以是各种各样的车,不同厂商的车甚至是不同颜色的车,而不应该依赖于某一个具体的车。例如,我们依赖某个车牌为 XXX 的车,那么一旦这辆车发生了故障或者这辆车正拉着其他乘客,就会对我的出行带来不便。所以我们应该依赖是“车”这一个顶级接口,而不是具体的某一辆车。

5. 接口隔离原则

接口隔离原则是指使用多个专门的接口比使用单一的总接口要好,即接口应该是相互隔离的小接口,而不是一个臃肿且庞杂的大接口。

使用接口隔离原则的好处是避免接口的污染,提高了程序的灵活性。

可以看出,接口隔离原则和单一职责原则的概念很像,单一职责原则要求接口的职责要单一,而接口隔离原则要求接口要尽量细化,二者虽然有异曲同工之妙,但可以看出单一职责原则要求的粒度更细。

6. 迪米特法则

迪米特法则又叫最少知识原则,它是指一个类对于其他类知道的越少越好。

迪米特法则设计的初衷是降低类之间的耦合,让每个类对其他类都不了解,因此每个类都在做自己的事情,这样就能降低类之间的耦合性。

这就好比我们在一些电视中看到的有些人在遇到强盗时,会选择闭着眼睛不看强盗,因为知道的信息越少反而对自己就越安全,这就是迪米特法则的基本思想。

最后

今天我们分享了 3 种设计模式:单例模式、原型模式和命令模式,结合 MyBatis 那一课时(第 13 课时)介绍的 7 种设计模式,足以应对日常的工作和一般性的面试了。最后我们还介绍了设计模式中的 6 大设计原则:单一职责原则、开闭原则、里氏替换原则、依赖倒置原则、接口隔离原则和迪米特法则,我们应该结合这些概念对照日常项目中的代码,看看还有哪些代码可以进行优化和改进。


  • 29
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值