设计模式—初识设计模式

1.设计模式经典面试题

        分析几个常见的设计模式对应的面试题。

1.1原型设计模式

        1.使用UML类图画出原型模式核心角色(意思就是使用会考察使用UML画出设计模式中关键角色和关系图等)

        2.原型设计模式的深拷贝和浅拷贝是什么,写出深拷贝的两种方式的源码(重写clone方法实现深拷贝,使用序列化实现深拷贝)=> 考察对原型模式细节的理解。

        3.Spring框架中哪里需要进行使用原型模式 => 分析框架中设计模式的体现。

        4.分析代码+Debug => Debug水平如何?会分析设计模式代码嘛?

1.2解释器设计模式

1.2.1解释器设计模式本身的理解

        1.介绍解释器模式是什么?=> 其实就是看对这个设计模式的理解如何。

        2.画出解释器设计模式的UML类图,分析其中的各个角色是什么?=> 其实就是在考察会不会进行画原型图,会不会通过原型图去分析其中的整体结构?

1.2.2解释器设计模式在Spring框架的理解

        1.请说明Spring的框架中,哪里使用到了解释器模式,进行源码级分析 => 其实就是进行考察对框架中一些关键功能的实现,其中的设计模式理解的如何。

        2.对于解释器模式在Spring框架中的应用有哪些?

Spring框架中SpelExpressionParser使用的就是解释器模式。

代码分析+Debug源码+说明。

1.3单例设计模式

        单例设计模式一共有多少种实现方式?请分别使用代码进行实现,并分析其中的优缺点。

        1.饿汉式 => 两种。

        2.懒汉式 => 三种。

        3.双重检查。

        4.静态内部类。

        5.枚举。

        所以说单例设计模式一共八种。

1.4场景题

1.4.1问题再现

        拼多多抢单项目:拼多多抢单的订单,有审核-发布-抢单等步骤,随着操作的不同,会改变订单的状态,项目中这个抢单的模块就是使用状态模式进行设计的,请你使用状态模式进行设计一下。

        其实其中就是给定一个场景,并提示/不提示给你使用什么设计模式进行实现,在有限的时间内思考出问题的解决方案。

1.4.2问题解决

        如果使用if-else进行判断各种状态,会显得整体代码十分臃肿,不便于维护,如果在其中发现哪个状态没有进行处理之类的,就会出现问题,所以可以进行使用状态设计模式来进行解决。

2.设计模式中的七大原则

        设计模式在进行设计的时候,均需要进行遵循七大OOP原则来进行设计。

        1.单一职责原则。

        2.接口隔离原则。

        3.依赖倒转原则。

        4.里氏替换原则。

        5.开闭原则OCP。

        6.迪米特法则。

        7.合成复用原则。

3.设计模式的重要性

3.1软件工程中的解决方案

        设计模式是一种解决方案,是对软件设计中普遍存在的问题进行提出的一种解决方案,是前人的经验的智慧,可以进行解决软件工程中的一些特定问题。

3.2使用设计模式架构的好处

        设计模式可以为软件工程带来良好的架构体系,不注意软件架构设计的项目,草草收工的项目就想一个简易的房子,没有进行良好的设计,就肯定不会建立成为一个高楼大厦。

        借助设计模式对整个项目进行良好的架构设计,才可以建立起坚固的地基,最终可以依据坚固的地基,架构起高楼大厦。

3.3设计模式便于扩展新功能

        一个项目需要进行扩展新功能的时候,如果没有良好的软件架构设计,那么将是十分艰难的,很有可能添加了这个功能,别的地方就会出现问题了,需要到处改正。

3.4设计模式可以提供可维护性

        如果进行胡乱的设计堆砌代码,不注重设计整个项目,只会导致整个项目可读性越来越差,可维护性越来越差,最终导致这个项目变成一个破烂玩意,烂尾楼。

        使用设计模式进行良好架构之后,项目的整体可读性,可维护性可以提高。

3.5设计模式解决面试问题

        两个人在学历条件等同或者上下浮动一级的情况下,你设计模式玩的六六的,他设计模式一窍不通,当然是你可以取得offer,抱的Offer归。

3.6设计模式在软件中的引用

        1.进行面向对象(OOP)设计时可以进行使用。

        2.进行设计功能模块时 => 可以使用设计模式+算法+数据结构的集合。

        3.框架(使用到多种设计模式)

        4.架构(架构整个软件设计,使得软件设计变得更为合理)

3.7设计模式的目的

        1.代码可用性(相同的代码一次编写即可,无需进行多次编写)

        2.可读性(提高代码的可读性)

        3.可扩展性(增加新功能时会非常方便,不会导致增加新功能的时候对其它功能有很大的影响)

        4.可靠性(设计模式使得整个软件工程的可靠性提高)

        5.使得整个项目达到高内聚,低耦合的特性

4.详解设计模式七大原则

4.1单一职责原则

4.1.1什么是单一职责原则

        对于类来说一个类仅仅负责一项职责,如果一个类进行负责两个不同的职责,当因为职责一需要进行改动增强的时候,可能会导致职责二出现问题,所以要对A类进行按职责细分为两个类,使得两个类负责自己的职责,达到类单一职责的目的。

4.1.2单一职责的案例

4.1.2.1不遵循单一职责

        如果一个类的不遵循单一职责,一个方法进行设计为可以进行处理多个职责,那么就会出现问题。

        例如下面这个SingleResposibility类中进行定义了一个action开始出发的方法,可以进行处理任意类型的交通工具进行启动执行,但是你会发现这样进行使用会出现问题。

