单一职责、接口隔离、依赖倒转、里氏替换、开闭原则、迪米特、合成复用_单一职责 开闭原则-程序员宅基地

技术标签: 设计模式  

用抽象构建架构,用实现扩展细节

  • 单一职责原则:实现类要职责单一;
  • 接口隔离原则:在设计接口的时候要精简单一;
  • 依赖倒转原则:面向接口编程;
  • 里氏替换原则:不要破坏继承关系;
  • 开闭原则原则:对扩展开发,对修改关闭;
  • 迪米特法则:最少知道原则;
  • 合成复用原则:先考虑组合或聚合,后考虑继承;

一、单一职责原则

1、目的

降低代码复杂度、降低系统耦合度、提高可读性

2、定义

对于一个类,只有一个引起该类变化的原因;该类的职责是唯一的,且这个职责是唯一引起其他类变化的原因。

3、具体实现

将不同的职责封装到不同的类或者模块中,当有新的需求将现有的职责分为颗粒度更小的职责的时候,应该及时对现有代码进行重构。

4、优点

(1)降低类的复杂度,一个类只负责一个职责。这样写出来的代码逻辑肯定要比负责多项职责简单得多。

(2)提高类的可读性,提高系统的可维护性。

(3)降低变更引起的风险。变更是必然的,如果单一职责原则遵守得好,当修改一个功能的时候可以显著降低对其他功能的影响。

5、注意事项和细节

(1)降低类的复杂度,一个类只负责一项职责;

(2)提高类的可读性,可维护性;

(3)降低变更引起的风险;

(4)通常情况下,我们应当遵守单一职责原则,只有逻辑足够简单、才可以在代码级违反单一职责原则,只有类中的方法足够少,才可以在类中保持方法级别的单一职责原则。

二、接口隔离原则

1、目的

避免接口过于臃肿

2、定义

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

3、具体实现

适度细化接口,将臃肿的接口拆分为独立的几个接口。

4、优点

(1)将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。

(2)接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。

(3)使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。

(4)能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。

5、注意事项和细节

如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。

三、依赖倒转原则

1、目的

避免需求变化导致过多的维护工作

2、定义

  • 高层模块不应该依赖底层模块,二者都应该依赖其抽象;
  • 抽象不应该依赖细节;
  • 细节应该依赖抽象。

每一个逻辑的实现都是由原子逻辑组成的,不可分割的原子逻辑就是低层模块(一般是接口,抽象类),原子逻辑的组装就是高层模块。在Java语言中,抽象就是指接口和或抽象类,两者都不能被直接实例化。细节就是实现类,实现接口或继承抽象类而产生的类就是细节,可以被直接实例化。

3、具体实现

面向接口编程,使用接口或者抽象类制定好规范和契约,而不去设计任何具体的操作,把展现细节的任务交给他们的实现类去完成。

4、DIP的好处

采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。

5、DIP的几种写法

(1)接口声明依赖对象;

(2)构造函数传递依赖对象;

在类中通过构造函数声明依赖对象(好比spring中的构造器注入),采用构造器注入。

(3)Setter方法传递依赖对象

在抽象中设置setter方法声明依赖对象(spring中的方法注入)

6、深入理解

依赖倒转原则的本质就是通过抽象使各个类或模块实现彼此独立,不互相影响,实现模块间的松耦合。

在项目中使用这个规则需要以下原则:

(1)每个类尽量都要有接口或抽象类:依赖倒转的基本要求,有抽象才能依赖倒转;

(2)变量的表明类型尽量是接口或者抽象类;

(3)任何类都不应该从具体类派生;

(4)尽量不要重写基类已经写好的方法(里氏替换原则);

(5)结合里氏替换原则来使用:

接口负责定义public属性和方法,并且声明与其他对象的依赖关系;

抽象类负责公共构造部分的实现;

实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化;

一句话,依赖倒转原则就是面向接口编程。

四、里氏替换原则

1、目的

避免系统继承体系被破坏

2、定义

所有引用基类的地方必须能透明地使用其子类的对象。

3、具体实现

(1)子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法;

(2)子类可以增加自己特有的方法;

(3)当子类覆盖或实现父类的抽象方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松;方法的后置条件(即方法的返回值)要比父类更严格。

(4)如果子类不能完整地实现父类的方法,或者父类的一些方法在子类中已经发生畸形,则建议断开继承关系,采用依赖,聚合,组合等关系继承。

4、代码实例

package designMode.advance.principle;

