软件设计与体系——面向对象设计的原则

一:前言

用一道题熟悉OO设计与编程:给定一个4位数(数字不完全相同),这4个数字可以得到最大4位数和最小4位数,算它们的差,不是6174则重复上述过程,最终会得到6174,验证这个过程。

import java.util.Arrays;
import java.util.Scanner;

public class Test {
    private static int[] x2Arr(int x) {
        int[] xArr = new int[4];
        for (int i = 0; i < 4; i++) {
            xArr[i] = (int)(x / Math.pow(10, i)) % 10;
        }
        Arrays.sort(xArr);
        return xArr;
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.print("请输入各位数字不完全相同的4位正整数:");
        int x = sc.nextInt();
        if (x < 1000 || x > 9999) return;

        int round = 0;
        int start = x;
        int end;
        while(true) {
            int[] xArr = x2Arr(start);
            int low = 0, high = 0;
            for (int i = 0; i < 4; i++) {
                low += xArr[i] * Math.pow(10, 3 - i);
                high += xArr[i] * Math.pow(10, i);
            }
            end = high - low;

            // 结束条件
            if (end == 0) {
                System.out.println("输入错误,各位数字不完全相同");
                return;
            }
            if (start == end) {
                System.out.println("最终结果稳定为:" + start + ",重复轮数为:" + round);
                return;
            }

            System.out.println("第" + (++round) + "轮差值为:" + end);
            start = end;
        }
    }
}

在这里插入图片描述

Robert C. Martin指出,导致软件难维护的有原因四个:

  • 过于僵硬(Rigidity):难加入新性能
  • 过于脆弱(Fragility):很难改动,与过于僵硬同时存在,牵一发动全身
  • 复用率低(Immobility):类似的算法、函数、模块,由于已有的代码依赖于很多其他东西,很难分离。最好的服用方法是直接CV
  • 黏度过高(Viscosity):一个改动有以下两种方式,一个系统的设计,如果总是后一种方法容易,则称为黏度过高
    • 保存原始设计意图和原始设计框架
    • 权益之计,解决短期问题,牺牲中长期利益

二:面向对象设计原则

面向对象设计模式七大原则:

  • 单一职责原则(Single Responsibility Principle, SRP):类的职责要单一,不能将太多的职责放在一个类中
  • 接口隔离原则(The Interface Segregation Principle):客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口
  • 依赖倒转原则(Dependency Inversion Principle, DIP):要针对抽象层编程,而不要针对具体类编程。
  • 里氏代换原则(Liskov Substitution Principle, LSP):在软件系统中,一个可以接受基类对象的地方必然可以接受一个子类对象。
  • 开闭原则(Open-Closed Principle, OCP):软件实体对扩展是开放的,但对修改是关闭的,即在不修改一个软件实体的基础上去扩展其功能。
  • 迪米特法则(Law of Demeter, LoD):一个软件实体对其他实体的引用越少越好,或者说如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,而是通过引入一个第三者发生间接交互。
  • 合成复用原则(组合优先原则)(Composite Reuse Principle, CRP):在系统中应该尽量多使用组合和聚合关联关系,尽量少使用甚至不使用继承关系。

使用设计模式的目的:编写软件过程中,程序员面临着来自耦合性,内聚性以及可维护性,可扩展性,重用性,灵活性等多方面的挑战,设计模式是为了让程序(软件)具有更好的:

  1. 代码重用性
  2. 可读性(编程规范性)
  3. 可扩展性(便于添加新功能)
  4. 可靠性(增加新功能后,对原功能不影响)
  5. 使程序呈现高内聚,低耦合的特性

其中单个类的设计原则

  • 抽象、封装和信息隐藏
  • 关注点分离和单一职责原则
  • 接口隔离原则

多个合作类的设计原则:

  • 松耦合
  • 开闭原则
  • 里氏代换原则
  • 依赖倒置原则
  • 契约式设计

程序的可读性:

  • 封装、抽象和信息隐藏
  • 关注点分离和单一职责原则

