Java设计模式之软件设计七大原则

发布于:2023-01-21 ⋅ 阅读:(617) ⋅ 点赞:(0)

为了提高软件系统的可维护性和可复用性,要尽量根据7大原则来开发程序,从而提高软件开发效率、节约软件开发成本和维护成本

1. 开闭原则

定义:Open Closed Principle(OCP)。软件实体(项目中划分出的模块、类与接口、方法)应当对扩展开放,对修改关闭

含义:当应用的需求改变时,在不修改软件实体的源代码的前提下,可以扩展模块的功能,使其满足新的需求

作用

它使软件实体拥有一定的适应性和灵活性的同时具备稳定性和延续性。具体如下

  1. 对软件测试的影响
    软件测试时只需要对扩展的代码进行测试就可以了,因为原有的测试代码仍然能够正常运行

  2. 可以提高代码的可复用性
    粒度越小,被复用的可能性就越大

  3. 可以提高软件的可维护性
    其稳定性高和延续性强,从而易于扩展和维护

实现方法

可以通过通过接口或者抽象类为软件实体定义一个相对稳定的抽象层,而将可变因素封装在具体的实现类中

因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节可以从抽象派生来的实现类来进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了

例子:Windows的桌面主题设计
Windows的主题是桌面背景图片、窗口颜色和声音等元素的组合。用户可以根据自己的喜爱更换自己的桌面主题。这些主题有共同的特点,可以为其定义一个抽象类(Abstract Subject),而每个用户的具体桌面主题(Specific Subject)是其子类。用户的桌面主题可以根据需要选择或者增加新的功能,而不需要修改原来的那一部分代码,所以它是满足开闭原则的

2. 里氏替换原则

定义:Liskov Substitution Principle(LSP)。继承必须确保超类所拥有的性质在子类中仍然成立

含义:主要阐述了继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。是对开闭原则的补充

作用

  1. 里氏替换原则是实现开闭原则的重要方式之一
  2. 它克服了继承中重写父类造成的可复用性变差的缺点
  3. 它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性
  4. 加强程序的健壮性,同时变更时可以做到非常好的兼容性,提高程序的维护性、可扩展性,降低需求变更时引入的风险

实现方法
里氏替换原则通俗来讲就是:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的非抽象方法

总结如下:

  1. 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
  2. 子类中可以增加自己特有的方法
  3. 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松
  4. 当子类的方法实现父类的方法时(实现抽象方法),方法的后置条件(即方法的返回值)要比父类的方法更严格或相等

关于里氏替换原则的例子,例如,企鹅、鸵鸟和几维鸟从生物学的角度来划分,它们属于鸟类;但从类的继承关系来看,由于它们不能继承“鸟”会飞的功能,所以它们不能定义成“鸟”的子类

3. 依赖倒置原则

定义:Dependence Inversion Principle(DIP)。高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。其核心思想是:要面向接口编程,不要面向实现编程

含义:依赖倒置原则是实现开闭原则的重要途径之一,它降低了客户与实现模块之间的耦合。在软件设计中,细节(具体的实现类)具有多变性,而抽象层(接口或者抽象类)则相对稳定

作用

  1. 可以降低类间的耦合性
  2. 可以提高系统的稳定性
  3. 可以减少并行开发引起的风险
  4. 可以提高代码的可读性和可维护性

实现方法
需要遵循以下4点:

  1. 每个类尽量提供接口或抽象类,或者两者都具备
  2. 变量的声明类型尽量是接口或者是抽象类
  3. 任何类都不应该从具体类派生。
  4. 使用继承时尽量遵循里氏替换原则

4. 单一职责原则

定义:Single Responsibility Principle(SRP)又称单一功能原则。这里的职责是指类变化的原因,单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分

该原则提出对象不应该承担太多职责,否则至少存在以下两个缺点:

  1. 一个职责的变化可能会削弱或者抑制这个类实现其他职责的能力
  2. 当客户端需要该对象的某一个职责时,不得不将其他不需要的职责全都包含进来,从而造成冗余代码

单一职责原则同样适用于方法。一个方法应该尽可能做好一件事情。如果一个方法处理的事情太多,其颗粒度会变得很粗,不利于重用

优点
核心就是控制类的粒度大小、将对象解耦、提高其内聚性。优点如下:

  1. 降低类的复杂度。一个类只负责一项职责,其逻辑肯定要比负责多项职责简单得多
  2. 提高类的可读性。复杂性降低,自然其可读性会提高
  3. 提高系统的可维护性。可读性提高,那自然更容易维护了
  4. 变更引起的风险降低。变更是必然的,如果单一职责原则遵守得好,当修改一个功能时,可以显著降低对其他功能的影响