/*** 单一职责设计模式*/
public class SingleResponsibility {public static void action(Vehicle vehicle) {System.out.println(vehicle.getName() + "在路上行走");}public static void main(String[] args) {Car car = new Car("汽车");action(car);Plant plant = new Plant("飞机");action(plant);}}abstract class Vehicle {protected String name;public abstract String getName();
}class Car extends Vehicle {public Car(String name) {this.name = name;}@Overridepublic String getName() {return name;}}class Plant extends Vehicle {public Plant(String name) {this.name = name;}@Overridepublic String getName() {return name;}
}

        看一下运行结果:

        会发现这个类并没有把多职责问题给处理好,多职责会出现一定的问题。

        并且如果我们再去修改处理多职责的代码,对于另一个职责的处理也会出现问题。

public static void action(Vehicle vehicle) {System.out.println(vehicle.getName() + "在天上飞行");
}

        发现确实出现了一定的问题:

        当我们进行修改想要兼容另一个职责的时候,发现对原来的职责发生了影响,所以这就是当一个类进行处理多职责的时候会出现一定的问题。

4.1.2.2修复遵循单一职责

        将类中的方法进行职责拆分,让每个方法进行具有单一职责的功能,这样就解决了刚刚的问题。

        对于每个类,如果一个类中的方法少,就可以将方法拆分为单一职责的方法;如果一个类中方法比较多,就将这个类拆分为多个遵循单一职责的类,让每个类都具有单一职责,这样就可以解决一定的问题。

/*** 单一职责设计模式*/
public class SingleResponsibility {public static void carAction(Car car) {System.out.println(car.getName() + "在路上行驶");}public static void plantAction(Plant plant) {System.out.println(plant.getName() + "在天上飞行");}public static void main(String[] args) {Car car = new Car("汽车");carAction(car);Plant plant = new Plant("飞机");plantAction(plant);}}

        可以发现这样就没问题啦!

4.1.2.2修复遵循单一职责

        将类中的方法进行职责拆分,让每个方法进行具有单一职责的功能,这样就解决了刚刚的问题。

        对于每个类,如果一个类中的方法少,就可以将方法拆分为单一职责的方法;如果一个类中方法比较多,就将这个类拆分为多个遵循单一职责的类,让每个类都具有单一职责,这样就可以解决一定的问题。

        可以发现这样就没问题啦!

4.1.3单一职责原则注意事项和细节

        1.单一职责原则可以降低复杂度,使得一个类只进行负责一项职责。

        2.提高类的可读性和可维护性 => 很容易理解出来一个类仅仅去负责一个职责。

        3.降低变更引起的风险 => 一个类中如果负责多个职责,当修改A职责的时候,存在影响B职责代码的风险。

        4.遵循类级别的单一职责原则还是方法级别的单一职责原则 => 如果类比较简单的时候,可以在一个类中进行处理多个职责,使用方法去区分每个职责即可;如果类比较复杂,建议进行对类进行拆分为多个具有单一职责的类,这样就可以降低类的复杂性,提高可维护性。

4.2接口隔离原则

4.2.1基本介绍

        客户端不应该依赖它不需要进行依赖的接口,也就是一个类对另一个类的依赖需要进行建立在最小接口上。

4.2.2建模分析

        先进行简单建模分析一下,目前进行定义了一个接口interface1,里面进行定义了五个抽象方法。

        B和D进行实现了interface1中的五个方法。

        A和C通过依赖于interface1接口调用B和D中的方法。

        如果A和C去分别依赖B和D中的方法时,将接口中所有的方法都进行调配使用了,就可以认为是达到了接口隔离原则,类去依赖于其它类的时候建立在了最小接口上。

4.2.3不规范代码分析

4.2.3.1定义的抽象接口

// 抽象接口
interface Interface1 {void operation1();void operation2();void operation3();void operation4();void operation5();
}
4.2.3.2定义的实现类B和实现类D
// 实现类B
class B implements Interface1 {@Overridepublic void operation1() {System.out.println("B进行操作1");}@Overridepublic void operation2() {System.out.println("B进行操作2");}@Overridepublic void operation3() {System.out.println("B进行操作3");}@Overridepublic void operation4() {System.out.println("B进行操作4");}@Overridepublic void operation5() {System.out.println("B进行操作5");}}
// 实现类D
class D implements Interface1 {@Overridepublic void operation1() {System.out.println("D进行操作1");}@Overridepublic void operation2() {System.out.println("D进行操作2");}@Overridepublic void operation3() {System.out.println("D进行操作3");}@Overridepublic void operation4() {System.out.println("D进行操作4");}@Overridepublic void operation5() {System.out.println("D进行操作5");}
}
4.2.3.3定义的依赖类A和依赖类C
// 类A
class A {public void depend1(Interface1 interface1) {interface1.operation1();}public void depend2(Interface1 interface1) {interface1.operation2();}public void depend3(Interface1 interface1) {interface1.operation3();}}// 类C
class C {public void depend1(Interface1 interface1) {interface1.operation1();}public void depend4(Interface1 interface1) {interface1.operation4();}public void depend5(Interface1 interface1) {interface1.operation5();}}
4.2.3.4不规范代码分析

        可以发现B和D两个实现类实现了整个接口中的所有方法,但是A和C两个类进行依赖于两个实现类进行调用方法的时候,并没有进行使用接口中定义的所有方法,都只是依赖了一部分方法,这样就不是建立在最小接口上了,出现了实现方法冗余的情况,打破了接口隔离原则。