public class Liskov {
    public static void main(String[] args) {
        A a = new A();
        System.out.println("11-3=" + a.func1(11, 3));
        System.out.println("1-8=" + a.func1(1, 8));
        System.out.println("-----------------------");
        B b = new B();
        //因为B类不再继承A类,因此调用者,不会再func1是求减法
        //调用完成的功能就会很明确
        System.out.println("11+3=" + b.func1(11, 3));//这里本意是求出11+3
        System.out.println("1+8=" + b.func1(1, 8));// 1+8
        System.out.println("11+3+9=" + b.func2(11, 3));
        //使用组合仍然可以使用到A类相关方法
        System.out.println("11-3=" + b.func3(11, 3));// 这里本意是求出11-3
    }
}

//创建一个更加基础的基类
class Base {
    //把更加基础的方法和成员写到Base类
}

// A类
class A extends Base {
    // 返回两个数的差
    public int func1(int num1, int num2) {
        return num1 - num2;
    }
}

// B类继承了A
// 增加了一个新功能:完成两个数相加,然后和9求和
class B extends Base {
    //如果B需要使用A类的方法,使用组合关系
    private A a = new A();

    //这里,重写了A类的方法, 可能是无意识
    public int func1(int a, int b) {
        return a + b;
    }

    public int func2(int a, int b) {
        return func1(a, b) + 9;
    }

    //我们仍然想使用A的方法
    public int func3(int a, int b) {
        return this.a.func1(a, b);
    }
}

五、开闭原则

1、目的

提高扩展性、便于维护

2、定义

对扩展开放(对提供方),对修改关闭(对使用方)。

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

开闭原则是面向对象设计中最基础的设计原则,它指导我们如何建立稳定灵活的系统,开闭原则只定义了对修改关闭,对扩展开放。

因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保证架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了,当然前提是抽象要合理,要对需求的变更有前瞻性和预见性。

六、迪米特法则

1、目的

降低类与类之间的耦合度

2、定义

迪米特法则又叫最少知道原则,即一个类对自己依赖的类知道的越少越好,对于依赖的类不管有多复杂,都尽量将逻辑封装在类的内部,对外除了提供public方法,不泄漏任何信息。

更简单的说法:只与直接朋友通信。

直接朋友:每个对象都会与其它对象有耦合关系,耦合的方式有很多,依赖、关联、组合、聚合等。我们称出现在成员变量,方法参数,方法返回值中的类称为直接朋友,而出现在局部变量中的类不能称为直接朋友,也就是说,陌生的类不要以局部变量的形式出现在类的内部。

3、注意事项和细节

(1)在类的结构设计上,尽量降低类成员的访问权限;

(2)在类的设计上,优先考虑将一个类设计成不变类;

(3)在类的引用上,将引起其他类的次数降到最低;

(4)不暴露类的属性成员,而应该提供相应的访问器(getter、setter);

(5)谨慎使用序列化(serializable)功能;

过分的使用迪米特原则,会产生大量这样的中介和传递类,类之间需要通信就通过第三方转发的方式,就会造成系统的不同模块之间的通信效率降低、使系统的不同模块之间不容易协调等缺点,同时大大增加了系统的复杂度。所以在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。

4、代码实例

下面代码违反了迪米特法则,我平时就这么写的。。。

package designMode.advance.principle;

import java.util.ArrayList;
import java.util.List;

public class Demeter {
    public static void main(String[] args) {
        //创建了一个 SchoolManager 对象
        SchoolManager schoolManager = new SchoolManager();
        //输出学院的员工id 和  学校总部的员工信息
        schoolManager.printAllEmployee(new CollegeManager());
    }
}

//学校总部员工类
class Employee {
    private String id;

    public void setId(String id) {
        this.id = id;
    }

    public String getId() {
        return id;
    }
}


//学院的员工类
class CollegeEmployee {
    private String id;

    public void setId(String id) {
        this.id = id;
    }

    public String getId() {
        return id;
    }
}

//管理学院员工的管理类
class CollegeManager {
    //返回学院的所有员工
    public List<CollegeEmployee> getAllEmployee() {
        List<CollegeEmployee> list = new ArrayList<CollegeEmployee>();
        for (int i = 0; i < 10; i++) { //这里我们增加了10个员工到 list
            CollegeEmployee emp = new CollegeEmployee();
            emp.setId("学院员工id= " + i);
            list.add(emp);
        }
        return list;
    }
}

//学校管理类

