1.设计模式概念
1.1 什么地方可以用到设计模式
面向对象(OO)=>功能模块[设计模式+算法(数据结构)]=>框架[使用多种设计模式]=>架构[服务器集群] 复制代码
1.2 使用设计模式的好处
- 使用设计模式,软件具有很好的可扩展性(可以增加新的功能)
- 使用开发模式,具有很好的维护性(可读性、规范性)
1.3 设计模式的目的
- 设计模式是为了让程序,具有更好的代码重复性、可读性(编程规范性)、可扩展性(可维护性)、可靠性、是程序呈现高内聚,低耦合的特征。(模块内部逻辑关系非常紧密,模块与模块之间的关系非常的松散)
- 分享金句:"懂了设计模式,你就懂了面向对象分析和面向对象设计(OOA/OOD)的精要"。
- C++老手与C++新手的区别就是,前者手背上有很多的伤疤。
2.设计模式的七大原则
- 设计模式原则,其实就是程序员在编译时,应当遵守的原则,也就是各种设计模式的基础(即:设计模式为什么这样设计的依据)
设计模式常用的七大原则:
- 单一职责原则
- 接口隔离原则
- 依赖倒转(倒置)原则
- 里氏替换原则
- 开闭原则
- 迪米特原则
- 合成复用原则(在一些地方不写这个原则)
2.1 单一职责原则
- 对于类来说,即一个类应该只负责一项职责。如果A类负责两个不同的职责:职责1、职责2。当职责1发生变化而改变A时,可能会对职责2造成影响使职责2运行错误,所以需要将类A的粒度分解为A1、A2。
- 如果再类中没有满足单一职责原则,在一个类的方法中遵守单一职责原则也是可以的(交通工具)
- 标准的单一职责原则,是在类的级别上进行拆分,而不是方法级别。
- 通常情况下,我们要遵守单一职责原则,只有当逻辑足够简单,才可以在代码级别违反单一职责原则;只有类中的方法数量足够少,可以在方法级别保持单一职责原则。
- 优秀的代码中使用类来区分多个分支,而不使用 if...else if()....else(耦合度高)
2.2 接口隔离原则
- 客户端不应该依赖它不需要接口,即一个类对另一个类的依赖应该建立在最小的接口上。
- 处理方式:将接口Interface拆分为独立的几个接口,类A与类C分别于他们需要的接口建立依赖关系。这就是使用的接口隔离原则。
没有使用接口隔离原则时的实现类图:(此时A、C要实现接口里的所有方法)
使用接口隔离原则时的实现类图:(此时将接口进行了拆分,A此时只需要实现它要使用的方法对应的接口即可,而不用将接口中的方法全部实现)
2.3 依赖倒转(倒置)原则
- 在Java中,抽象是指接口或者抽象类,细节是指具体的实现类。
- 高层模块不应该依赖低层模块,二者都应该依赖其抽象(接口、抽象类)。
- 抽象类不应该依赖细节,细节应该依赖抽象类。
- 依赖倒倒转(倒置)的中心思想是面向接口编程。
依赖倒转原则是基于这样的设计理念:
- 相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比细节为基础的架构要稳定的多。
- 使用接口或者抽象类的目的是制定好规范,而不涉及任何具体的操作,把展示细节的任务交给他们的实现类去完成。
注意:在一个类文件中可以声明其他类、接口,只是这些都不能使用public修饰。但是声明的这些类和方法还是可以被其他的类继承或者实现的。
依赖关系传递的三种方式
- 接口传递
- 构造方法传递
- setter方式传递
依赖原则要注意的地方
- 底层模块尽量都要有抽象类和接口,或者两者都有,程序稳定性更好。
- 变量的声明类型尽量是抽象类和接口,这样我们的变量引用个实际对象间,就曾在一个缓冲层,利于程序的扩展和优化。(就比如你和对象吵架,你先找丈母娘来劝说对象,而不是与对象直接沟通)
- 继承时遵循里氏替换原则。
2.4 里氏替换原则
- 使用继承的时候,父类会对子类进行约束。并且如果父类中的方法发生改变的时候,可能会对所有的子类造成影响。
里氏替换原则
- 里氏替换原则是在1988年麻省理工学院的一个姓李的女士提出的。
- 所有引用基类的地方必须先是透明的使用其子类的对象。
- 在继承中,遵循里氏替换原则,在子类中尽量不在重写父类的方法。
- 里氏替换原则告诉我们,继承实际上让两个类耦合性增强了,在适当的情况下,可以通过聚合、组合、依赖来解决问题。
解决问题的办法
- 原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合、组合等关系替代。
2.5 开闭原则(ocp原则)
- 开闭原则是编程中最基础、最重要的设计原则。
- 一个软件的实体如类、模块和函数应该对扩展开放(针对提供方),对修改关闭(对使用者)。
- 用抽象构建架构,用实现扩展细节。
- 当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现。
- 编程中遵循其他原则,以及使用设计模式的的目的就是遵循开闭原则。
2.6 迪米特法则
- 一个对象应该对其他对象保持最少的了解。
- 类与类关系越密切,耦合度越大。
- 迪米特法则又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于依赖的类不管多么的复杂,都尽量将逻辑封装在类的内部。对外除了了提供public 方法,不对外泄露任何信息。
- 迪米特法则还有个人更简单的定义:只与直接的朋友通信。
- 直接的朋友: 每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式有很多,依赖、关联、组合、聚合等。其中 ,我们称出现在成员变量、方法参数、方法的返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类不要以局部变量的形式出现在类的内部。
迪米特法则
- 核心:降低类之间的耦合
- 注意:由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间(对象间)耦合关系,并不是要求完全没有依赖关系。
2.7 合成复用原则
- 基本介绍:尽量使用合成/聚合的方式,而不是使用继承。(依赖、聚合、组合)
- 找出应用中可能需要变化之处,把他们独立出来,不要和那些不需要变化的代码混在一起。
- 针对接口编程,而不是针对实现编程。
- 为了交互对象间的松耦合设计而努力
设计模式
1. 说说什么是单例模式
答:单例模式是一种常用的软件设计模式,在应用这个模式时,单例对象的类必须保证只有一个实
例存在,整个系统只能使用一个对象实例。
优点:不会频繁地创建和销毁对象,浪费系统资源。
可能这会需要你手写一个单例模式,这就得自己去学了,因为单例模式有很多种写法,懒汉模式,
饿汉模式,双重检查模式等。懒汉模式就是用的时候再去创建对象,饿汉模式就是提前就已经加载
好的静态static对象,双重检查模式就是两次检查避免多线程造成创建了多个对象。
单例模式有很多种的写法,我总结一下:
(1)饿汉式单例模式的写法:线程安全
(2)懒汉式单例模式的写法:非线程安全
(3)双检锁单例模式的写法:线程安全
2.说说你对代理模式的理解
代理模式是给某一个对象提供一个代理,并由代理对象控制对原对象的引用。
优点:
代理模式能够协调调用者和被调用者,在一定程度上降低了系统的耦合度;
可以灵活地隐藏被代理对象的部分功能和服务,也增加额外的功能和服务。缺点:
由于使用了代理模式,因此程序的性能没有直接调用性能高;
使用代理模式提高了代码的复杂度。
黄牛卖火车票:没有流行网络购票的年代是很喜欢找黄牛买火车票的,因为工作忙的原因,没时间
去买票,然后就托黄牛给你买张回家过年的火车票。这个过程中黄牛就是代理人,火车票就是被代
理的对象。
婚姻介绍所:婚姻介绍所的工作人员,搜集单身人士信息,婚介所的工作人员为这个单身人士找对
象,这个过程也是代理模式的生活案例。对象就是被代理的对象。
注意了,问代理模式的时候,很有可能会问:动态代理。在Spring篇中已经讲述过,如果你把动态
代理讲了后,很有可能还会问什么是静态代理?一个洞一个是静,大致也能才出来,就是中间代理
层使我们手动写的,通常说的代理模式就是静态代理。
3.说说工厂模式
答:简单工厂模式又叫静态工厂方法模式,就是建立一个工厂类,对实现了同一接口的一些类进行
实例的创建。比如,一台咖啡机就可以理解为一个工厂模式,你只需要按下想喝的咖啡品类的按钮
(摩卡或拿铁),它就会给你生产一杯相应的咖啡,你不需要管它内部的具体实现,只要告诉它你
的需求即可。
优点:
工厂类含有必要的判断逻辑,可以决定在什么时候创建哪一个产品类的实例,客户端可以免除
直接创建产品对象的责任,而仅仅“消费”产品;简单工厂模式通过这种做法实现了对责任的分
割,它提供了专门的工厂类用于创建对象;
客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可,对于
一些复杂的类名,通过简单工厂模式可以减少使用者的记忆量;
通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一
定程度上提高了系统的灵活性。
缺点:
不易拓展,一旦添加新的产品类型,就不得不修改工厂的创建逻辑;
产品类型较多时,工厂的创建逻辑可能过于复杂,一旦出错可能造成所有产品的创建失败,不
利于系统的维护。
4.抽象工厂模式
答:抽象工厂模式是在简单工厂的基础上将未来可能需要修改的代码抽象出来,通过继承的方式让
子类去做决定。比如,以上面的咖啡工厂为例,某天我的口味突然变了,不想喝咖啡了想喝啤酒,这个时候如果直
接修改简单工厂里面的代码,这种做法不但不够优雅,也不符合软件设计的“开闭原则”,因为每次
新增品类都要修改原来的代码。这个时候就可以使用抽象工厂类了,抽象工厂里只声明方法,具体
的实现交给子类(子工厂)去实现,这个时候再有新增品类的需求,只需要新创建代码即可。
5.装饰器模式是什么
答:装饰器模式是指动态地给一个对象增加一些额外的功能,同时又不改变其结构。
优点:装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模
式可以动态扩展一个实现类的功能。
装饰器模式的关键:装饰器中使用了被装饰的对象。
比如,创建一个对象“laowang”,给对象添加不同的装饰,穿上夹克、戴上帽子......,这个执行过程
就是装饰者模式。
一句名言:人靠衣裳马靠鞍。都是装饰器模式的生活案列。
6.代理模式和装饰器模式有什么区别?
答:都是结构型模式,代理模式重在访问权限的控制,而装饰器模式重在功能的加强。
7.模板方法模式
答:模板方法模式是指定义一个算法骨架,将具体内容延迟到子类去实现。
优点:
提高代码复用性:将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中;
实现了反向控制:通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,
实现了反向控制并且符合开闭原则。
喝茶茶:烧水----放入茶叶---喝茶。放入的茶叶每个人自己的喜好不一样,有的是普洱、有的是铁观
音等。
每日工作:上班打卡----工作---下班打卡。每个人工作的内容不一样,后端开发的、前端开发、测
试、产品每个人的工作内容不一样。
8.知道享元模式吗?
答:顾名思义就是被共享的单元。享元模式的意图是复用对象,节省内存,前提是享元对象是不可
变对象。具体来讲,当一个系统中存在大量重复对象的时候,如果这些重复的对象是不可变对象,我们就可
以利用享元模式将对象设计成享元,在内存中只保留一份实例,供多处代码引用。这样可以减少内
存中对象的数量,起到节省内存的目的。
典型的使用场景:Integer中cache,就是享元模式很经典的实现。
怎么看起来享元模式和单例模式是一毛一样的?面试官很有可能会继续问:
9.享元模式和单例模式的区别?
答:单例模式是创建型模式,重在只能有一个对象。而享元模式是结构型模式,重在节约内存使
用,提升程序性能。
享元模式:把一个或者多可对象霍村起来,用的时候,直接从缓存里获取。也就是说享元模式不一
定只有一个对象。
10.说说策略模式在我们生活的场景?
答:策略模式是指定义一系列算法,将每个算法都封装起来,并且使他们之间可以相互替换。
优点:遵循了开闭原则,扩展性良好。
缺点:随着策略的增加,对外暴露越来越多。
条条大路通罗马,条条大路通北京。
我们去北京的交通方式(策略)很多,比如说坐飞机、坐高铁、自己开车等方式。每一种方式就可
以理解为每一种策略。
这就是生活中的策略模式
11.知道责任链模式吗?
答:是行为型设计模式之一,其将链中每一个节点看作是一个对象,每个节点处理的请求均不同,
且内部自动维护一个下一节点对象。当一个请求从链式的首端发出时,会沿着链的路径依次传递给
每一个节点对象,直至有对象处理这个请求为止。
优点
解耦了请求与处理;
请求处理者(节点对象)只需关注自己感兴趣的请求进行处理即可,对于不感兴趣的请求,直
接转发给下一级节点对象;
具备链式传递处理请求功能,请求发送者无需知晓链路结构,只需等待请求处理结果;
链路结构灵活,可以通过改变链路结构动态地新增或删减责任;
易于扩展新的请求处理类(节点),符合 开闭原则;
缺点责任链路过长时,可能对请求传递处理效率有影响;
如果节点对象存在循环引用时,会造成死循环,导致系统崩溃;
生活案列:我们在公司内部发起一个OA审批流程,项目经理审批、部门经理审批。老板审批、人力
审批。这就是生活中的责任链模式,每个角色的责任是不同。
SpringMVC中的拦截器和Mybatis中的插件机制,都是拦截器经典实现。
12.了解过适配器模式么?
答:适配器模式是将一个类的接口变成客户端所期望的另一种接口,从而使原本因接口不匹配而无
法一起工作的两个类能够在一起工作。
优点:
可以让两个没有关联的类一起运行,起着中间转换的作用;
灵活性好,不会破坏原有的系统。
缺点:过多地使用适配器,容易使代码结构混乱,如明明看到调用的是 A 接口,内部调用的却是 B
接口的实现。
生活中的插座,为了适应各种插头,然后上面有两个孔的,三个空的,基本都能适应。还有万能充
电器、USB接口等。这些都是生活中的适配器模式。
13.知道观察者模式吗?
答:观察者模式是定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式又叫做发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。 优点:观察者模式可以实现表示层和数据逻辑层的分离,并定义了稳定的消息更新传递机制,抽象了更新接口,使得可以有各种各样不同的表示层作为具体观察者角色;观察者模式在观察目标和观察者之间建立一个抽象的耦合;观察者模式支持广播通信;观察者模式符合开闭原则(对拓展开放,对修改关闭)的要求。
缺点:
如果一个观察目标对象有很多直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间;如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃;观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。在观察者模式中有如下角色:
Subject:抽象主题(抽象被观察者),抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象;
ConcreteSubject:具体主题(具体被观察者),该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知;
Observer:抽象观察者,是观察者者的抽象类,它定义了一个更新接口,使得在得到主题更改通知时更新自己;
ConcrereObserver:具体观察者,实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态。
在Spring中大量的使用的观察者模式,只要看到是以Event结尾或者Publish开头的基本上都是观察者模式。
上面一共说了11种设计模式,这些设计模式能应对绝大多数人和面试场景来说。建议私下自己用代码实现一番,便于更好地理解。