4.2.4接口隔离的的改进策略

        为了保证A类依赖B类的时候,建立在最小接口上,需要进行遵循接口隔离原则。

        1.类A通过接口interface1依赖B,类C通过接口interface1依赖类D,如果接口interface1对于A类和C类来说都不是最小接口(接口中存在冗余方法,A和C无法进行利用),那么B类和D类必须去实现A类和C类不需要的方法。

        2.将接口interface1拆分为几个接口,类A和类C分别去和需要的接口建立依赖关系,实现接口隔离原则。

4.2.5遵循接口隔离后的规范代码

        进行按接口隔离的规范进行修改代码。

4.2.5.1定义的抽象接口
// 抽象接口
interface Interface1 {void operation1();
}interface Interface2 {void operation2();void operation3();
}interface Interface3 {void operation4();void operation5();
}
4.2.5.2定义的实现类B和实现类D
// 实现类B
class B implements Interface1, Interface2 {@Overridepublic void operation1() {System.out.println("B进行操作1");}@Overridepublic void operation2() {System.out.println("B进行操作2");}@Overridepublic void operation3() {System.out.println("B进行操作3");}}// 实现类D
class D implements Interface1, Interface3 {@Overridepublic void operation1() {System.out.println("D进行操作1");}@Overridepublic void operation4() {System.out.println("D进行操作4");}@Overridepublic void operation5() {System.out.println("D进行操作5");}
}
4.2.5.3定义的依赖类A和依赖类C
// 类A
class A {public void depend1(Interface1 interface1) {interface1.operation1();}public void depend2(Interface2 interface1) {interface1.operation2();}public void depend3(Interface2 interface1) {interface1.operation3();}}// 类C
class C {public void depend1(Interface1 interface1) {interface1.operation1();}public void depend4(Interface3 interface1) {interface1.operation4();}public void depend5(Interface3 interface1) {interface1.operation5();}}

4.3依赖倒转原则

4.3.1基本介绍

        依赖倒转原则指的是:

        1.高层模块不应该依赖底层模块,二者都应该依赖于抽象。

        2.抽象不应该依赖于细节,细节应该依赖于抽象。

        3.依赖倒转(倒置)的中心思想就是面向接口编程。

        4.依赖倒转基于的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在Java中,抽象指的是接口和抽象类,细节就是具体的实现类。

        5.使用接口或者抽象类的目的时制定好规范,而不涉及到任何具体的操作,吧展现细节的任务交给他们的实现类去完成。

4.3.2建模分析

        下面这个UML类图表示的就是一个非常不规范的结构分析,Person可以进行接收邮件消息和微信消息,但是由于邮件消息和微信消息是两个类,所以Person就进行定义了两个方法进行接收Email/WX消息,但是这个设计不是一个优秀的设计,因为这个设计没有进行遵循依赖倒转原则,没有进行抽象化设计,其实Email和Wx这两个类都可以进行抽象为Message消息类,让实现类都去继承Message消息抽象类,让不同消息类去实现Message抽象类,并自定义消息发送的逻辑。

        Person类去使用一个方法只去接收Message抽象类即可。

4.3.3不规范的代码分析

4.3.3.1两个消息类
class Email {public String getInfo() {return "电子邮件信息: Hello, world";}
}class Wx {public String getInfo() {return "微信信息: 土皇帝牛逼";}
}
4.3.3.2Person使用者类
class Person extends Wx {public void receive(Email email) {System.out.println(email.getInfo());}public void receiveWX(Wx wx) {System.out.println(wx.getInfo());}
}
4.3.3.3不规范设计的总结

        每多一个消息渠道,Person使用者就需要多增加一个代码,这就是不使用抽象去架构的坏处,违反了依赖倒转原则。

4.3.4规范代码的设计

4.3.4.1抽象类Message的架构设计

        使用抽象去架构设计。

abstract class Message {public abstract String getInfo();
}
4.3.4.2各种消息类的架构设计
class Email extends Message {@Overridepublic String getInfo() {return "电子邮件信息: Hello, world";}
}class Wx extends Message {@Overridepublic String getInfo() {return "微信信息: 土皇帝牛逼";}
}
4.3.4.3Person使用者类
class Person {public void receive(Message message) {System.out.println(message.getInfo());}
}
4.3.4.4建模分析

        使用抽象架构设计的方式,让Person类仅仅使用一个方法就可以接收所有按抽象类Message设计的类的对象,增强了架构设计,简化了Person使用的类的冗余性。

4.3.5依赖关系传递的三种方式和应用案例

        1.接口传递依赖关系 => 使用接口向类中进行传递依赖对象。

        2.构造方法传递依赖关系 => 使用构造方法向类中进行传递依赖对象。

        3.setter方法传递依赖关系 => 使用setter方法向类中进行传递依赖对象。

4.3.6通过接口传递依赖

4.3.6.1定义依赖接口
// ITV接口
interface ITV {void play();void stop();
}

4.3.6.2定义操作接口