//分析 SchoolManager 类的直接朋友类有哪些 Employee、CollegeManager
//CollegeEmployee 不是 直接朋友 而是一个陌生类,这样违背了 迪米特法则
class SchoolManager {
    //返回学校总部的员工
    public List<Employee> getAllEmployee() {
        List<Employee> list = new ArrayList<Employee>();

        for (int i = 0; i < 5; i++) { //这里我们增加了5个员工到 list
            Employee emp = new Employee();
            emp.setId("学校总部员工id= " + i);
            list.add(emp);
        }
        return list;
    }

    //该方法完成输出学校总部和学院员工信息(id)
    void printAllEmployee(CollegeManager sub) {

        //分析问题
        //1. 这里的 CollegeEmployee 不是  SchoolManager的直接朋友
        //2. CollegeEmployee 是以局部变量方式出现在 SchoolManager
        //3. 违反了 迪米特法则

        //获取到学院员工
        List<CollegeEmployee> list1 = sub.getAllEmployee();
        System.out.println("------------学院员工------------");
        for (CollegeEmployee e : list1) {
            System.out.println(e.getId());
        }
        //获取到学校总部员工
        List<Employee> list2 = this.getAllEmployee();
        System.out.println("------------学校总部员工------------");
        for (Employee e : list2) {
            System.out.println(e.getId());
        }
    }
}

使用迪米特法则改进:

//该方法完成输出学校总部和学院员工信息(id)
void printAllEmployee(CollegeManager sub) {

    //分析问题
    //1. 将输出学院的员工方法,封装到CollegeManager
    sub.printEmployee();

    //获取到学校总部员工
    List<Employee> list2 = this.getAllEmployee();
    System.out.println("------------学校总部员工------------");
    for (Employee e : list2) {
        System.out.println(e.getId());
    }
}
//管理学院员工的管理类
class CollegeManager2 {
    //返回学院的所有员工
    public List<CollegeEmployee> getAllEmployee() {
        List<CollegeEmployee> list = new ArrayList<CollegeEmployee>();
        for (int i = 0; i < 10; i++) { //这里我们增加了10个员工到 list
            CollegeEmployee emp = new CollegeEmployee();
            emp.setId("学院员工id= " + i);
            list.add(emp);
        }
        return list;
    }

    //输出学院员工的信息
    public void printEmployee() {
        //获取到学院员工
        List<CollegeEmployee> list1 = getAllEmployee();
        System.out.println("------------学院员工------------");
        for (CollegeEmployee e : list1) {
            System.out.println(e.getId());
        }
    }
}

简单来说,就是将获取学院员工的方法写到学校员工类中。

七、合成复用原则

1、目的

防止类的体系庞大

2、定义

它要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。

3、注意事项和细节

(1)通常的复用分为继承复用和合成复用,继承复用虽然有简单和易实现的优点,但它也有如下的缺点:

  • 继承复用破坏了类的封装性

因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。

  • 子类和父类的耦合度高

父类的改变会直接影响子类,不利于类的扩展和维护。

  • 限制了复用的灵活性

从父类继承而来的实现是静态的,在编译时已经定义,运行时无法发生变化。

(2)采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点。

  • 维护了类的封装性

因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。

  • 低耦合

这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。

  • 复用的灵活性高

这种复用可以在运行时动态进行,新对象可以动态地引用与成员对象类型相同的对象。

 

 

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/guorui_java/article/details/106469226

智能推荐

基于水平集的图像分割方法_fcxvsd-程序员宅基地

文章浏览阅读1.2w次,点赞10次,收藏60次。一、引言借鉴一些流体中的重要思想, 1988年,Osher和Sethian首次提出了水平集算法[1],这是一种有效解决曲线演化问题的数值方法,并且计算稳定,适宜任意维数空间。随后,Osher等人对水平集算法做出扩展和总结[2,3], Giga也做了相关的理论扩展[4]。近年来这种算法已被广泛地应用在图像处理领域[5]中 ,尤其在图像分割中已取得了很大的进展。事实上,用水平集来解决图像分割问题的_fcxvsd

php var_export 对循环引用的问题(Uncaught ErrorException: var_export does not handle circular references)-程序员宅基地

文章浏览阅读4.9k次。因为有些数据不知道具体类型,在使用var_export($data, true) 的时候,遇到:Uncaught ErrorException: var_export does not handle circular references。改用: print_r($data, true) 即可解决这个问题。..._var_export does not handle circular references

matlab 生成文件夹,matlab实现遍历文件夹并自动创建对应的新文件夹方法-程序员宅基地

