本人本科毕业,21届毕业生,一年工作经验,简历专业技能如下,现根据简历,并根据所学知识复习准备面试。
记录日期:2022.1.4
大部分知识点只做大致介绍,具体内容根据推荐博文链接进行详细复习。
文章目录
设计模式 - 七大原则
这部分我之前的学习,是通过博客,以及《Spring 5核心原理与30个类手写实战》这本书学习的,这本书前面会介绍设计模式多一点。
当然比较推荐的是《Java设计模式》。
有一篇比较详细的博客链接参考:史上最全设计模式导学目录(完整版)
前言
学习方法
首先,我从本人的角度来说一下设计模式的学习方法。
- 看文章
首先,是阅读大师们整理的博客,23种设计模式的demo通通都得了解一遍,了解它们的设计思想。
- 实战
当然看了demo不能说你已经明白了,在很多java源码中,比如jdk中的io流使用了装饰者模式、jdbcTemplate使用的模板模式、log4j使用了门面模式、springMVC的责任链模式等…很多都需要在平时代码的阅读中积累应用场景,在平时的开发思想中应用进去,这才是最重要的,在很多面试中你应该做到能够从一个设计模式,谈到它的实际应用场景,比如说源码中的应用,或者在什么业务场景下的解决方式,能做到侃侃而谈就是真正的理解它。
文章内容
下面我在介绍设计模式的时候,会着重讲解面试容易问的,比如单例的几种方式手写等,还有就是着重讲在哪些源码中有用到,而对应设计模式的具体概念不会讲太详细,这些应该都是必会的。
学习好处
学设计模式的好处,我觉得是在平时开发代码中,对代码的重构、以及开始编写前,就应该有大体的框架,在后期需求中,能保证你的代码拥有拓展性。
设计模式七大原则
七大原则比六大原则多了一个单一职责原则。
面向对象设计原则为支持可维护性复用而诞生,这些原则蕴含在很多设计模式中,它们是从许多设计方案中总结出的指导性原则。
最常见的7种面向对象设计原则如下所示:
- 单一职责原则(Single Responsibility Principle, SRP)
- 开闭原则(Open-Closed Principle, OCP)
- 里氏代换原则(Liskov Substitution Principle, LSP)
- 依赖倒转原则(Dependence Inversion Principle, DIP)
- 接口隔离原则(Interface Segregation Principle, ISP)
- 合成复用原则(Composite Reuse Principle, CRP)
- 迪米特法则(Law of Demeter, LoD)
单一职责原则
单一职责原则,是指一个类只负责一个功能,不要存在多余一个导致类变更的原因
。
假设一个类负责两个职责,修改一个可能会影响另一个功能发生故障。只负责一个类可降低类的复杂度,提高类的可读性,提高系统的可维护性,降低变更引起的风险。
单一职责原则是实现高内聚、低耦合
的指导方针,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离。
举例说明
一般一个对象可以分为属性和行为二部分,所以在类的设计时,我们一般把对象的属性抽象成一个BO(Business Object,业务对象),把对象的行为抽象成一个Biz(Business Logic,业务逻辑)。
我们经常会管理一个系统的用户信息,比如修改一个用户的信息(密码,用户名),删除用户信息,查询用户集合等等…下面模拟写一个UserInfo
类,就是实现此功能的:
class UserInfo {
// 用户操作类
public Long id;
public String password;
public String username;
public void setUserPassword(String password) {
System.out.println("这是修改密码的方法");
}
public void setUserUsername(String username) {
System.out.println("这是修改用户名的方法");
}
public Boolean deleteUser(Long userId) {
System.out.println("根据id删除用户的方法");
return Boolean.TRUE;
}
public List<UserInfo> listUsers() {
System.out.println("查询用户集合的方法");
return Collections.EMPTY_LIST;
}
}
如果一个用户的属性发生改变(密码,名字),或者添加,删除用户都会导致类的改变,也就是说此类没有把用户的属性和用户的行为分开,导致了在有用户的属性和用户的行为变化时,UserInfo
类也会改变。这就违反了我们的单一职责原则(应该有且仅有一个原因引起类的变更)。
我们就按照把用户信息重新抽象成二个接口,一个UserModel
接口负责处理用户的属性,一个UserService
接口负责处理用户的行为,这样用户属性改变,只会导致UserModel
接口改变,用户的行为改变,只会导致UserService
接口改变,这样也就更符合单一职责原则。
// 用户属性操作类
class UserModel {
public Long id;
public String password;
public String username;
public void setUserPassword(String password) {
System.out.println("这是修改密码的方法");
}
public void setUserUsername(String username) {
System.out.println("这是修改用户名的方法");
}
}
// 用户行为操作类
class UserModel {
public Boolean deleteUser(Long userId) {
System.out.println("根据id删除用户的方法");
return Boolean.TRUE;
}
public List<UserInfo> listUsers() {
System.out.println("查询用户集合的方法");
return Collections.EMPTY_LIST;
}
}
优点
- 可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;
- 提高类的可读性,提高系统的可维护性;
- 变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。
开闭原则
参考博客链接:面试被问到如何理解开闭原则
开闭原则,是指对扩展和修改行为的一个原则,指的是软件中的函数、类、模块应该对扩展开放,对修改关闭。
强调的是用抽象构建框架,用实现扩展细节。常用于解决的问题如:更新版本时,尽量在不修改源代码,但增加新功能。
为什么要使用开闭原则(优点)
最基础的设计原则
其它的五个设计原则都是开闭原则的具体形态,也就是说其它的五个设计原则是指导设计的工具和方法,而开闭原则才是其精神领袖。依照java语言的称谓,开闭原则是抽象类,而其它的五个原则是具体的实现类。
提高复用性
在面向对象的设计中,所有的逻辑都是从原子逻辑组合而来,不是在一个类中独立实现一个业务逻辑。只有这样的代码才可以复用,粒度越小,被复用的可能性越大。那为什么要复用呢?减少代码的重复,避免相同的逻辑分散在多个角落,减少维护人员的工作量。那怎么才能提高复用率呢?缩小逻辑粒度,直到一个逻辑不可以分为止。
提高维护性
一款软件量产后,维护人员的工作不仅仅对数据进行维护,还可能要对程序进行扩展,维护人员最乐意的事是扩展一个类,而不是修改一个类。让维护人员读懂原有代码,再进行修改,是一件非常痛苦的事情,不要让他在原有的代码海洋中游荡后再修改,那是对维护人员的折磨和摧残。
面向对象开发的要求
万物皆对象,我们要把所有的事物抽象成对象,然后针对对象进行操作,但是万物皆发展变化,有变化就要有策略去应对,怎么快速应对呢?这就需要在设计之初考虑到所有可能变化的因素,然后留下接口,等待“可能”转变为“现实”。
如何使用开闭原则
抽象约束
抽象是对一组事物的通用描述,没有具体的实现,也就表示它可以有非常多的可能性,可以跟随需求的变化而变化。因此,通过接口或抽象类可以约束一组可能变化的行为,并且能够实现对扩展开放,其包含三层含义:
- 通过接口或抽象类约束扩散,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法。
- 参数类型,引用对象尽量使用接口或抽象类,而不是实现类,这主要是实现里氏替换原则的一个要求。
- 抽象层尽量保持稳定,一旦确定就不要修改。
元数据(metadata)控件模块行为
编程是一个很苦很累的活,那怎么才能减轻压力呢?答案是尽量使用元数据来控制程序的行为,减少重复开发。什么是元数据?用来描述环境和数据的数据,通俗的说就是配置参数,参数可以从文件中获得,也可以从数据库中获得。
制定项目章程
在一个团队中,建立项目章程是非常重要的,因为章程是所有人员都必须遵守的约定,对项目来说,约定优于配置。这比通过接口或抽象类进行约束效率更高,而扩展性一点也没有减少。
封装变化
封装变化,也就是受保护的变化,找出预计有变化或不稳定的点,我们为这些变化点创建稳定的接口。
对变化封装包含两层含义:
- 将相同的变化封装到一个接口或抽象类中。
- 将不同的变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或抽象类中。
举例说明
最容易理解的并且符合开闭原则的就是getter、setter
方法了。
我们对于UserModel
类的password
这个成员变量的修改和获取,一般这样写:
// user实体类
class UserModel {
public String password;
}
// 测试输出类
class Test {
public static void main(String[] args) {
UserModel user = new UserModel();
System.out.println(user.password); // 输出密码
}
}
实际开发肯定不是这么写,我们一般写getter、setter
方法时,一般都是这样写的:
// user实体类
class UserModel {
private String password;
// 设置密码
public void setPassword(String password) {
this.password = password;
}
// 修改密码
public String getPassword() {
return password;
}
}
// 测试输出类
class Test {
public static void main(String[] args) {
UserModel user = new UserModel();
System.out.println(user.getPassword()); // 输出密码
}
}
这个时候,我们已经对读取、修改password
变量增加了拓展性,如果说我们现在有两个业务需求:
- 设置密码时长度必须大于等于8位。
- 获取密码时必须使用md5加密。
我们就可以这么修改:
// user实体类
class UserModel {
private String password;
// 设置密码
public void setPassword(String password) {
if (password.length() >= 8) {
this.password = password;
} else {
System.out.println("密码长度太短,修改密码失败");
}
}
// 修改密码
public String getPassword() throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(password.getBytes());
return new BigInteger(1, md.digest()).toString(16);
}
}
通过这样的编码方式提高了代码的拓展性。
里氏替换原则
里氏替换原则,是指一个软件实体如果能适用父类的话,一定也能适用子类。所有能引用父类的地方都能透明的使用子类对象。子类可替换父类对象而使程序逻辑不变。
引申为:子类可扩展父类的方法,但不能覆盖父类原有的功能。
里氏替换原则是继承复用的基石。只有当衍生类可以替换掉基类,软件单位的功能不会受到影响时,基类才能真正的被复用,而衍生类也才能够在基类的基础上增加新的行为。
举例说明
以长方形和正方形为例,正方形是一种特殊的长方形,我们通过代码模拟它们的关系:
// 长方形类
public class Rectangle {
protected long width;
protected long height;
public void setWidth(long width) {
this.width = width;
}
public long getWidth() {
return this.width;
}
public void setHeight(long height) {
this.height = height;
}
public long getHeight() {
return this.height;
}
}
// 正方形类 继承自长方形类
public class Square extends Rectangle {
public void setWidth(long width) {
this.height = width;
this.width = width;
}
public long getWidth() {
return width;
}
public void setHeight(long height) {
this.height = height;
this.width = height;
}
public long getHeight() {
return height;
}
}
我们写一个测试类,编写一个方法,传入长方形实例对象,长度小于宽库的时候自增加长,直到超过宽,代码如下:
public class Test {
public static void main(String[] args) {
Rectangle rectangle = new Rectangle();
rectangle.setHight(3);
rectangle.setWidth(5);
resize(rectangle); // 增加到hight = 6,width = 5
Rectangle square = new Square();
square.setHight(5);
resize(square); // 死循环
}
/** * 长方形的长小于宽的时候自增加长 直到超过宽 */
public static void resize(Rectangle r)
{
while (r.getHeight() <= r.getWidth() )
{
r.setHeight(r.getHeight() + 1);
}
}
}
在上边的测试示例中我们定义了一个长方形和一个继承自长方形的正方形,看着是非常符合逻辑的,但是当我们调用Test类中的resize方法时,长方形是可以的,但是正方形就会一直增大进入死循环。但是如果按照里氏替换原则,父类可以的地方,换成子类一定也可以,所以很明显,上边的这个例子是不符合里氏替换原则的。
所以我们需要按照里氏替换原重构一下这个代码,我们给长方形和正方形定义一个父类(即四边形类),代码如下:
// 四边形类
public abstract class Quadrangle {
// 只有get方法没有set方法,set方法由子类自己实现
protected abstract long getWidth();
protected abstract long getHeight();
}
// 长方形类
public class Rectangle extends Quadrangle {
private long width;
private long height;
public void setWidth(long width) {
this.width = width;
}
public long getWidth() {
return this.width;
}
public void setHeight(long height) {
this.height = height;
}
public long getHeight() {
return this.height;
}
}
// 正方形类
public class Square extends Quadrangle
{
private long width;
private long height;
public void setWidth(long width) {
this.height = width;
this.width = width;
}
public long getWidth() {
return width;
}
public void setHeight(long height) {
this.height = height;
this.width = height;
}
public long getHeight() {
return height;
}
}
在基类Quadrange
类中没有赋值方法,因此类似于resize()
方法不可能适用于Quadrangle
类,而只能适用于不同的具体子类Rectangle
和Aquare
,因此里氏替换原则不可能被破坏了。
总结
- 子类可以实现父类的抽象方法,但不能父类的非抽象方法。
- 子类可增加自己特有的方法。
- 当子类的方法重载父类的方法时,子类的前置参数(入参)相比父类更宽松。
- 当子类的方法重载父类的方法时,子类的后置参数(函数返回值)相比父类更严格或相等。
依赖倒转原则
依赖倒转原则,是指设计系统代码结构时,高层模块不依赖底层模块,它们都应依赖于其抽象。
细节应该依赖抽象。通过依赖倒置,可减少系统之间模块的耦合性,提高系统的稳定性,提高系统的可读性与可维护性,降低修改程序带来的风险。
以抽象为基准设计的架构要比以细节为基准设计的架构稳定很多。所以在拿到需求时,要面向接口编程,先顶层再细节来设计代码结构。
依赖的三种写法,或者说依赖传递的三种方式:
- 构造参数传递依赖对象。
- setter方法传递依赖对象。
- 接口声明依赖对象,也叫 接口注入。
举例说明
以张三开车为例子说明:
// 司机类
public class Driver {
public void drive(BenChi benchi) {
benchi.run();
}
}
// 奔驰车类
public class BenChi {
public void run() {
System.out.println("奔驰车启动...")
}
}
现在我们编写测试类,如下:
public class Test {
Driver zhangsan = new Driver();
BenChi benchi = new BenChi();
zhangsan.drive(benchi);
}
此时张三完成了开奔驰车的方法,那如果此时,出了新的车,比如宝马,奥迪等,张三就需要新增开宝马、开奥迪的方法。
这里就发现了一个高耦合度的地方,就是司机类和奔驰车类是一个紧耦合关系,这里如果每增加一辆车就需要修改司机类,增加了代码的不稳定性,被依赖者的变更或增加(新增宝马车类)却要依赖者(司机类)来承担修改的成本,代码的维护会非常困难。
这样的代码类关系还会引起并行开发的风险,并行开发最大的风险就是风险扩散,本来只是一段程序的错误或异常,逐步波及一个功能,一个模块,甚至最后毁坏整个项目。如果一个团队,20人开发,每个人负责不同的功能模块,甲负责汽车类,乙负责司机类, 在甲没有完成的情况,乙是不能完全编写代码的,缺少汽车类,编译器根本不会让你通过,就更不用说进行单元测试, 在上面那种不使用依赖倒置原则环境中,所有的开发工作都是单线程的,甲做完,乙再做,然后是丙…, 在小项目中是可以的一个人完成所有的代码开发工作,但是在现在的大中型项目中已经是完全不能胜任了;要协作就要并行开发,要并行开发就要解决模块之间的项目依赖关系,使用依赖倒转原则才可以改善。
下面将会优化上面的代码。
先编写司机类。
// 司机接口
public interface Driver{
public void drive(ICar car);
}
// 司机实现类
public class Driver implements Driver{
public void drive(Car car){
car.run();
}
}
再编写汽车类。
// 汽车接口
public interface Car{
public void run();
}
// 奔驰车
public class BenChi implements Car {
public void run(){
System.out.println("奔驰车启动...");
}
}
// 宝马车
public class BaoMa implements Car {
public void run(){
System.out.println("宝马车启动...");
}
}
现在我们编写测试类,代码如下:
public class Test {
public static void main(String[] args) {
Driver zhangsan = new Driver(); // 司机张三
Car benchi = new BenChi(); // 一辆宝马车
zhangsan.drive(benchi); // 张三开奔驰
}
}
上述的代码屏蔽了细节对抽象的影响,在新增加底层模块时,只修改了业务场景类,也就是高层模块,对其他模块如Driver类不需要做任何修改,把”变更“的风险降到最低。
如何使用依赖倒转原则
-
每个类尽量都要有接口或抽象类,或者抽象类和接口两者都具备。
-
变量的显示类型 尽量是接口或者抽象类。
-
任何类都不应该从具体类派生。 这样不是绝对的如,在项目开发阶段要遵从此原则。要是做维护老项目,基本就是做扩展开发,通过继承关系,覆写一个方法就可以修改一个bug,何必要去继承最高的基类内容。
-
尽量不要覆写基类的方法
-
结合里氏替换原则。 父类出现的地方子类就能出现。
总结
- 高层模块不应该依赖低层模块,二者都应该依赖其抽象。
- 抽象(接口/抽象类)不应该依赖细节(类),细节应该依赖抽象。
- 依赖倒转(倒置)的中心思想是面向接口编程。
- 依赖倒转原则是基于这样的设计理念:**相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。**在java中,抽象指的是接口或抽象类,细节就是具体的实现类。
- 使用接口或抽象类的目的是:制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。
接口隔离原则
接口隔离原则,是指一个类对另一个类的依赖应该建立在多个最小的接口之上,而不是使用单一的总接口,客户端不应该依赖他不需要的接口
。
接口隔离原则符合我们所说的“高内聚,低耦合”
的思想,从而使类具有很好的可维护性、可读性、可扩展性。我们在设计接口时,要多花时间去思考业务模型,包括以后可能要修改的还要去做一些预判。所以,对于抽象,对业务模型的理解是最重要的。
一张图其实就能说明:
举例说明
比如说张三是一个会跳舞的飞行员,它的属性就能分为人、跳舞、飞行员三点。
我们通过代码实现:
// 人接口
interface IPersion {
void eat(); // 吃 接口
void fly() // 开飞机 接口
void dance(); // 跳舞 接口
void sleep(); // 睡觉 接口
void sing(); // 唱歌 接口
}
// 张三类
class Zhangsan implements IPersion {
public void eat() {
System.out.println("会吃饭");
}
public void fly() {
System.out.println("会开飞机");
}
public void dance() {
System.out.println("会跳舞");
}
public void sleep() {
System.out.println("会睡觉");
}
public void sing() {
throw new RuntimeException("不会唱歌!");
}
}
可以看到,如果一个接口过于臃肿,只要接口中出现的方法,不管对依赖于它的类有没有用处,实现类中都必须去实现这些方法,这显然不是好的设计。
如果将这个设计修改为符合接口隔离原则,就必须对接口进行拆分。在这里我们将原有的接口拆分为四个接口,拆分后的代码设计如下:
// 人接口
interface Persion {
void eat(); // 吃 接口
void sleep(); // 睡觉 接口
}
// 舞者接口
interface Dancer {
void dance(); // 跳舞 接口
}
// 飞行员接口
interface Flyer {
void fly() // 开飞机 接口
}
// 歌手接口
interface Singer {
void sing(); // 唱歌 接口
}
// 张三类
class Zhangsan implements Persion, Dancer, Flyer {
public void eat() {
System.out.println("会吃饭");
}
public void fly() {
System.out.println("会开飞机");
}
public void dance() {
System.out.println("会跳舞");
}
public void sleep() {
System.out.println("会睡觉");
}
}
接口隔离原则的含义是:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。在本例中,将一个庞大的接口变更为四个专用的接口所采用的就是接口隔离原则。
在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的“契约”,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
接口是对类的约束。
如何使用接口隔离原则
- 一个类对一个类的依赖应该建立在最小的基础上。
- 建立单一接口,不应建立臃肿的接口。
- 细化接口功能,每个接口内方法要尽量少(不是越少越好,要适度)。
总结
- 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
- 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
- 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
合成复用原则
合成复用原则,是指尽量使用对象组合、聚合,而不是继承来实现软件复用的目的
。这样可以使类更加灵活、降低类与类之间的耦合度。
- 继承复用也叫白箱复用,会将父类的实现细节全部暴露给子类。
- 合成复用也叫黑箱复用,对类以外是无法获取到实现细节的。
使用继承复用虽然有简单和易实现的优点,但仍会存在以下缺点:
- 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
- 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
- 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
举例说明
写一个合成复用的例子。
比如说一个工具箱中,有扳手、钳子、螺丝刀等,通过代码实现如下:
首先编写各个工具的代码。
// 工具接口
interface Util {
void use();
}
// 扳手
class BanShou implements Util {
public void use() {
System.out.println("使用扳手");
}
}
// 钳子
class Qianzi implements Util {
public void use() {
System.out.println("使用钳子");
}
}
// 螺丝刀
class LuoSiDao implements Util {
public void use() {
System.out.println("使用螺丝刀");
}
}
我们再来编写工具箱的代码。
// 工具箱
class UtilBox {
BanShou banshou;
Qianzi qianzi;
LuoSiDao luosidao;
public UtilBox(BanShou banshou, Qianzi qianzi, LuoSiDao luosidao) {
this.banshou = banshou;
this.qianzi = qianzi;
this.luosidao = luosidao;
}
// 使用工具箱的方法
public void useThem() {
banshou.use();
qianzi.use();
luosidao.use();
}
}
这就是一种合成复用原则的一种体现。
is-a、has-a、like-a的区别
参考博客链接:小猿圈java之is-a、have-a和like-a的区别
is-a
is-a,顾名思义,是一个,代表继承关系。
如果A is-a B,那么B就是A的父类。
一个类完全包含另一个类的所有属性及行为。
例如苹果是水果,香蕉也是水果,苹果和香蕉是两种不同类型的水果,但都继承了水果的共同特性。因此在用Java语言实现时,应该将苹果和香蕉定义成两种类,均继承水果类。
has-a
has-a,顾名思义,有一个,代表从属关系。
如果A has a B,那么B就是A的组成部分。
同一种类的对象,通过它们的属性的不同值来区别。
例如一台PC机的操作系统是Windows,另一台PC机的操作系统是Linux。操作系统是PC机的一个成员变量,根据这一成员变量的不同值,可以区分不同的PC机对象。
like-a
like-a,顾名思义,像一个,代表组合关系。
如果A like a B,那么B就是A的接口。
新类型有老类型的接口,但还包含其他函数,所以不能说它们完全相同。
例如一台手机可以说是一个微型计算机,但是手机的通讯功能显然不是计算机具备的行为,所以手机继承了计算机的特性,同时需要实现通讯功能,而通讯功能需要作为单独接口,而不是计算机的行为。
三者的区别
如果你确定两件对象之间是is-a的关系,那么此时你应该使用继承;比如菱形、圆形和方形都是形状的一种,那么他们都应该从形状类继承。
如果你确定两件对象之间是has-a的关系,那么此时你应该使用聚合;比如电脑是由显示器、CPU、硬盘等组成的,那么你应该把显示器、CPU、硬盘这些类聚合成电脑类。
如果你确定两件对象之间是like-a的关系,那么此时你应该使用组合;比如空调继承于制冷机,但它同时有加热功能,那么你应该把让空调继承制冷机类,并实现加热接口。
优点
-
新对象存取成分对象的唯一方法是通过成分对象的接口。
-
这种复用是黑箱复用,因为成分对象的内部细节是新对象看不见的。
-
这种复用支持包装。
-
这种复用所需的依赖较少。
-
每一个新的类可以将焦点集中到一个任务上。
-
这种复用可以再运行时间内动态进行,新对象可以动态地引用与成分对象类型相同的对象。
迪米特法则
迪米特法则,是指一个对象应该保持对其他对象最少的了解
,也叫最少了解法则,尽量降低类与类之间的耦合。它强调之和朋友交流,不和陌生人说话。如类中的成员变量、函数参数、函数返回值都是朋友,函数内部的对象是陌生人。
举例说明
一个黑帮Boss,雇佣杀手去杀一个普通人,使用代码实现:
// 普通人
class Person{
public String name;
}
// 黑帮老大
class Boss{
// 命令去杀指定人物
public void comandToKill(Person someone)
{
Killer killer = new Killer();
killer.kill(someone);
}
}
// 杀手协会(待使用)
class KillerManager{
private Killer killer;
}
// 杀手
class Killer{
// 猎杀人物
public void kill(Person someone)
{
System.out.println(someone.name+"被杀死了");
}
}
此时我们发现,黑帮老大和杀手存在耦合的联系,但是黑帮老大不应该直接去找杀手击杀指定目标,他应该要通过联系杀手协会去击杀指定目标,那我们的代码重新改动一下:
// 普通人
class Person{
public String name;
}
// 黑帮老大
class Boss{
KillerManager killerManager;
// 命令去杀指定人物
public void comandToKill(Person someone)
{
killerManager.kill(someone);
}
}
// 杀手协会
class KillerManager{
private Killer killer; // 一名协会中的杀手
// 委派杀手去猎杀人物
public void kill(Person someone)
{
System.out.println("委派一名杀手去执行任务");
killer.kill(someone);
}
}
// 杀手
class Killer{
// 猎杀人物
public void kill(Person someone)
{
System.out.println(someone.name+"被杀死了");
}
}
上述设计显然会更符合实际情况,并且这样做也是符合迪米特法则的,对于黑帮老大来说,杀手是黑帮老大的陌生人,所以黑帮老大不应当和杀手产生耦合关系,而应当是通过杀手协会去和杀手交接。
如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,如果其中的一个类需要调用另一个类的一个方法的话,可以通过第三者转发这个调用。
如何界定陌生人和朋友?
在代码中,类与类之间的朋友,是满足下面的条件之一的:
-
当前对象本身(this)。
-
以参量形式传入当前对象方法中的对象。
-
当前对象的实例变量直接引用的对象。
-
当前对象的实例变量如果是一个聚集,那么聚集中的元素也都是朋友。
-
当前对象所创建的对象。
满足上面条件之一就是朋友,否则就是陌生人。
记住,这里的朋友定义,可不是朋友的朋友就是朋友。在我们的设计中,和自己的朋友打交道即可,别管陌生人。
如何使用迪米特法则
强调两点:
- 从依赖者的角度来说,只依赖应该依赖的对象。
- 从被依赖者的角度说,只暴露应该暴露的方法。
运用时注意一下六点:
- 在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标。
- 在类的结构设计上,尽量降低类成员的访问权限。
- 在类的设计上,优先考虑将一个类设置成不变类。
- 在对其他类的引用上,将引用其他对象的次数降到最低。
- 不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。
- 谨慎使用序列化(Serializable)功能。
优点
- 降低了类之间的耦合度,提高了模块的相对独立性。
- 由于亲合度降低,从而提高了类的可复用率和系统的扩展性。
缺点
产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。所以,在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。