// 开关的接口
interface IOpenAndClose {void open(ITV tv);void close(ITV tv);
}
4.3.6.3定义实现接口
// 实现接口
class OpenAndClose implements IOpenAndClose {@Overridepublic void open(ITV tv) {tv.play();}@Overridepublic void close(ITV tv) {tv.stop();}
}
4.3.6.4总结使用接口传递依赖

        接口传递依赖是什么呢,其实就是通过实现接口的方式将依赖给注入到类中进行使用。

        通过下面的UML建模图可以观察到其中的玄机:

        ITV其实就是依赖类的抽象接口,IOpenAndClose是动作类的抽象接口,当动作类去实现了抽象接口之后,抽象接口就会通过抽象方法,强制性地将依赖(ITV)注入到实现类中。

4.3.7通过构造方法传递依赖

4.3.7.1定义依赖接口
// ITV接口
interface ITV {void play();void stop();
}
4.3.7.2定义操作接口
// 开关的接口
interface IOpenAndClose {void open();void close();
}
4.3.7.3定义实现接口
// 实现接口
class OpenAndClose implements IOpenAndClose {private ITV tv;public OpenAndClose(ITV tv) {this.tv = tv;}@Overridepublic void open() {tv.play();}@Overridepublic void close() {tv.stop();}
}
4.3.7.4总结使用构造方法传递依赖

        构造方法传递依赖其实就是将依赖对象使用构造方法的形式传递过来在字段中进行存储。

存储到类的字段中后,进行在类内部进行使用。

4.3.8通过Setter方法进行传递

4.3.8.1定义依赖接口
// ITV接口
interface ITV {void play();void stop();
}
4.3.8.2定义操作接口
// 开关的接口
interface IOpenAndClose {void open();void close();
}
4.3.8.3定义实现接口
// 实现接口
class OpenAndClose implements IOpenAndClose {private ITV tv;public void setTv(ITV tv) {this.tv = tv;}@Overridepublic void open() {tv.play();}@Overridepublic void close() {tv.stop();}
}
4.3.8.4总结使用setter方法传递依赖

        setter方法主要是使用setter方法将数据进行注入到类的字段中,在类的内部进行使用。

4.3.9依赖倒转原则的注意事项和细节

1.底层模块尽量都要有抽象类或者接口,或者两者都有,程序稳定性更好。

        也就是说底层的模块最好先使用抽象类或者接口进行抽象设计,再使用底层模块类进行实现,保证了程序的阔拓展性。

2.变量的声明类型尽量是抽象类或者接口,这样我们的变量引用和实际对象间,就存在一个缓冲层,利于程序扩展和优化。

        其实就是例如A是继承自B的,我们要进行实例化的时候,最好不要进行指定实例化A,指定类型是B最好,因为这样可以在变量引用和实际对象间存在一个缓冲层。这个缓冲层可以起到一个便于扩展的作用。

        为什么便于扩展呢?

        因为如果我们想要进行扩展对象的功能的时候,可以不去直接修改A类里面的内容,去扩展B类(父类)的内容,也可以进行达到扩展对象的能力,如果父类的方法达不到要求,可以进行Override重写父类中的方法,使用多态达到重写方法的目的。

        所以多层缓冲可以提高整体的可扩展性和维护性。

3.继承遵循里氏替换原则。

4.4里氏替换原则

4.4.1什么是里氏替换原则

1.继承包含的一层含义(子类不可随意更改父类方法 => 打破契约):

        父类中凡是实现好的方法,实际上是在设定规范和契约,虽然不强制要求所有子类必须遵循这些契约,但是如果子类对这些已经实现的方法随意修改,会给整个继承体系带来巨大的破坏。

2.继承带来的弊端

        继承给程序带来了侵入性,程序的可移植性降低,增加了对象之间的耦合性,如果一个类被其它的类所继承,则当这个类需要进行修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能出现故障。

3.里氏替换原则可以保障正确的使用继承

        里氏替换原则其实就是:子类可以进行直接替换掉基类型,并且替换掉之后不会出现任何问题,即子类替换掉基类后,与基类原本的行为特征表示一致,子类不能随便去重写基类中的方法。

4.4.2违反里氏替换原则的典型

        这个例子就是违反了里氏替换原则,B1可以进行替换掉A1,但是B1重写了A1中的方法,使得自己内部的方法和原本基类中方法行为不一致,违反了里氏替换原则。

/*** 违反里氏替换原则*/
public class RichterSubstitution {public static void main(String[] args) {A1 a1 = new A1();System.out.println("11 - 3 = " + a1.func1(11, 3));System.out.println("---------------------------");B1 b1 = new B1();System.out.println("11 - 3 = " + b1.func1(11, 3));System.out.println("11 + 3 + 9 = " + b1.func2(11, 3));}}class A1 {public int func1(int num1, int num2) {return num1 - num2;}
}class B1 extends A1 {@Overridepublic int func1(int a, int b) {return a + b;}public int func2(int a, int b) {return func1(a, b) + 9;}
}

4.4.3遵循里氏替换原则的方案

4.4.3.1出现问题的原因

        出现问题的原因其实就是因为类B无意重写了父类中的方法,造成了原有功能出现了错误。在实际编程中常常会通过重写父类的方法完成新的功能,这样虽然写起来简单,但是整个继承体系的复用性会变差。特别是当运行堕胎比较频繁的时候。

        所以尽量不要无意重写方法,遵循里氏替换原则,提高继承体系的复用性。

4.4.3.2出现问题的原因

        通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖,聚合,组合等关系代替。