程序的正确性:

  • 里氏代换原则
  • 契约式设计原则
  • 多态程序的正确性

可扩展性

  • 接口隔离原则
  • 松耦合
  • 里氏替换原则
  • 开闭原则
  • 依赖倒置原则

一些概念:

  • 封装
    • 方法和数据封装成一个对象
    • 实现信息隐藏、接口对外
  • 抽象
    • 方法的接口使对象功能抽象
    • 对象实现接口定义方法
  • 信息隐藏
    • 封装内部属性
    • 通过接口访问(外部属性)
    • 保密原则

2.1:单一职责原则

基本介绍:对类来说的,即一个类应该只负责一项职责。如类A负责两个不同职责:职责1,职责2。当职责1需求变更而改变A时,可能造成职责2执行错误,所以需要将类A的粒度分解为A1,A2。

单一职责原则的核心含意是:

  • 一个类应该有且仅有一个职责
  • 一个类的职责是指引起该类变化的原因
  • 并非极端为一个类只设定一个方法,而是一组方法只能有一个变化轴线
  • 最好有一个概念统构该组方法

一个类(或者大到模块,小到方法)承担的职责越多,它被复用的可能性越小。而且如果一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作。

而单一职责原则是实现高内聚、低耦合的指导方针,在很多代码重构手法中都能找到它的存在,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责(单一职责的界定)需要设计人员具有较强的分析设计能力和相关重构经验。

单一原则注意事项和细节:

  • 降低类的复杂度,一个类只负责一项职责。提高类的可读性,可维护性
  • 降低变更引起的风险
  • 通常情况下,我们应当遵守单一职责原则,只有逻辑足够简单,才可以在代码级违反单一职责原则;只有类中方法数量足够少,才可以在方法级别保持单一职责原则

代码示例:

package com.eyes.base.designMode.principle;

public class SingleResponsibility {
    public static void main(String[] args) {
        // 方案一测试
        Vehicle vehicle = new Vehicle();
        vehicle.run("摩托车");
        vehicle.run("飞机");
        vehicle.run("潜艇");

        // 方案二测试
        new RoadVehicle().run("汽车");
        new AirVehicle().run("飞机");
        new WaterVehicle().run("潜艇");

        // 方案三测试
        VehicleImproved vehicleImproved = new VehicleImproved();
        vehicleImproved.runRoad("汽车");
        vehicleImproved.runAir("飞机");
        vehicleImproved.runWater("潜艇");
    }
}

/*
 **************
 * 方案一:违反了单一职责原则
 **************
 */
// 交通工具类
class Vehicle {
    // 飞机和潜艇不是在公路上跑,因此违反单一职责原则
    public void run(String vehicle) {
        System.out.println(vehicle + " 在公路上跑");
    }
}

/*
 **************
 * 方案二:
 *      遵守单一职责原则
 *      这么做改动很大,将类分离的同时修改客户端
 **************
 */
class RoadVehicle {
    public void run(String vehicle) {
        System.out.println(vehicle + " 在公路上跑");
    }
}
class AirVehicle {
    public void run(String vehicle) {
        System.out.println(vehicle + " 在天上飞");
    }
}
class WaterVehicle {
    public void run(String vehicle) {
        System.out.println(vehicle + " 在水里行驶");
    }
}

/*
 **************
 * 方案三:
 *      没有在类级别遵守单一职责原则,但是方法级别遵守
 *      对原本的类修改不大
 **************
 */
class VehicleImproved {
    public void runRoad(String vehicle) {
        System.out.println(vehicle + " 在公路上跑");
    }
    public void runAir(String vehicle) {
        System.out.println(vehicle + " 在天上飞");
    }
    public void runWater(String vehicle) {
        System.out.println(vehicle + " 在水里行驶");
    }
}

2.2:接口隔离原则

基本介绍:客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口

"胖"接口

  • 接口不内聚,表示该类具有"胖"接口
  • 类的"胖"接口可以分解成多组方法
  • 每一组方法都服务于一组不同的客户程序
  • 一些客户程序使用一组方法,其他使用其他组方法