实现方法
是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,再封装到不同的类中

例子:学生工作管理程序

描述:学生工作主要包括生活辅导和学业指导两个方面的工作,其中生活辅导主要包括班委建设、出勤统计、心理辅导、费用催缴、班级管理等工作,学业指导主要包括专业引导、学习辅导、科研指导、学习总结等工作。如果将这些工作交给一位老师负责显然不合理,正确的做法是生活辅导由辅导员负责,学业指导由学业导师负责,其类图如下所示:

单一职责原则

5. 接口隔离原则

定义:Interface Segregation Principle(ISP)要求程序员尽量将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含客户感兴趣的方法

含义:要为各个类建立它们需要的专用接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用

接口隔离原则和单一职责原则都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:

  • 单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离
  • 单一职责原则主要是约束类,它针对的是程序中的实现和细节,接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建

优点

  1. 将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性
  2. 接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性
  3. 如果接口的粒度大小定义合理,能够保证系统的稳定性;但是,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务
  4. 使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义
  5. 能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码

实现方法
6. 接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑
7. 为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法
8. 根据实际情况进行接口的拆分
9. 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情

例子: 学生成绩管理程序

描述:学生成绩管理程序一般包含插入成绩、删除成绩、修改成绩、计算总分、计算均分、打印成绩信息、査询成绩信息等功能。如果将这些功能全部放到一个接口中显然不太合理,正确的做法是将它们分别放在输入模块、统计模块和打印模块中,其类图如下所示

学生成绩管理程序一般包含插入成绩、删除成绩、修改成绩、计算总分、计算均分、打印成绩信息、査询成绩信息等功能,如果将这些功能全部放到一个接口中显然不太合理,正确的做法是将它们分别放在输入模块、统计模块和打印模块等 3 个模块中,其类图如图 1 所示

接口隔离原则
程序实现如下

public class ISPTest {

    public static void main(String[] args) {

        InputModule input = StuScoreList.getInputModule();
        CountModule count = StuScoreList.getCountModule();
        PrintModule print = StuScoreList.getPrintModule();
        input.insert();
        count.countTotalScore();
        print.printStuInfo();

    }
}



// 输入模块接口
interface InputModule {
    void insert();
    void delete();
    void modify();
}

// 统计模块接口
interface CountModule {
    void countTotalScore();
    void countAverage();
}

// 打印模块接口
interface PrintModule {
    void printStuInfo();
    void queryStuInfo();
}

// 实现类
class StuScoreList implements InputModule, CountModule, PrintModule {
    private StuScoreList() {
    }

    public static InputModule getInputModule() {
        return (InputModule) new StuScoreList();
    }

    public static CountModule getCountModule() {
        return (CountModule) new StuScoreList();
    }

    public static PrintModule getPrintModule() {
        return (PrintModule) new StuScoreList();
    }

    public void insert() {
        System.out.println("输入模块的insert()方法被调用");
    }

    public void delete() {
        System.out.println("输入模块的delete()方法被调用");
    }

    public void modify() {
        System.out.println("输入模块的modify()方法被调用");
    }

    public void countTotalScore() {
        System.out.println("统计模块的countTotalScore()方法被调用");
    }

    public void countAverage() {
        System.out.println("统计模块的countAverage()方法被调用");
    }

    public void printStuInfo() {
        System.out.println("打印模块的printStuInfo()方法被调用");
    }

    public void queryStuInfo() {
        System.out.println("打印模块的queryStuInfo()方法被调用");
    }
}

执行程序,输出结果如下:

输入模块的insert()方法被调用
统计模块的countTotalScore()方法被调用
打印模块的printStuInfo()方法被调用

6. 迪米特法则

定义:Law of Demeter(LoD)又叫作最少知识原则(Least Knowledge Principle,LKP),只与你的直接朋友交谈(明星和经纪人),不跟“陌生人”(明星和粉丝)说话

含义:如果两个软件实体(明星和粉丝)无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方(经纪人)转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性

迪米特法则中的“朋友”(经纪人中定义的属性和方法)是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法

优点

  1. 降低了类之间的耦合度,提高了模块的相对独立性
  2. 由于亲合度降低,从而提高了类的可复用率和系统的扩展性

但是,过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。所以需要确保高内聚和低耦合的同时,保证系统的结构清晰