4.4.4遵循里氏替换原则的代码

        准备Base基类,让B1和A1均去继承Base基础类,将A1中不被B1重写的方法都放在Base基类中,这样一方面达到了继承公有方法,提高了代码复用性,第二方面又不会破坏里氏替换原则,保留了继承的复用性。

// 准备一个基类
class Base {// 将更基础的方法和成员写到Base基类中
}// A1类
class A1 extends Base {// 返回两个数的差public int func1(int num1, int num2) {return num1 - num2;}
}class B1 extends Base {public int func1(int a, int b) {return a + b;}public int func2(int a, int b) {return func1(a, b) + 9;}
}

4.5开闭原则 => 最重要的原则

        1.开闭原则是编程中最基础,最重要的设计原则。

        2.一个软件实体如类,模块和函数应该对扩展开放(对提供方),对修改关闭(对使用方)。用抽象构建框架,用实现扩展细节。

        3.当软件需要变化的时候,尽量通过扩展软件实体的行为来实现变化,而不是通过已有代码来实现变化。

        4.编程中遵循其它原则,以及使用设计模式的目的就是遵循开闭原则。

4.5.1违反开闭原则的典型

        很容易可以发现这就是一个违反开闭原则的典型,开闭原则中我们要进行遵守对提供者开放,对使用者关闭的原则,在这里我们区分出1职责,其中提供者就是Shape的子类,Rectangle和Circle,使用者就是GaphicEditor,使用者接收这些类的实例对象的时候,是写死里面的,可以发现drawShape去接收的是Shape类型的实例,但是仅仅定义了两个分支(如果提供者进行增加数据,就会出现问题),并且将不同类型的shape的执行操作定义在了GraphicEditor中,如果提供者进行修改代码,新增一个Shape的实现类就会出现问题,就要去修改GraphicEditor中的代码。

        当我们去修改GraphicEditor中的代码的时候,就违反了开闭原则,因为我们进行改动了使用者中的代码,所以说这段代码违反了开闭原则。

4.5.1.1提供方代码
class Shape {int type;
}class Rectangle extends Shape {public Rectangle() {super.type = 1;}
}class Circle extends Shape {public Circle() {super.type = 2;}
}
4.5.1.2使用方代码
class GraphicEditor {public void drawShape(Shape shape) {if (shape.type == 1) {drawRectangle(shape);} else if (shape.type == 2) {drawCircle(shape);}}public void drawRectangle(Shape shape) {System.out.println("矩形");}public void drawCircle(Shape shape) {System.out.println("圆形");}
}

4.5.2遵循开闭原则的修改方式

        开闭原则要进行保障对使用者关闭,对提供者开放,所以可以将Shape定义为抽象类,提供一个抽象的draw方法,让子类都去实现,这样当又新的图像种类时,仅仅需要让新的图像类去继承Shape,并实现draw方法即可,这样就保证了,只需要去修改提供者,增强提供者,不需要去修改使用者。

        其实就是让使用者aware(感知)不到提供者发生了变化,进行了完美的解耦合。

4.5.3遵循开闭原则的代码

4.5.3.1提供方代码
abstract class Shape {int type;public abstract void draw();
}class Rectangle extends Shape {public Rectangle() {super.type = 1;}@Overridepublic void draw() {System.out.println("矩形");}
}class Circle extends Shape {public Circle() {super.type = 2;}@Overridepublic void draw() {System.out.println("圆形");}
}
4.5.3.2使用方代码
class GraphicEditor {public void drawShape(Shape shape) {shape.draw();}}

4.6迪米特法则

4.6.1什么是迪米特法则

1.一个对象应该对其它对象保持最少的了解

        其实就是设计类的时候,将字段都设置为private私有的,如果想要外部对象进行访问/更改自己内部的状态,使用getter/setter函数即可。

2.类与类关系越密切,耦合度越大

        假设有两个类,类A和类B,如果类A中的字段/方法,依赖于类B中的static字段/实例化对象中的实例字段,就称为类A和类B中有关系,如果这种关系越密切(字段使用越多),则代表类A和类B和耦合度大。

3.迪米特法则 => 最少知道原则

        一个类对依赖自己的类知道的越少越好。也就是说,对于被依赖的类不管多复杂,都要尽量将逻辑封装在类的内部。对外除了进行提供public方法,不要对外泄漏任何信息。尽量将自己对外开放的逻辑都封装在public方法中,不要进行对外暴露更多的信息。

4.迪米特法则更简单的定义 => 仅仅与直接朋友通信。

5.直接的朋友

        什么是直接的朋友?每个对象都会与其它对象有耦合关系,只要两个对象之间有耦合关系,我们就说两个对象之间是朋友关系。

        耦合的方式都有什么?依赖,关联,聚合等。其中我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量的类不是直接的朋友。

        其实意思就是说,不要让陌生的类以局部变量的方式出现在类的内部。

4.6.2违反迪米特法则的典型

4.6.2.1定义的学院员工类和学院管理类

1.学院员工类

/*** 学院员工类*/
public class CollegeEmployee {private String id;public void setId(String id) {this.id = id;}public String getId() {return id;}
}

2.学院管理类

import java.util.ArrayList;
import java.util.List;/*** 管理学院员工的管理类*/
public class CollegeManager {// 返回学院的所有员工public List<CollegeEmployee> getAllEmployee() {List<CollegeEmployee> list = new ArrayList<>();for (int i = 0; i < 10; i++) {CollegeEmployee emp = new CollegeEmployee();emp.setId("学院员工ID = " + i);list.add(emp);}return list;}
}
4.6.2.2定义的学院员工类和学院管理类