而LSP原则(接口隔离原则)可以用来处理"胖"接口所具有的缺点

接口污染:为接口添加了不必要的职责

  • 在接口中加一个新方法只是为了减少类的数目,持续这么做接口就会被不断污染,变胖
  • 实际上,类的数目不是问题
  • 接口污染会带来维护和重用方面的问题
  • 最常见的问题:为了重用被污染的接口,被迫实现并维护不必要的方法
/*
 **************
 * 传统方法:
 *      类C通过Interface1依赖类A,类D通过接口Interface1依赖类B
 *      如果接口Interface对于类A和类C来说不是最小接口,那么类C和类D就必须去实现他们不需要的方法
 **************
 */

interface Interface1 {
    void operation1();
    void operation2();
    void operation3();
    void operation4();
}

class A implements Interface1 {

    @Override
    public void operation1() {
        System.out.println("A实现了operation1");
    }

    @Override
    public void operation2() {
        System.out.println("A实现了operation2");
    }

    @Override
    public void operation3() {
        System.out.println("A实现了operation3");
    }

    @Override
    public void operation4() {
        System.out.println("A实现了operation4");
    }
}

class B implements Interface1 {

    @Override
    public void operation1() {
        System.out.println("B实现了operation1");
    }

    @Override
    public void operation2() {
        System.out.println("B实现了operation2");
    }

    @Override
    public void operation3() {
        System.out.println("B实现了operation3");
    }

    @Override
    public void operation4() {
        System.out.println("B实现了operation4");
    }
}

// C类通过接口Interface1依赖A或B类,但只会用到1,2,3方法
class C {
    public void depend1(Interface1 i) {
        i.operation1();
    }

    public void depend2(Interface1 i) {
        i.operation2();
    }

    public void depend3(Interface1 i) {
        i.operation3();
    }
}

// D类通过接口Interface1依赖A或B类,但只会用到1,3,4方法
class D {
    public void depend1(Interface1 i) {
        i.operation1();
    }

    public void depend3(Interface1 i) {
        i.operation3();
    }

    public void depend4(Interface1 i) {
        i.operation4();
    }
}

/*
 **************
 * 接口隔离改进:
 *      将接口Interface1拆分为独立的几个接口,类C和类D分别与他们需要的接口建立依赖关系,也就是采用接口隔离原则
 *      接口Interface1中出现的方法可以根据实际情况分为三个接口
 **************
 */
interface Interface2 {
    void operation1();
    void operation3();
}

interface Interface3 {
    void operation2();
}

interface Interface4 {
    void operation4();
}

2.3:依赖倒转原则

依赖倒转原则是Robert C. Martin在1996年为《C++ Reporter》所写的专栏Engineering Notebook的第三篇,后来加入到他在2002年出版的经典著作《Agile Software Development, Principles, Patterns, andPractices》中。

依赖倒转原则是指:

  • 高层模块不应该依赖低层模块,二者都应该依赖其抽象
  • 抽象不应该依赖细节,细节应该依赖抽象
  • 依赖倒转的中心思想是面向接口编程

倒置的含义:很多传统的软件开发方法,比如结构化分析和设计,总是倾向于创建高层模块依赖于低层模块、抽象则依赖于细节的软件结构。一个设计良好的面向对象的程序的依赖关系结构相对于传统过程式方法设计的通常的结构而言就是被“倒置”了

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

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

层次化概念: Booch曾经说过:“所有的结构良好的面向对象架构都具有非常清晰的层次,每一个层次通过一个被很好地定义和控制的接口向外提供了一系列相互内聚的服务。”这个陈述的简单理解可能会致使一个设计者设计出类似下图的结构,在图中高层的策略类调用了低层的机制层;而机制层又调用更具体的工具类。存在一个隐伏的特性,那就是:Policy Layer对于其下层次一直到Utility Layer的改动都是非常敏感的,这意味着依赖是可传递的

在这里插入图片描述

