面试准备系列——设计模式探索(16)

tech2022-10-16  104

文章目录

1.java程序中的常说的设计模式六大原则单一职责原则(SRP)里氏替换原则依赖倒置原则接口隔离原则迪米特法则开闭原则总结 2.介绍几个设计模式2.1 单例模式2.2 工厂方法模式

1.java程序中的常说的设计模式六大原则

这其实不只是java程序,而是所有的程序(更具体点的话应该是面向对象语言)都会遵循的设计模式:

单一职责原则——SRP里氏替换原则——OCP依赖倒置原则——LSP接口隔离原则——DIP迪米特法则——ISP开闭原则——LOD

单一职责原则(SRP)

单一职责规定了一个类应该只有一个发生变化的原因。如果一个类承担了多个职责,则会导致多个职责耦合在一起。但部分职责发生变化的时候,可能会导致其余职责跟着受到影响,也就是说我们的程序耦合性太强,不利于变化。 下边使用IUser这个类说明一下: IUser类中拥有设置和获取用户名字和年龄的方法,还有指挥该User去跑步,学习以及玩耍的功能。那么,IUser类其实就是违反了单一职责原则。我们可以将其拥有到职责进行划分,一类是用户User的固有属性,另一类是用户User拥有的实际能力。固有属性包括设置和获取名字和年龄,实际能力则包括User具有的跑步,学习以及玩耍功能。

所以,我们需要将该IUser类进行划分,分为多个接口,划分之后的类图如下所示: 那么,单一职责有哪些优点呢?

降低了类的复杂度,每一个类都有清晰明确的职责。 程序的可读性和可维护性都得到了提高。 降低业务逻辑变化导致的风险,一个接口的修改只对相应的实现类有影响,对其他接口无影响。

里氏替换原则

里氏替换是指所有父类可以出现的地方,子类就都可以出现,使用子类来替换父类,调用方不需要关心目前传递的父类还是子类。 里氏替换原则可以增强程序的健壮性,子类可以任意增加和缩减,我们都不需要修改接口参数。在实际开发中,实现了传递不同的子类来完成不同的业务逻辑。

依赖倒置原则

依赖倒置原则是指高层模块不应该依赖于底层模块,抽象不应该依赖细节,细节应该依赖抽象。在Java中,接口和抽象类都是抽象,而其实现类就是细节。也就是说,我们应该做到面向接口编程,而非面向实现编程。 可以看个饲养员养动物的小案例: 刚开始的时候,饲养员只需要喂养一个狗就行了,这种时候面向实现编编程没有任何的问题,但是后边,突然要养多个大象和猫了,那么是不是需要写很多个类的实现? 而当我们面向接口编程的时候,使用依赖倒置原则,接口图如下: 面向接口编程了之后,只需要将狗、猫、大象继承动物类的接口就行了,然后饲养员也是面对不同的动物可以有不同的饲养方法,并且可以有多个饲养员。

所以,依赖倒置原则的好处可以总结一下:

依赖倒置通过抽象(接口或抽象类)使各个类或模块的独立,实现模块间的松耦合。面向接口编程可以使得当需求变化的时候,程序改动的工作量不至于太大。

接口隔离原则

接口隔离原则是指客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。

接口隔离原则的使用原则:

根据接口隔离原则拆分接口时,首先必须满足单一职责原则。接口需要高内聚,提高接口,类和模块的处理能力,减少对外的交互。定制服务,单独为一个个体提供优良服务(只提供访问者需要的方法)。接口设计要有限度,接口设计的太小,容易造成开发难度增加或者可维护性降低。

迪米特法则

迪米特法则也叫最少知识原则,是指一个对象应该对其依赖的对象有最少的了解。该类不需要知道其依赖类的具体实现,只需要依赖类给其提供一个公开对外的public方法即可,其余一概不需要了解。

迪米特法则的核心就是解耦合,减弱类间的各个耦合,提高类的复用率。

开闭原则

开闭原则是指一个软件实体如类,模块和函数应该对扩展开放,对修改关闭。也就是说,通过开闭原则,我们可以通过扩展行为来实现新的功能,而不是通过修改已有的代码。开闭原则可以帮助我们构建一个稳定,灵活的软件系统。

总结

六大设计原则是我们在进行程序设计和软件开发过程中应该去重点参考和遵循的准则。但是,鉴于我们复杂的业务逻辑场景以及多变的业务需求,往往不可能做到遵循全部的准则。所以,六大设计原则只是一个参考,具体设计中需要灵活使用各个原则,争取设计出优雅的软件架构。

六大原则总结:

单一职责原则:类或者接口要实现职责单一里氏替换原则:使用子类来替换父类,做出通用的编程依赖倒置原则:面向接口编程接口隔离原则:接口的设计需要精简单一迪米特法则:降低依赖之间耦合开闭原则:对扩展开放,对修改关闭

2.介绍几个设计模式

2.1 单例模式

通过案例来说明单例模式的具体实现方式。下边的案例展示的是单例模式的几种实现形式和各自的优缺点:

public class _1_单例模式的实现与优化 { /** * 饿汉式加载 * 饿汉模式的优点就是线程安全的,在并发的情况下不会像懒汉模式那样可能两三个线程判断的实例都为空,都一起创建了实例, * 但是饿汉模式的缺点也是很明显的,因为一旦将该类加载进内存就必须先创建实例,所以饿汉模式是非常耗费内存资源的。 */ class Single1 { private Single1 s1 = new Single1(); private Single1(){} public Single1 getInstance(){ return s1; } } /** * 懒汉式加载 * 下边的这种是不加锁的懒汉模式,它在并发的情况下,线程是不安全的。 * 所以对于这种情形下的懒汉模式最好使用双重检查加锁模式。 */ class Single2{ private Single2 s2; private Single2(){} public Single2 getInstance(){ if (s2 == null) { this.s2 = new Single2(); } return s2; } } /** * 懒汉式加载 * 下边的这种懒汉式加载是对上边的优化,由于上边的那种是线程不安全的,所以想办法解决这个问题 * 首先想到的就是使用同步方法解决 */ class Single3{ private Single3 s3; private Single3(){} public synchronized Single3 getInstance(){ if (s3 == null) { this.s3 = new Single3(); } return s3; } } /** * 懒汉式加载 * 其实上边的优化在多线程下是可以使用的了,但是对于性能来说,由于是已经串行化运行了,多线程程序变成了单线程程序那样去运行。 * 下边使用同步代码块的方法继续优化 */ class Single4{ private Single4 s4; private Single4() {} public Single4 getInstance(){ if (s4 == null) { synchronized (Single4.class){ if (s4 == null) { this.s4 = new Single4(); } } } return s4; } } /** * 懒汉加载 * 对于上边的这种模式,其实还是不安全的,因为他可能存在指令重排的问题 * 如: * Instance instance = new Instance()都发生了啥? * * 具体步骤如下三步所示: * * 在堆内存上分配对象的内存空间 * 在堆内存上初始化对象 * 设置instance指向刚分配的内存地址 * * 第二步和第三步可能会发生重排序,导致引用型变量指向了一个不为null但是也不完整的对象。所以,在多线程下上述的代码会返回一个不完整的对象。 * 根据前面章节所学的内容,我们需要加入一个volatile关键字来禁止指令重排序。 * * 所以完整的多线程模式下的单例模式的实现应该是下边的这样的 * * 这种方式也叫 DCL单例模式+Volatile(DCL即Double-Check-Locking) */ class Single5{ private volatile Single5 s5; private Single5(){} public Single5 getInstance(){ if (s5 == null) { synchronized (Single5.class){ if (s5 == null) { s5 = new Single5(); } } } return s5; } } }

单例模式的优点:单例模式保证了一个类在一个系统中有且只有一个对象实例,减少了系统内存和性能的开销。

单例模式的使用场景:创建一个对象需要消耗太多的资源或者在一个系统中不适合创建多个对象实例的情况下,我们可以采用单例模式设计实现。

2.2 工厂方法模式

工厂方法模式是一种常见的设计模式。工厂方法模式定义了一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法模式使一个类的实例化延迟到其子类。 其中,Product和ConcreteProduct分别表示抽象产品类和具体产品类。Creator和ConcreteCreator则分别表示抽象创建类和具体创建类。抽象创建类Creator中定义了创建产品的方法createProduct。

public class FactoryMethodTest { public static void main(String[] args) { // 创建具体的创建类对象 Creator creator = new ConcreteCreator(); // 通过传入指定的产品类对象,来创建对应的产品 Product product = creator.createProduct(ConcreteProduct1.class); // 创建对象之后,可以进行业务逻辑处理 product.method1(); product.method2(); } } // 定义抽象产品类 abstract class Product { // 产品类的公共方法 public void method1(){ // 公共的业务逻辑 } // 抽象方法 public abstract void method2(); } // 定义具体产品类 class ConcreteProduct1 extends Product { public void method2() { // 具体产品类1的业务逻辑处理 } } class ConcreteProduct2 extends Product { public void method2() { // 具体产品类2的业务逻辑处理 } } // 定义抽象创建类 abstract class Creator { // 创建对象的抽象方法 public abstract <T extends Product> T createProduct(Class<T> c); } // 定义具体的创建类,真正来创建所需的对象 class ConcreteCreator extends Creator { public <T extends Product> T createProduct(Class<T> c){ Product product=null; try { // 通过反射技术来创建对象 product = (Product)Class.forName(c.getName()).newInstance(); } catch (Exception e) { //异常处理 } return (T)product; } }

那么,工厂方法模式的优点有哪些呢?

工厂方法模式具有很好的封装性。客户端不需要知道创建对象的过程,只需要知道要创建的是哪个具体的产品即可。工厂方法模式对扩展开放。当新增一个产品种类的时候,我们只需要传入新增产品类对象给具体工厂,即可返回新增的产品对象。

工厂方法模式的使用场景:

工厂方法模式的作用就是创建指定的对象,可以做为new一个对象的替代方式。但是需要考虑是否有必要使用工厂方法模式来创建对象。当需要灵活,可扩展的创建多个对象的场景时,可以使用工厂方法模式。

工厂方法模式总结: 工厂方法模式是一种常见,并且比较简单的设计模式。在面试中,我们一般需要说出工厂方法模式的思想和具体实现步骤,当然可以熟练的画出工厂方法模式的类图会更棒。 工厂方法模式的本质就是指定一个要创建对象的类,然后传给具体的工厂类,由具体工厂类通过反射技术来创建并且返回一个对象。工厂方法模式可以扩展成为简单工厂模式和多工厂模式等。

最新回复(0)