1.学校员工类

/*** 学校总部员工*/
public class Employee {private String id;public void setId(String id) {this.id = id;}public String getId() {return id;}
}

2.学院管理类

import java.util.ArrayList;
import java.util.List;// 学校管理类
public class SchoolManager {// 返回学校总部的员工public List<Employee> getAllEmployee() {List<Employee> list = new ArrayList<>();for (int i = 0; i < 10; i++) {Employee emp = new Employee();emp.setId("学校总部员工ID = " + i);list.add(emp);}return list;}// 使用该方法进行输出学校总部和学院总部员工信息(id)void printAllEmployee(CollegeManager sub) {List<CollegeEmployee> list1 = sub.getAllEmployee();System.out.println("------------计算机科学学院的员工-------------");for (CollegeEmployee e : list1) {System.out.println(e.getId());}System.out.println("------------学校总部的员工-------------");List<Employee> list2 = getAllEmployee();for (Employee e : list2) {System.out.println(e.getId());}}
}
4.6.2.3问题所在

        问题主要是出现在printAllEmployee这个方法中,在SchoolManager这个类的的printAllEmployee方法中,进行使用了局部变量List<CollegeEmployee>进行其它类,没有遵循迪米特法则,使用了非直接朋友(局部变量)。

4.6.3问题解决

4.6.3.1解决方案

        解决这个问题十分简单,只需要进行将打印学院管理的员工的这段逻辑,封装抽取到学院类中,在SchoolManager中的printAllEmployee中进行调用学院封装的打印学院管理的员工的逻辑即可,这样就不会不遵循迪米特法则了。

4.6.3.2抽取逻辑到学院类中
public void printAllEmployee() {List<CollegeEmployee> list1 = getAllEmployee();System.out.println("------------计算机科学学院的员工-------------");for (CollegeEmployee e : list1) {System.out.println(e.getId());}
}
4.6.3.3在学校类中调用封装的学院方法
// 使用该方法进行输出学校总部和学院总部员工信息(id)
void printAllEmployee(CollegeManager collegeManager) {collegeManager.printAllEmployee();System.out.println("------------学校总部的员工-------------");List<Employee> list2 = getAllEmployee();for (Employee e : list2) {System.out.println(e.getId());}
}
4.6.3.4这样封装到底解决了什么问题?

        为什么要遵循迪米特法则,将局部变量给取消呢?封装为一个方法有什么好处呢?

        先做一个抽象理解,将SchoolManager抽象为类A,将College抽象为类B,将方法printAllEmployee抽象为方法K。

        其实我们就可以理解A类在K方法中进行局部依赖了B,其实这是一个很不好的行为,如果类B进行发生了改变,那么A的方法势必会受到影响,必须要去更改A中的逻辑,其实这也违反了开闭原则,在这里完全可以将类A抽象为使用者,类B抽象为提供者,提供者发生更改的时候,使用者也需要更改,这样是不是就违反了开闭原则,造成了问题的出现。

        所以说我们将类A依赖类B的逻辑部分,抽象到类B中,定为方法M,让A去调用M,这样如果B发生了变化,只要B能修改M,让M的行为保持不变,就不会影响A类中K方法,做到了下层类修改,上层类无感知的设计模式,真正实现了遵循迪米特法则解耦合。

4.6.4迪米特法则需要注意点

        1.迪米特法则的核心 => 将降低类之间的耦合。

        2.但是注意:由于每个类都减少了不必要的依赖,所以迪米特法则只是要求降低类间(对象间)耦合关系,不是要求做到完全没有依赖关系。

4.7合成复用原则

        合成复用原则(Composite Reuse Principle)

4.7.1什么是合成复用原则

        原则是尽量使用合成/聚合的方式,而不是使用继承。

4.7.2继承的缺点 - 增强耦合

        假设我们现在有两个类,类A和类B,类A具有强大的能力(方法),类B是一个废材,B类很渴望和A类一样强大,那么我们可以进行使用继承帮它嘛。

4.7.2.1定义具有强大能力的A类

        A类狂的不行,毕竟太有实力了,此时他在大街上携带者三个强大的能力(方法)溜达着。

/*** 定义一个父类A*/
public class A {public void operation1() {System.out.println("进行了操作1");}public void operation2() {System.out.println("进行了操作2");}public void operation3() {System.out.println("进行了操作3");}
}
4.7.2.2定义废材B类

        B类看见A类金光闪闪,必是一个强大人物,扑通一声,B类跪下说,大哥请赐给我力量,A类微微抬头,得意一笑,说你喊我声爹,认我当你爹,我就给你力量,B类思索片刻,心情十分复杂,脑海中回忆着往事,又浮现出对未来获取强大的能力后的幻想,在A类就要离开之时,B类抬起头,大喊:父亲,my Dad,I would get your force!!!

        于是B类成为了A类的儿子,继承得到了A类的力量。

/*** 渴望拥有能力(方法)的B类*/
public class B extends A {
}
4.7.2.3测试B类的力量

        B得到了满足,回到家乡,展示给大家看他从A类中继承出来的能力。

/*** 测试类*/
public class Test {public static void main(String[] args) {B b = new B();b.operation1();b.operation2();b.operation3();}}
4.7.2.4继承带来的代价

        继承太重了,会增强类之间的强耦合性,如果仅仅是想要去获取方法使用,不建议使用继承的方式去获取。

        因为继承会带来很重的耦合关系(B类直接变成A类的儿子,这操作难道不重?)