下图是一个更合适的模型:
在这里插入图片描述

代码示例:

package com.eyes.base.designMode.principle;

public class DependencyInversion {
    public static void main(String[] args) {
        // 方法一
        Person person = new Person();
        person.receive(new Email());

        // 方法二
        new PersonPlus().receive(new EmailPlus());
        new PersonPlus().receive(new WeChat());
        new PersonPlus().receive(new Phone());

        // 方法三
        new OpenAndClose().open(new ColorTV());

        // 方法四
        new OpenAndClose2(new ColorTV2()).open();

        // 方法五
        OpenAndClose3 openAndClose3 = new OpenAndClose3();
        openAndClose3.setTv(new ColorTV3());
        openAndClose3.open();
    }
}

/*
 ******************
 * 方式一:
 *      实现简单,receive直接依赖Email类
 *      但如果获取的对象是微信、短信等,则需要新增类,Person也要增加相应接收方法
 ******************
 */

class Email {
    public String getInfo() {
        return "邮件信息:hello world";
    }
}

class Person {
    public void receive(Email email) {
        System.out.println(email.getInfo());
    }
}

/*
 ******************
 * 方式二:遵循依赖倒转原则
 ******************
 */

interface Info {
    String getInfo();
}

class EmailPlus implements Info {
    @Override
    public String getInfo() {
        return "邮件信息:hello world";
    }
}
class WeChat implements Info {
    @Override
    public String getInfo() {
        return "微信信息:hello world";
    }
}
class Phone implements Info {
    @Override
    public String getInfo() {
        return "短信信息:hello world";
    }
}

class PersonPlus {
    public void receive(Info info) {
        System.out.println(info.getInfo());
    }
}

/*
 ******************
 * 方式三:通过接口传递依赖关系
 ******************
 */
interface IOpenAndClose {
    void open(ITV tv);
}

interface ITV {
    void play();
}

class ColorTV implements ITV {
    @Override
    public void play() {
        System.out.println("彩色电视播放");
    }
}

class OpenAndClose implements IOpenAndClose {
    @Override
    public void open(ITV tv) {
        tv.play();
    }
}

/*
 ******************
 * 方式四:通过构造方法传递依赖关系
 ******************
 */
interface IOpenAndClose2 {
    void open();
}

interface ITV2 {
    void play();
}

class ColorTV2 implements ITV2 {
    @Override
    public void play() {
        System.out.println("彩色电视2播放");
    }
}

class OpenAndClose2 implements IOpenAndClose2 {
    private final ITV2 tv;

    public OpenAndClose2(ITV2 tv) {
        this.tv = tv;
    }

    @Override
    public void open() {
        tv.play();
    }
}

/*
 ******************
 * 方式五:通过setter方法传递依赖关系
 ******************
 */
interface IOpenAndClose3 {
    void setTv(ITV3 tv);
    void open();
}

interface ITV3 {
    void play();
}

class ColorTV3 implements ITV3 {
    @Override
    public void play() {
        System.out.println("彩色电视3播放");
    }
}


class OpenAndClose3 implements IOpenAndClose3 {
    private ITV3 tv;

    @Override
    public void setTv(ITV3 tv) {
        this.tv = tv;
    }

    @Override
    public void open() {
        tv.play();
    }
}

结论:

  • 依赖倒置原则是实现面向对象所声称的诸多优点的一个重要原则
  • 这个原则的正确应用对于创建可重用框架是必需的
  • 对于构建具有高弹性的代码同样是至关重要的,当抽象和具体细节被分离以后,代码的维护工作就变得容易多了

2.4:里氏代换原则

里氏代换原则由2008年图灵奖得主、美国第一位计算机科学女博士、麻省理工学院Barbara Liskov教授和卡内基-梅隆大学JeannetteWing教授于1994年提出。

芭芭拉·利斯科夫(Barbara Liskov),美国计算机科学家,2008年图灵奖(计算机领域的诺贝尔奖)得主,2004年约翰·冯诺依曼奖得主。美国工程院院士,美国艺术与科学院院士,美国计算机协会会士。现任麻省理工学院电子电气与计算机科学系教授。她是美国第一个计算机科学女博士,第二位获得图灵奖的女科学家。