实现方法:从依赖者(经纪人)的角度来说,只依赖应该依赖的对象;从被依赖者(明星)的角度说,只暴露应该暴露的方法

  1. 在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标
  2. 在类的结构设计上,尽量降低类成员的访问权限
  3. 在类的设计上,优先考虑将一个类设置成不变类
  4. 在对其他类的引用上,将引用其他对象的次数降到最低
  5. 不暴露类的属性成员,而应该提供相应的访问器(set和get方法)
  6. 谨慎使用序列化(Serializable)功能

例子:明星与经纪人的关系实例

描述:明星由于全身心投入艺术,所以许多日常事务由经纪人负责处理,如与粉丝的见面会,与媒体公司的业务洽淡等。这里的经纪人是明星、粉丝、媒体公司的朋友,而明星与粉丝、媒体公司是陌生人,所以适合使用迪米特法则,其类图如下 所示

迪米特法则
程序实现如下:

public class LoDTest {

    public static void main(String[] args) {

        Agent agent = new Agent();
        agent.setStar(new Star("彭于晏"));
        agent.setFans(new Fans("粉丝美美"));
        agent.setCompany(new Company("湖南传媒有限公司"));
        agent.meeting();
        agent.business();
    }
}

// 明星
class Star {
    private String name;

    Star(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

// 粉丝
class Fans {
    private String name;

    Fans(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

// 媒体公司
class Company {
    private String name;

    Company(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}


// 经纪人
class Agent {
    private Star myStar;
    private Fans myFans;
    private Company myCompany;

    public void setStar(Star myStar) {
        this.myStar = myStar;
    }
    public void setFans(Fans myFans) {
        this.myFans = myFans;
    }
    public void setCompany(Company myCompany) {
        this.myCompany = myCompany;
    }

    public void meeting() {
        System.out.println(myFans.getName() + "与明星" + myStar.getName() + "见面了");
    }
    public void business() {
        System.out.println(myCompany.getName() + "与明星" + myStar.getName() + "洽淡业务");
    }
}

执行程序,输出结果如下:

粉丝美美与明星彭于晏见面了
湖南传媒有限公司与明星彭于晏洽淡业务

7. 合成复用原则

定义:Composite Reuse Principle(CRP)又叫组合/聚合复用原则(Composition/Aggregate Reuse Principle,CARP)。它要求在软件复用时,要尽量先使用组合或者聚合(将颜色定义为汽车的属性,而不是通过继承实现颜色)等关联关系来实现,其次才考虑使用继承关系来实现

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

重要性
通常类的复用分为继承复用和合成复用两种,继承复用虽然有简单和易实现的优点,但它也存在以下缺点:

  1. 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用
  2. 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护
  3. 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化

采用组合或聚合复用时,可以将已有对象纳入新对象中(将颜色定义为汽车的属性),使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:

  1. 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用
  2. 新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口
  3. 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象

实现方法
通过将已有的对象纳入新对象中(将颜色定义为汽车的属性),作为新对象的成员对象来实现的,新对象可以调用已有对象的功能,从而达到复用

例子:汽车分类管理程序

描述:汽车按“动力源”划分可分为汽油汽车、电动汽车等;按“颜色”划分可分为白色汽车、黑色汽车和红色汽车等。如果同时考虑这两种分类,使用继承实现复用其子类就很多。可以使用组合关系解决以上问题,其类图如下:

合成复用原则

8. 软件设计七大原则总结

并不是所有代码都要遵循设计原则,而是要综合考虑人力、时间、成本、质量等,在适当的场景遵循设计原则

七大原则的目的是降低对象之间的耦合,增加程序的可复用性、可扩展性和可维护性

总结如下:

设计原则 归纳 目的
开闭原则 对扩展开放,对修改关闭 降低维护带来的新风险
里氏替换原则 不要破坏继承体系,子类重写方法功能发生改变,不应该影响父类方法的含义 防止继承泛滥
依赖倒置原则 高层不应该依赖低层,要面向接口编程 更利于代码结构的升级扩展
单一职责原则 一个类只干一件事,实现类要单一 便于理解,提高代码的可读性
接口隔离原则 一个接口只干一件事,接口要精简单一 功能解耦,高聚合、低耦合
迪米特法则 不该知道的不要知道,一个类应该保持对其它对象最少的了解,降低耦合度 只和朋友交流,不和陌生人说话,减少代码臃肿
合成复用原则 尽量使用组合或者聚合关系实现代码复用,少使用继承 降低代码耦合
本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到