4.7.3解决方案

        像仅仅是去借用一个类中的方法,不是为了在层次上进行设计,不建议进行使用继承,继承应该是在层次上有设计需求的时候,进行使用比较好,如果没有设计需求,不建议去使用这个,因为耦合性实在是太强了。

        解决方案其实就是将继承的方案,替换为合成(依赖),聚合,组合的方式,来进行使用其它类中的方法,而不是使用继承,这样就可以做到解耦合。

4.7.4使用合成(依赖)解决问题

        合成(依赖)其实就是B中哪里需要调用A中的方法,在B中需要调用的方法中,将A对象作为参数传进去,这样就可以在B需要的地方去调用A中的方法了,这就是合成(依赖)

4.7.4.1定义具有强大能力的A类

        在你面前的仍然是具有强大能力的A类。

/*** 定义一个父类A*/
public class A {public void operation1() {System.out.println("进行了操作1");}public void operation2() {System.out.println("进行了操作2");}public void operation3() {System.out.println("进行了操作3");}}
4.7.4.2定义废材B类

        B类这次没有认A当爹,而是进行在自己的方法中去调用A中的方法进行使用,其实就是一个狐假虎威。

/*** 渴望拥有能力(方法)的B类*/
public class B {public void operation1(A a) {a.operation1();}public void operation2(A a) {a.operation2();}public void operation3(A a) {a.operation3();}
}
4.7.4.3总结使用合成解决问题

        使用合成(依赖)的方式其实就是进行在B的方法中进行接收一个A类型的参数,调用A中的方法进行使用,这样就实现了脱离继承体系去使用A中的方法的目的。

4.7.5使用聚合解决问题

        聚合就是在类B中定义一个类A的字段,使用setter方法为类B设置类A字段的A实例对象。

4.7.5.1定义具有强大能力的A类

        A类没变化,不贴代码啦。

4.7.5.2定义废材B类
/*** 渴望拥有能力(方法)的B类*/
public class B {private A a;public void setA(A a) {this.a = a;}public void operation1() {a.operation1();}public void operation2() {a.operation2();}public void operation3() {a.operation3();}
}

4.7.6使用组合解决问题

        组合就是直接进行在B类中的A字段中new出来一个A对象,进行使用,这样就是组合。

4.7.6.1定义具有强大能力的A类

        A类没变化,不贴代码啦。

4.7.6.2定义废材B类

        可以看见确实是在B类中的A字段中直接进行new了一个A对象出来进行使用。