OO中的继承性的思考和说明

  • 继承包含这样一层含义,父类中凡是已经实现好的方法,实际上是在设定规范和契约,虽然它不强制要求所有的子类必须遵循这些契约,但是如果子类对这些已经实现的方法任意修改,就会对整个继承体系造成破坏
  • 继承在给程序设计带来便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能产生故障

问题提出:在编程中,如何正确的使用继承? => 里氏替换原则

里氏替换原则:如果对每个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。换句话说,所有引用基类的地方必须能透明地使用其子类的对象。

里氏替换原则告诉我们,继承实际上让两个类耦合性增强了,在适当的情况下,可以通过聚合、组合、依赖来解决问题。因此在使用集成时,遵守里氏替换原则,在子类中尽量不要重写父类的方法

违背里氏替换原则的后果:如果某个函数使用了指向基类的指针或引用,却违背LSP原则(里氏替换原则),那么这个函数必须了解该基类的所有派生类,显然违背开闭原则OCP

代码示例:

public class Liskov {
    public static void main(String[] args) {
        // 错误演示
        TestA testA = new TestA();
        System.out.println("11 - 3 = " + testA.func1(11, 3));
        TestB testB = new TestB();
        System.out.println("11 - 3 = " + testB.func1(11, 3)); // 但B重写了A类方法,因此出错。
        System.out.println("11 + 3 + 9 = " + testB.func2(11, 3));

        // 修正方案
        TestA2 testA2 = new TestA2();
        System.out.println("11 + 3 = " + testA2.func1(11, 3));
        TestB2 testB2 = new TestB2();
        System.out.println("11 - 3 = " + testB2.func1(11, 3));
        System.out.println("11 + 3 + 9 = " + testB2.func2(11, 3));
    }
}

/*
 ******************
 * 错误演示:
 *      在实际开发中,我们常常通过重写父类的方法完成新的功能
 *      这样写虽然简单,但整个继承体系的复用性会变差,特别是运行多态比较频繁时
 ******************
 */
class TestA {
    public int func1(int num1, int num2) {
        return num1 - num2;
    }
}

class TestB extends TestA {
    public int func1(int a, int b) {
        return a + b;
    }

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

/*
 ******************
 * 修正方案:
 *      原来的父类和子类都继承一个更通俗的基类
 *      原有的继承关系去掉,采用依赖、聚合、组合等关系代替
 ******************
 */
class Base {

}

class TestA2 extends Base {
    public int func1(int num1, int num2) {
        return num1 - num2;
    }
}

class TestB2 extends Base {
    public int func1(int a, int b) {
        return a + b;
    }

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

结论:

  • OCP原则是OOD中很多说法的核心,它可以让应用程序更易维护、更易重用及更健壮。LSP原则是符合OCP原则应用程序的一项重要特性
  • 仅当派生类能完全替换基类时,才能放心地重用那些使用基类的函数和修改派生类型

2.5:开闭原则

开闭原则由Bertrand Meyer于1988年提出,它是面向对象设计中最重要的原则之一。

开闭原则的设计目标:

  • 可扩展性(Extensibility):与过于僵硬的相反
  • 灵活性(Flexibility):与过于脆弱相反,允许代码修改平稳发生,不波及其他模块
  • 可插入性(Pluggability):黏度过高的反面

开闭原则:一个软件实体如类,模块和函数应该对扩展开放(对提供方),对修改关闭(对使用方)。用抽象构建框架,用实现扩展细节。当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。

示例代码:

public class Ocp {
    public static void main(String[] args) {
        // 方法一
        GraphicEditor graphicEditor = new GraphicEditor();
        graphicEditor.drawShape(new Rectangle());
        graphicEditor.drawShape(new Circle());

        // 方法二
        GraphicEditor2 graphicEditor2 = new GraphicEditor2();
        graphicEditor2.drawShape(new Rectangle2());
        graphicEditor2.drawShape(new Circle2());
    }
}

/*
 *********************
 * 方式一:
 *      优点是比较好理解,简单易操作
 *      缺点是违反了设计模式的ocp原则。即对扩展开放,对修改关闭。
 *      即当我们给类增加新功能的时候,尽量不要或者少修改代码
 *********************
 */
class GraphicEditor {
    // 接收Shape的type,根据type绘制不同的图形
    public void drawShape(Shape s) {
        if (s.m_type == 1) drawRectangle();
        else drawCircle();
    }