文章浏览阅读789次。能自动将文件夹中所有文件自动的识别并创建对应的文件夹,文件夹名要和文件中某个字段相同。用system()函数。如下tmp1=['mkdir ' dir_final];system(tmp1); %文件夹创建完成我试了下,用下面的函数系统就自动关机了,哈哈fun='shutdown -s';system(fun);你运行了?哈哈,别急赶紧在运行里输入shutdown -a 就取消自动关机了,不过..._natlab nkdir生成文件夹

Android - ImageButton单击切换按钮图片效果的实现_安卓imagebutton加入java-程序员宅基地

文章浏览阅读3.6k次。在android中有一个ImageButton的View,跟Button按钮的区别是可以在Imagebutton上加载一个图片。从ImageButton这个字面意思上来看,它是一个图片按钮,那么我们就可以使用它做一个我们想要的图片按钮了,但是我们在实际使用的过程当中,就会发现该按钮的使用并没有想像中的那么简单,需要再增加一些代码或再配置XML才能实现图片按钮按下的效果,个人感觉有点麻烦,不_安卓imagebutton加入java

IOS 关于NSNotification_nsnotification userinfo 什么类型-程序员宅基地

文章浏览阅读3.4k次。这是一个观察者模式。首先在你需要监听的类中加入观察者:- (void)addObserver:(id)observer selector:(SEL)aSelector name:(NSString *)aName object:(id)anObject;这个观察者在监听到anObject发送名字为aName的notification时,调用selector的方法,在aSelector方_nsnotification userinfo 什么类型

android bundler机制_安卓bundler机制-程序员宅基地

文章浏览阅读2.1k次。在android中提供了一种异步回调机制Handler,使用它,我们可以在完成一个很长时间的任务后做出相应的通知 handler基本使用: 在主线程中,使用handler很简单,new一个Handler对象实现其handleMessage方法,在handleMessage中提供收到消息后相应的处理方法即可,这里不对handler使用进行详细说明,在看本博文前,读者应_安卓bundler机制

随便推点

Cassandra配置多节点集群_cassandra_seeds-程序员宅基地

文章浏览阅读1.5k次。上一篇文章 Cassandra入门指南 中,我们已经配置好了一个单节点集群,接下来,我们要往这个集群中多加几个节点,看看多个节点是如何同时工作的。Cassandra节点之间交换信息是通过一种叫做Gossip(暂时不知道该翻译为哪个专有名词合适,暂且意会吧)的机制。但是要想让一个消息传递到一个新加入的节点,至少还需要知道另外一个节点,这个节点叫做种子(Seed)。通常我们会选择一小部分相对稳定的节点..._cassandra_seeds

python 如何调用子程序并自动传参数 os.system os.popen subprocess_python如何调用子程序-程序员宅基地

文章浏览阅读4.5k次,点赞4次,收藏14次。背景最近工作中有遇到这样一种情况,需要执行一个 exe 文件更改某些设置来触发Service 工作,而执行这个 exe 程序需要一个屏幕输入参数,这个输入参数也是需要python 脚本生成的。如果每次都是 cmd 执行这个 exe 程序,把..._python如何调用子程序

React 中 CSS in JS 的最佳实践_react使用scss-程序员宅基地

文章浏览阅读854次,点赞26次,收藏11次。本人分享一下这次字节跳动、美团、头条等大厂的面试真题涉及到的知识点,以及我个人的学习方法、学习路线等,当然也整理了一些学习文档资料出来是附赠给大家的。知识点涉及比较全面,包括但不限于前端基础,HTML,CSS,JavaScript,Vue,ES6,HTTP,浏览器,算法等等详细大厂面试题答案、学习笔记、学习视频等资料领取,点击资料领取直通车!!全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。**_react使用scss

小程序跳转外链_外链跳转小程序-程序员宅基地

文章浏览阅读2.2k次。小程序跳转外链_外链跳转小程序

微信内置WeixinJSBridge_weixinjsbridge.call-程序员宅基地

文章浏览阅读941次。微信H5页面(关闭页面&&退出网页)isWechat = () => { let ua = window.navigator.userAgent.toLowerCase(); return ua.match(/MicroMessenger/i) == 'micromessenger';};if (isWechat()) { WeixinJSBri..._weixinjsbridge.call

PHP setcookie() 首次存储不上值_首次set-cookie-程序员宅基地

文章浏览阅读888次。客户端:可以看到,浏览器(客户端)向服务器发出一次请求,发出请求的时候,在请求头信息中带上了各种参数,告诉服务器,我要接收什么样的文本(Accept)、什么编码格式(Accept-Encoding)、什么语言(Accept-Language)等等,当然,还把Cookie也传到了服务器(Cookie)。服务器端:第一步:setcookie('a','value')因为cookie是设..._首次set-cookie