/*** 渴望拥有能力(方法)的B类*/
public class B {private A a = new A();public void operation1() {a.operation1();}public void operation2() {a.operation2();}public void operation3() {a.operation3();}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/914766.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/914766.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深度学习-参数初始化、损失函数

A、参数初始化参数初始化对模型的训练速度、收敛性以及最终的性能产生重要影响。它可以尽量避免梯度消失和梯度爆炸的情况。一、固定值初始化在神经网络训练开始时&#xff0c;将权重或偏置初始化为常数。但这种方法在实际操作中并不常见。1.1全零初始化将所有的权重参数初始化…

格密码--Ring-SIS和Ring-LWE

1. 多项式环&#xff08;Polynomial Rings&#xff09; 设 f∈Z[x]f \in \mathbb{Z}[x]f∈Z[x] 是首一多项式&#xff08;最高次项系数为1&#xff09; 则环 RZ[x]/(f)R \mathbb{Z}[x]/(f)RZ[x]/(f) 元素为&#xff1a;所有次数 <deg⁡(f)< \deg(f)<deg(f) 的多项式…

前端工作需要和哪些人打交道?

前端工作中需要协作的角色及协作要点 前端工作中需要协作的角色及协作要点 前端开发处于产品实现的 “中间环节”,既要将设计方案转化为可交互的界面,又要与后端对接数据,还需配合团队推进项目进度。日常工作中,需要频繁对接的角色包括以下几类,每类协作都有其核心目标和…

万字长文解析 OneCode3.0 AI创新设计

一、研究概述与背景 1.1 研究背景与意义 在 AI 技术重塑软件开发的浪潮中&#xff0c;低代码平台正经历从 “可视化编程” 到 “意图驱动开发” 的根本性转变。这种变革不仅提升了开发效率&#xff0c;更重新定义了人与系统的交互方式。作为国内领先的低代码平台&#xff0c;On…

重学前端006 --- 响应式网页设计 CSS 弹性盒子

文章目录盒模型一、盒模型的基本概念二、两种盒模型的对比 举例三、总结Flexbox 弹性盒子布局一、Flexbox 的核心概念​​二、Flexbox 的基本语法​​​​1. 定义 Flex 容器​​​2. Flex 容器的主要属性​​​​3. Flex 项目的主要属性​​​​三、Flexbox 的常见布局示例​​…

rLLM:用于LLM Agent RL后训练的创新框架

rLLM&#xff1a;用于LLM Agent RL后训练的创新框架 本文介绍了rLLM&#xff0c;一个用于语言智能体后训练的可扩展框架。它能让用户轻松构建自定义智能体与环境&#xff0c;通过强化学习进行训练并部署。文中还展示了用其训练的DeepSWE等智能体的出色表现&#xff0c;以及rLL…

rocky8 --Elasticsearch+Logstash+Filebeat+Kibana部署【7.1.1版本】

软件说明&#xff1a; 所有软件包下载地址&#xff1a;Past Releases of Elastic Stack Software | Elastic 打开页面后选择对应的组件及版本即可&#xff01; 所有软件包名称如下&#xff1a; 架构拓扑&#xff1a; 集群模式&#xff1a; 单机模式 架构规划&#xff1a…

【JVM】内存分配与回收原则

在 Java 开发中&#xff0c;自动内存管理是 JVM 的核心能力之一&#xff0c;而内存分配与回收的策略直接影响程序的性能和稳定性。本文将详细解析 JVM 的内存分配机制、对象回收规则以及背后的设计思想&#xff0c;帮助开发者更好地理解 JVM 的 "自动化" 内存管理逻辑…

Qt获取hid设备信息

Qt 中通过 HID&#xff08;Human Interface Device&#xff09;接口获取指定的 USB 设备&#xff0c;并读取其数据。资源文件中包含了 hidapi.h、hidapi.dll 和 hidapi.lib。通过这些文件&#xff0c;您可以在 Qt 项目中实现对 USB 设备的 HID 接口调用。#include <QObject&…

Anaconda Jupyter 使用注意事项

Anaconda Jupyter 使用注意事项 1.将cell转换为markdown。 First, select the cell you want to convertPress Esc to enter command mode (the cell border should turn blue)Press M to convert the cell to Markdown在编辑模式下按下ESC键&#xff0c;使单元块&#xff08;c…

[硬件电路-20]:模拟信号处理运算与数字信号处理运算的相同点与不同点

模拟信号处理运算与数字信号处理运算是信号处理领域的两大核心方法&#xff0c;二者在信号形式、处理机制、性能特点和应用场景上存在显著差异&#xff0c;但也共享一些基础目标与理论支撑。以下从多个维度进行系统对比分析&#xff1a;一、相同点1. 核心目标一致信号变换与分析…

Redis 高频面试题

1. 缓存穿透 1.1 描述 用户想要查询某个数据,在 Redis 中查询不到,即没有缓存命中,这时就会直接访问数据库进行查询。当请求量超出数据库最大承载量时,就会导致数据库崩溃。这种情况一般发生在非正常 URL 访问,目的不是为了获取数据,而是进行恶意攻击。 1.2 现象 1、应…

OWASP Top 10 攻击场景实战

OWASP (开放式Web应用程序安全项目) Top 10 榜单是全球公认的、针对Web应用最关键安全风险的权威指南。它不是一份详尽无遗的清单&#xff0c;而是一份凝聚了安全专家共识的“高危预警”。本文将不止于罗列这些风险&#xff0c;而是深入每个风险的核心&#xff0c;通过生动的比…

Three.js 实战:使用 PBR 贴图打造真实地面材质

在 Three.js 中&#xff0c;我们可以通过 MeshStandardMaterial 材质配合多张贴图来实现真实的地面效果。这种方式模拟了物理世界中光照与表面材质的复杂交互&#xff0c;常用于构建高质量场景&#xff0c;如数字孪生、建筑可视化、游戏等。 本文将以一个完整示例为基础&#x…

Java基础的总结问题(第一篇)

JDK和JRE的区别&#xff1f;JRE是Java运行环境&#xff08;Java Runtime Environment&#xff09;&#xff0c;包含了JVM和Java核心类库JDK是Java开发工具包&#xff08;Java Developers Kit&#xff09;,包含了JRE和Java常见的开发工具与equals的区别&#xff1f;可以用来比较…

[智能算法]MOEA/D算法的Python实现

一、初始化不同于NSGA-II&#xff0c;MOEA/D在进行迭代之前需要先进行初始化&#xff0c;初始化的主要内容是计算个体向量权重之间的欧氏距离&#xff0c;并得出其邻域集合。# 计算T个邻居 def cpt_W_Bi_T(moead):# 设置的邻居个数错误(自己不能是自己的邻居)if moead.T_size &…

Java设计模式之-组合模式

什么是组合模式&#xff1f; 组合模式允许你将对象组合成树形结构来表示"部分-整体"的层次结构。它让客户端能够以统一的方式处理单个对象和对象组合。 简单来说&#xff0c;就像公司的组织结构&#xff1a; 公司有部门部门有小组小组有员工但无论是对公司、部门还是…

2021-10-29 C++与反转数的和

缘由输入一个三位数 与它倒过来的数相加&#xff0c;输出和-编程语言-CSDN问答 直接写 int n0,nn0,nnn0; cin>>n;nnn; while(nn)nnn*10,nnnnn%10,nn/10; cout<<nnnn<<endl; 缘由https://ask.csdn.net/questions/7552128 int 反转数(int n) { int nn 0…

论安全架构设计(威胁与措施)

安全架构威胁与措施摘要2021年4月&#xff0c;我有幸参与了某保险公司的“优车险”项目的建设开发工作&#xff0c;该系统以车险报价、车险投保和报案理赔为核心功能&#xff0c;同时实现了年检代办、道路救援、一键挪车等增值服务功能。在本项目中&#xff0c;我被安排担任架构…

022_提示缓存与性能优化

提示缓存与性能优化 目录 缓存技术概述缓存工作原理实现方法详解成本优化策略性能优化实践高级应用场景最佳实践指南 缓存技术概述 什么是提示缓存 提示缓存是Claude API的一项优化功能&#xff0c;允许缓存提示的特定部分以便重复使用&#xff0c;从而显著减少处理时间和…