    private void drawRectangle() {
        System.out.println("绘制矩形");
    }

    private void drawCircle() {
        System.out.println("绘制圆形");
    }
}

class Shape {
    int m_type;
}

class Rectangle extends Shape {
    Rectangle() {
        super.m_type = 0;
    }
}

class Circle extends Shape {
    Circle() {
        super.m_type = 1;
    }
}

/*
 *********************
 * 方式二:满足了开闭原则
 *********************
 */
class GraphicEditor2 {
    public void drawShape(Shape2 s) {
        s.draw();
    }
}

abstract class Shape2 {
    int m_type;

    public abstract void draw();
}

class Rectangle2 extends Shape2 {
    Rectangle2() {
        super.m_type = 0;
    }

    @Override
    public void draw() {
        System.out.println("绘制矩形");
    }
}

class Circle2 extends Shape2 {
    Circle2() {
        super.m_type = 1;
    }

    @Override
    public void draw() {
        System.out.println("绘制圆形");
    }
}

结论:

  • 从多种意义上来讲,开闭原则是面向对象设计的核心
  • 遵循这个原则带来的好处就是面向对象技术所声称的优点:可重用性和可维护性
  • 并不是说仅仅使用一种面向对象编程语言就是遵循这个原则,它依赖于设计者对程序中他认为可能发生变化的部分做出合理的设计上的抽象

2.6:迪米特法则

迪米特法则(Demeter Principle,又叫最少知道原则):一个类对自己依赖的类知道的越少越好,因为类与类关系越密切,耦合度越大。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供的public方法,不对外泄露任何信息。

迪米特法则还有个更简单的定义:只与直接的朋友通信。

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

代码示例:

public class Demeter {
    public static void main(String[] args) {
        SchoolManager schoolManager = new SchoolManager();
        schoolManager.printAllEmployee(new CollegeManager());
    }
}

/*
 ******************
 * 方式一:
 *      SchoolManager类的直接朋友类:Employee、CollegeManager
 *      CollegeEmployee不是其直接朋友类,因为它出现在局部变量中,因此违反迪米特原则
 ******************
 */
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() {
        ArrayList<CollegeEmployee> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            CollegeEmployee employee = new CollegeEmployee();
            employee.setId("学校员工id=" + i);
            list.add(employee);
        }
        return list;
    }
}

class SchoolManager {
    public List<Employee> getAllEmployee() {
        List<Employee> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            Employee employee = new Employee();
            employee.setId("学校总部员工id=" + i);
            list.add(employee);
        }
        return list;
    }

    public void printAllEmployee(CollegeManager sub) {
        System.out.println("---------学院员工---------");
        List<CollegeEmployee> list1 = sub.getAllEmployee();
        for (CollegeEmployee e: list1) {
            System.out.println(e.getId());
        }

        System.out.println("---------学校总部员工---------");
        List<Employee> list2 = this.getAllEmployee();
        for (Employee e: list2) {
            System.out.println(e.getId());
        }
    }
}

/*
 ******************
 * 方式二:遵守迪米特原则
 ******************
 */
class CollegeManager2 {
    public List<CollegeEmployee> getAllEmployee() {
        ArrayList<CollegeEmployee> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            CollegeEmployee employee = new CollegeEmployee();
            employee.setId("学校员工id=" + i);
            list.add(employee);
        }
        return list;
    }

    public void printAllCollegeEmployee() {
        System.out.println("---------学院员工---------");
        List<CollegeEmployee> list1 = this.getAllEmployee();
        for (CollegeEmployee e: list1) {
            System.out.println(e.getId());
        }
    }
}

class SchoolManager2 {
    public List<Employee> getAllEmployee() {
        List<Employee> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            Employee employee = new Employee();
            employee.setId("学校总部员工id=" + i);
            list.add(employee);
        }
        return list;
    }

    public void printAllEmployee(CollegeManager2 sub) {
        sub.getAllEmployee();

        System.out.println("---------学校总部员工---------");
        List<Employee> list2 = this.getAllEmployee();
        for (Employee e: list2) {
            System.out.println(e.getId());
        }
    }
}

总结:

  • 迪米特法则的核心是降低类之间的耦合
  • 由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间耦合关系,并不是要求完全没有依赖关系

三:其他思想

3.1:耦合性

耦合性(Coupling),也叫耦合度,是对模块间关联程度的度量。耦合的强弱取决于模块间接口的复杂性、调用模块的方式以及通过界面传送数据的多少。模块间的耦合度是指模块之间的依赖关系,包括控制关系、调用关系、数据传递关系。模块间联系越多,其耦合性越强,同时表明其独立性越差( 降低耦合性,可以提高其独立性)。软件设计中通常用耦合度和内聚度作为衡量模块独立程度的标准。划分模块的一个准则就是高内聚低耦合。

功能耦合:子系统之间互相影响的程度
内聚性:内部互相依赖程度或子系统内部相互合作的程度

高内聚、低耦合:设计松耦合的子系统,实现每个子系统高内聚

3.2:契约式设计

契约式设计(Design by Contract ,DbC):是一种设计计算机软件的方法。这种方法要求软件设计者为软件组件定义正式的,精确的并且可验证的接口,这样,为传统的抽象数据类型又增加了先验条件、后验条件和不变式。这种方法的名字里用到的“契约”或者说“契约”是一种比喻,因为它和商业契约的情况有点类似。

契约式设计的三个关键词:

  • 前置条件(precondition):为了调用函数,必须为真的条件,在其违反时,函数决不调用,传递好数据是调用者的责任。即“对调用者提出要求,被调用者的权益”。
  • 后置条件 (postcondition):函数保证能做到的事情,函数完成时的状态,函数有这一事实表示它会结束,不会无休止的循环。即“被调用者的责任,调用者的权益”。
  • 类不变项(class invariant):从调用者的角度来看,该条件总是为真,在函数的内部处理过程中,不变项可以为变,但在函数结束后,控制返回调用者时,不变项必须为真。

契约式设计六大原则:

  1. 区分命令和查询。查询返回一个结果,但不改变对象的可见性质。命令改变对象的状态,但不返回结果。(应当是不一定返回结果)
  2. 将基本查询同派生查询分开。派生查询可以用基本查询来定义。
  3. 针对每个派生查询,设定一个后验条件,使用一个或多个基本查询的结果来定义它。这样我们只要知道基本查询的值,也就能知道派生查询的值。
  4. 对于每个命令都撰写一个后验条件,规定每个基本查询的值。结合“用基本查询定义派生查询”的原则,我们已经能够知道每个命令的全部可视效果。
  5. 对于每个查询和命令,采用一个合适的先验条件。先验条件限定了客户调用查询和命令的时机。
  6. 撰写不变式来定义对象的恒定特性。类是某种抽象的体现,应当将注意力集中在最重要的属性上,以帮助读者建立关于类抽象的正确概念模型。

契约式设计与断言:断言可以看做是契约式设计的一个缩影或者是一部分,因为你不可能做DBC所做的一切事情。断言不能沿着继承层次往下遗传。如果你重新定义了某个具有合约的基类方法,实现该合约的的断言不会被正确调用(除非你再新的代码中复制他们),你必须手工调类不变项,基本的合约不会主动实现。

如果有兴趣了解更多内容,欢迎来我的个人网站看看:瞳孔的个人网站

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值