乐趣区

关于设计模式:设计模式第一篇概述耦合UML七大原则详细分析总结基于Java

迷茫了一周,一段时间反复的 CRUD,着实让我有点烦闷,最近打算将这些技术栈系列的文章先临时搁置一下,开启一个新的篇章《设计模式》,毕竟后面写了不少“文治招式”的文章,也该晋升一下内功了

一 设计模式概述

(一) 什么是设计模式

设计模式,即 Design Patterns,是指在软件设计中,被重复应用的一种代码设计教训。应用设计模式的目标是为了可重用代码,进步代码的可扩展性和可维护性

1995 年,GoF(Gang of Four, 四人组 / 四人帮)单干出版了《设计模式:可复用面向对象软
件的根底》一书,收录了 23 种设计模式,从此建立了软件设计模式畛域的里程碑,【GoF 设计模式】

(二) 为什么学习设计模式

后面咱们学习了 N 种不同的技术,然而归根结底,也只是 CRUD 与 调用之间的堆砌,或者这个创意亦或是业务很欠缺、很弱小,其中也奇妙使用了各种高效的算法,然而说白了,这也只是为了实现或者说解决某个问题而做的

还有时候,两个人同时开发一款雷同的产品,均满足了预期的需要,然而 A 的程序,不仅 代码健壮性强 ,同时 前期保护扩大更是便捷(这种感觉,咱们会在前面具体的设计模式中愈发的感觉到)而 B 的代码却是一言难尽啊

有一句话总结的十分好:

  • 设计模式的实质是面向对象设计准则的理论使用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充沛了解

也就是说,毕竟像例如 Java 这样面向对象的语言中,如何实现一个可保护,可保护的代码,那必然就是要升高代码耦合度,适当复用代码,而要实现这所有,就须要充沛的利用 OOP 编程的个性和思维

注: 上面第二大点补充【耦合】的相干概念, 若不须要跳转第三四大点【UML 类图及类图间的关系】/【设计模式七大准则】

在之前我写 Spring 依赖注入的时候【万字长文】Spring 框架层层递进轻松入门(0C 和 D), 就是从传统开发, 讲到了如何通过工厂模式,以及多例到单例的改良,来一步步实现解耦,有趣味的敌人能够看一下哈

二 什么是耦合?(高 / 低)

作为一篇老手都能看懂的文章,开始就一堆 IOC AOP 等专业名词扔出去,如同是不太礼貌,我得把须要铺垫的常识给大家尽量说一说,如果对这块比拟明确的大佬,间接略过就 OK 了

耦合,就是模块间关联的水平,每个模块之间的分割越多,也就是其耦合性越强,那么独立性也就越差了,所以咱们在软件设计中,应该尽量做到 低耦合,高内聚

生存中的例子:家里有一条串灯,下面有很多灯泡,如果灯坏了,你须要将整个灯带都换掉,这就是高耦合的体现,因为灯和灯带之间是严密相连,不可分割的,然而如果灯泡能够随便装配,并不影响整个灯带,那么这就叫做低耦合

代码中的例子:来看一个多态的调用,前提是 B 继承 A,援用了很屡次

A a = new B();
a.method();

如果你想要把 B 变成 C,就须要批改所有new B() 的中央为 new C() 这也就是高耦合

如果如果应用咱们明天要说的 spring 框架 就能够大大的升高耦合

A a = BeanFactory().getBean(B 名称);
a.method();

这个时候,咱们只须要将 B 名称改为 C,同时将配置文件中的 B 改为 C 就能够了

常见的耦合有这些分类:

(一) 内容耦合

当一个模块间接批改或操作另一个模块的数据, 或者间接转入另一个模块时,就产生了内容耦合。此时,被批改的模块齐全依赖于批改它的模块。这种耦合性是很高的,最好防止

public class A {public int numA = 1;}

public class B {public static A a = new A();
    public static void method(){a.numA += 1;}
    public static void main(String[] args) {method();
       System.out.println(a.numA);
    }
}

(二) 公共耦合

两个以上的模块独特援用一个全局数据项就称为公共耦合。大量的公共耦合构造中,会让你很难确定是哪个模块给全局变量赋了一个特定的值

(三) 内部耦合

一组模块都拜访同一全局简略变量,而且不通过参数表传递该全局变量的信息,则称之为内部耦合 从定义和图中也能够看出,公共耦合和内部耦合的区别就在于 前者是全局数据结构 后者是全局简略变量

(四) 管制耦合

管制耦合。一个模块通过接口向另一个模块传递一个管制信号,承受信号的模块依据信号值而进行适当的动作,这种耦合被称为管制耦合,也就是说,模块之间传递的不是数据,而是一些标记,开关量等等

(五) 标记耦合

标记耦合指两个模块之间传递的是数据机构,如高级语言的数组名、记录名、文件名等这些名字即为标记,其实传递的是这个数据结构的地址

(六) 数据耦合

模块之间通过参数来传递数据,那么被称为数据耦合。数据耦合是最低的一种耦合形 式,零碎中个别都存在这种类型的耦合,因为为了实现一些有意义的性能,往往须要将某些模块的输入数据作为另 一些模块的输出数据

(七) 非间接耦合

两个模块之间没有间接关系,它们之间的分割齐全是通过主模块的管制和调用来实现的

三 UML 类图及类图之间的关系

在一个绝对欠缺的软件系统中,每个类都有其责任,类与类之间,类与接口之间同时也存在着各种关系,UML(对立建模语言)从不同的角度定义了多种图,在软件建模时十分罕用,上面咱们说一下在设计模式中波及绝对较多的 类图,因为在前面单个设计模式的解说中,咱们会波及到,也算是一个根底铺垫。

(一) 类

类是一组相干的属性和行为的汇合,是一个形象的概念,在 UML 中,个别用一个分为三层的矩形框来代表类

  • 第一层:类名称, 是一个字符串,例如 Student
  • 第二层:类属性(字段、成员变量)格局如下:

    • [可见性]属性名: 类型[= 默认值]
    • 例如:-name:String
  • 第三层:类操作(办法、行为),格局如下:

    • [可见性]名称(参数列表)[: 返回类型]
  • 例如:+ display():void

(二) 接口

接口,是一种非凡而又罕用的类,不可被实例化,定义了一些形象的操作(办法),但不蕴含属性其实能见到接口 UML 形容的有三种模式:

  • 第一种:应用一个带有名称的小圆圈来示意,下面的 Dog 是接口名,上面是接口定义的办法
  • 第二种:应用一个“框”来示意,和类很像,然而在最下面特地标注了 <<interface>>

(三) 关系

(1) 依赖关系

定义:如果一个元素 A 的变动影响到另一个元素 B,然而反之却不成立,那么这两个元素 B 和 A 就能够称为 B 依赖 A

  • 例如:开门的人 想要执行 开门 这个动作,就必须借助于 钥匙,这里也就能够说,这个开门的人,依赖于钥匙,如果钥匙产生了什么变动就会影响到开门的人,然而开门的人变动却不会影响到钥匙开门
  • 例如:动物生存须要氧气、水分、食物,这就是一个很字面的依赖关系

依赖关系作为对象之间 耦合度最低 的一种临时性关联形式

在代码中,某个类的办法通过局部变量、办法的参数或者对静态方法的调用来拜访另一个类(被依赖类)中的某些办法来实现一些职责。

(2) 关联关系

关联就是类(精确的说是实例化后的对象)之间的关系,也就是说,如果两个对象须要在肯定工夫内放弃肯定的关系,那么就能够称为关联关系。

  • 例如:学生(Student)在学校(School)学习常识(Knowledge)那么这三者之间就存一个某种分割,能够建设关联关系
  • 例如:大雁(WildGoose)年年南下迁徙,因为它晓得气象(climate)法则

关联关系的单方是能够相互通信的,也就是说,“一个类晓得另一个类”

这种关联是能够 双向的,也能够是单向的

  • 双向的关联能够用带两个箭头或者没有箭头的实线来示意
  • 单向的关联用带一个箭头的实线来示意,箭头从应用类指向被关联的类
  • 也能够在关联线的两端标注角色名,代表两种不同的角色

在代码中通常将一个类的对象作为另一个类的成员变量来实现关联关系

下图是一个老师和学生的双向关联关系

(3) 聚合关系

聚合关系也称为汇集关系,它是一种非凡的较强关联关系。示意类(精确的说是实例化后的对象)之间整体与局部的关系,是一种 has-a 的关系

  • 例如:汽车(Car)有轮胎(Wheel),Car has a Wheel,这就是一个聚合关系,然而轮胎(Wheel)独立于汽车也能够独自存在,轮胎还是轮胎

聚合关系能够用带 空心菱形 的实线箭头来示意,菱形指向整体

(4) 组合关系

组合是一种比聚合更强的关联关系,其也示意类整体和局部之间的关系。然而整体对象能够管制局部对象的生命周期,一旦整体对象隐没,局部也就天然隐没了,即局部不能独立存在

聚合关系能够用带 实心菱形 的实线箭头来示意,菱形指向整体

(5) 泛化关系

泛化形容个别与非凡(类图中“个别”称为超类或父类,“非凡”称为子类)的关系,是父类和子类之间的关系,是一种继承关系,形容了一种 is a kind of 的关系,特地要阐明的是,泛化关系式对象之间耦合度最大的一种关系

Java 中 extend 关键字就代表着这种关系,通常抽象类作为父类,具体类作为子类

  • 例如:交通工具为形象父类,汽车,飞机等就位具体的子类

泛化关系用带空心三角箭头的实线来示意,箭头从子类指向父类

(6) 实现关系

实现关系就是接口和实现类之间的关系,实现类中实现了接口中定义的形象操作

实现关系应用带空心三角箭头的虚线来示意,箭头从实现类指向接口

四 设计模式七大准则

(一) 开闭准则

定义:软件实体该当对扩大凋谢,对批改敞开

咱们在开发任何产品的时候,别指望需要是肯定不变的,当你不得不更改的你的代码的时候,一个高质量的程序就体现出其价值了,它只须要在原来的根底上减少一些扩大,而不至于去批改原先的代码,因为这样的做法经常会牵一发而动全身。

也就是说,开闭准则要求咱们在开发一个软件(模块)的时候,要保障能够在不批改原有代码的模块的根底上,而后能扩大其性能

咱们上面来具体谈谈

(1) 对批改敞开

对批改敞开,即不容许在原来的模块或者代码上进行批改。

A:抽象层次

例如定义一个接口,不同的定义解决思路,会有怎么的差异呢

定义一

boolean connectServer(String ip, int port, String user, String pwd)

定义二

boolean connectServer(FTP ftp)
public class FTP{
    private String ip;
    private int port;
    private String user;
    private String pwd;
    ...... 省略 get set
}

两种形式看似都是差不多的,也都能实现要求,然而如果咱们想要在其根底上减少一个新的参数

  • 如果以定义一的做法,一旦接口被批改,所有调用 connectServer 办法的地位都会呈现问题
  • 如果以定义二的做法,咱们只须要批改 FTP 这个实体类,增加一个属性即可

    • 这种状况下没有用到这个新参数的调用处就不会呈现问题,即便须要调用这个参数,咱们也能够在 FTP 类的构造函数中,对其进行一个默认的赋值解决
B:具体档次

对原有的具体档次的代码进行批改,也是不太好的,尽管带来的变动可能不如抽象层次的大,或者碰巧也没问题,然而这种问题有时候是不可意料的,或者一些不经意的批改会带了和预期齐全不统一的后果

(2) 对扩大凋谢

对扩大凋谢,也就是咱们不须要在原代码上进行批改,因为咱们定义的形象层曾经足够的正当,足够的容纳,咱们只须要依据需要从新派生一个实现类来扩大就能够了

(3) 开发时如何解决

无论模块是如许“关闭”,都会存在一些无奈对之关闭的变动。既然不可能齐全关闭,设计人员必须对他设计的模块应该对那种变动关闭做出抉择,他必须先猜测出最有可能发现的变动品种,而后结构形象来隔离那些变动 ——《大话设计模式》

事后猜想程序的变动,实际上是有很大难度,或者不欠缺,亦或者齐全是谬误的,所以为了躲避这一点,咱们能够抉择在刚开始写代码的时候,假如不会有任何变动呈现,但当变动产生的时候,咱们就要立刻采取行动,通过“形象束缚,封装变动”的形式,创立形象来隔离产生的同类变动

举例:

例如写一个加法程序,很容易就能够写的进去,这个时候变动还没有产生

如果这个时候让你减少一个减法或者乘除等的性能,你就发现,你就须要在原来的类下面批改,这显然违反了“开闭准则”,所以 变动一旦产生,咱们就立刻采取行动 ,决定重构代码,首先 创立一个抽象类 的运算类,通过继承多态等隔离代码,当前还想增加什么类型的运算形式,只须要减少一个新的子类就能够了,也就是说,对程序的改变,是通过新代码进行的,而不是更改现有代码

小结:

  • 咱们心愿开发刚开始就晓得可能产生的变动,因为期待发现变动的工夫越长,要形象代码的代价就越大
  • 不要刻意的去形象,回绝不成熟的形象和形象自身一样重要

(二) 里氏替换准则

(1) 具体阐明

定义:继承必须确保超类所领有的性质在子类中依然成立

里氏替换准则,次要阐明了对于继承的内容,明确了何时应用继承,亦或应用继承的一些规定,是对于开闭准则中抽象化的一种补充

这里咱们次要谈一下,继承带来的问题:

  • 继承是侵入性的,子类继承了父类,就必须领有父类的所有属性和办法,升高了代码灵便度
  • 耦合度变高,一旦 父类的属性和办法被批改,就须要思考子类的批改,或者会造成大量代码重构

里氏替换准则说简略一点就是:它认为,只有当子类能够替换父类,同时程序性能不受到影响,这个父类才算真正被复用

其外围次要有这么四点内容:

  • ① 子类能够实现父类的形象办法,但不能笼罩父类的非形象办法
  • ② 子类中能够减少本人特有的办法
  • ③ 当子类的办法重载父类的办法时,子类办法的前置条件(即办法的输出参数)要比父类的办法更宽松
  • ④ 当子类的办法实现父类的办法时(重写 / 重载或实现形象办法),办法的后置条件(即办法的的输入 / 返回值)要比父类的办法更严格或相等

对照简略的代码来看一下,就高深莫测了

① 子类能够实现父类的形象办法,但不能笼罩父类的非形象办法

前半句很好了解,如果不实现父类的形象办法,会编译报错

后半句是这里的重点,父类中凡是实现好的办法,其实就是在设定整个继承体系中的一系列标准和默认的契约,例如 鸟类 Bird 中,getFlyingSpeed(double speed) 用来获取鸟的飞行速度,但几维鸟作为一种非凡的鸟类,其实是不能航行的,所以须要重写继承的子类办法 getFlyingSpeed(double speed) 将速度置为 0,然而会对整个继承体系造成毁坏

尽管咱们平时常常会通过重写父类办法来实现一些性能,同样这样也很简略,然而一种潜在的继承复用体系就被打乱了,如果在不适当的中央调用重写后的办法,或屡次使用多态,还可能会造成报错

咱们看上面的例子:

父类 Father

public class Father {public void speaking(String content){System.out.println("父类:" + content);
    }
}

子类 Son

public class Son extends Father {
    @Override
    public void speaking(String content) {System.out.println("子类:" + content);
    }
}

子类 Daughter

public class Daughter extends Father{}

测试类 Test

public class Test {public static void main(String[] args) {
        // 间接调用父类运行的后果
        Father father = new Father();
        father.speaking("speaking 办法被调用");

        // Son 子类替换父类运行的后果
        Son son = new Son();
        son.speaking("speaking 办法被调用");

        // Daughter 子类替换父类运行的后果
        Daughter daughter = new Daughter();
        daughter.speaking("speaking 办法被调用");

    }
}

运行后果:

父类: speaking 办法被调用
子类: speaking 办法被调用
父类: speaking 办法被调用

② 子类中能够减少本人特有的办法

这句话了解起来很简略,间接看代码

父类 Father

public class Father {public void speaking(String content){System.out.println("父类:" + content);
    }
}

子类 Son

public class Son extends Father {public void playGuitar () {System.out.println("这是 Son 类 playGuitar 办法");
    }
}

测试类 Test

public class Test {public static void main(String[] args) {
        // 间接调用父类运行的后果
        Father father = new Father();
        father.speaking("speaking 办法被调用");

        // Son 子类替换父类运行的后果
        Son son = new Son();
        son.speaking("speaking 办法被调用");
        son.playGuitar();}
}

运行后果:

父类: speaking 办法被调用
父类: speaking 办法被调用
这是 Son 类 playGuitar 办法

③ 当子类的办法重载父类的办法时,子类办法的前置条件(即办法的输出参数)要比父类的办法更宽松

这里要留神,咱们说的是 重载,可不是重写,上面咱们依照里氏替换准则要求的,将父类办法参数范畴设小一点 (ArrayList),将子类同名办法参数范畴写大一些 (List),测试后的后果,就是只会执行父类的办法,不执行父类重载后的办法(注:参数名尽管雷同,然而类型不同,还是重载,不是重写)

父类 Father

public class Father {public void speaking(ArrayList arrayList) {System.out.println("父类:" + arrayList.get(0));
    }
}

子类 Son

public class Son extends Father {public void speaking(List list) {System.out.println("子类:" + list.get(0));
    }
}

测试类 Test

public class Test {public static void main(String[] args) {ArrayList arrayList = new ArrayList();
        arrayList.add("speaking 办法被调用");

        // 间接调用父类运行的后果
        Father father = new Father();
        father.speaking(arrayList);

        // Son 子类替换父类运行的后果
        Son son = new Son();
        son.speaking(arrayList);
    }
}

运行后果:

父类: speaking 办法被调用
父类: speaking 办法被调用

如果咱们将范畴颠倒一下,将父类办法参数范畴设大一些,子类办法参数设小一些,就会发现我明明想做的是重载办法,而不是重写,然而父类的办法却被执行了,逻辑齐全出错了,所以这也是这一条的反例,并不满足里氏替换准则

父类 Father

public class Father {public void speaking(List list) {System.out.println("父类:" + list.get(0));
    }
}

子类 Son

public class Son extends Father {public void speaking(ArrayList arrayList) {System.out.println("子类:" + arrayList.get(0));
    }
}

测试类 Test

public class Test {public static void main(String[] args) {ArrayList arrayList = new ArrayList();
        arrayList.add("speaking 办法被调用");

        // 间接调用父类运行的后果
        Father father = new Father();
        father.speaking(arrayList);

        // Son 子类替换父类运行的后果
        Son son = new Son();
        son.speaking(arrayList);
    }
}

运行后果:

父类: speaking 办法被调用
子类: speaking 办法被调用

④ 当子类的办法实现父类的办法时(重写 / 重载或实现形象办法),办法的后置条件(即办法的的输入 / 返回值)要比父类的办法更严格或相等

父类中定义一个形象办法,返回值类型是 List,子类中重写这个办法,返回值类型能够为 List,也能够更准确或更严格,例如 ArrayList

父类 Father

public abstract class Father {public abstract List speaking();
}

子类 Son

public class Son extends Father {
    @Override
    public ArrayList speaking() {ArrayList arrayList = new ArrayList();
        arrayList.add("speaking 办法被调用");
        return arrayList;
    }
}

测试类 Test

public class Test {public static void main(String[] args) {Father father = new Son();
        System.out.println(father.speaking().get(0));
    }
}

运行后果:

speaking 办法被调用

然而,如果反过来,将父类形象办法返回值定义为范畴较小的 ArrayList,将子类重写办法中,反而将返回值类型办法,设置为 List,那么程序在编写的时候就会报错

(2) 修改违反里氏替换准则的代码

当初网上几种比拟经典的反例,“几维鸟不是鸟”,“鲸鱼不是鱼”等等

我打个比方,如果依照惯性和字面意思,如果咱们将几维鸟也继承鸟类

然而几维鸟是不能航行的,所别的鸟通过 setSpeed 办法都能附一个无效的值,然而几维鸟就不得不重写这个 setSpeed 办法,让其设置 flySpeed 为 0,这样曾经违反了里氏替换准则

面对子类如果不能残缺的实现父类的办法,或者父类的办法曾经在子类中产生了“异变”,就例如这里几维鸟非凡的 setSpeed 办法,则个别抉择断开父类和子类的继承关系,从新设计关系

例如:

勾销鸟和几维鸟的继承关系,定义鸟和几维鸟更个别的父类,动物类

(三) 依赖倒置

定义:

  • ① 高层模块不应该依赖低层模块,两者都应该依赖其形象
  • ② 形象不应该依赖细节,细节应该依赖形象

先解释第 ① 点,其实这一点在咱们以往的分层开发中,就曾经用过了,例如咱们的业务层 Service(高层模块)就没有依赖数据拜访层 Dao/Mapper(低层模块),咱们都通过 Mapper 的接口进行拜访,这种状况下,如果数据拜访层的细节产生了变动,那么也不会影响到业务层,然而如果间接依赖于实现,那么就会影响微小

第 ② 点,还是在探讨要进行形象的问题,形象是高层,具体细节是底层,这和前一点也是符合的,正式阐明了一条十分要害的准则“面向接口编程,而非针对事实编程”

举个例子

例如一个 Client 客户想拜访学校的 readBook 办法,能够这么写

public class Client {public void read(ASchool aSchool){System.out.println(aSchool.readBook());
    }
}

然而,这个中央其实就呈现了一个比拟大的问题,咱们就是间接依赖了具体,而不是形象,当咱们想要查看另一个 B 学校的 readBook 办法,就须要将代码批改为

public class Client {public void read(BSchool bSchool){System.out.println(bSchool.readBook());
    }
}

然而开闭准则规定,对批改敞开,所以显著违反了开闭准则,如果咱们将代码形象进去,以接口拜访就能够解决

定义学校接口 ISchool(I 是大写的 i 只是命名习惯问题,无非凡意义)

public interface ISchool {String readBook();
}

学校 A 和 B 别离实现这个接口,而后实现接口办法

public class ASchool implements ISchool {
    @Override
    public String readBook() {return "浏览《Java 编程思维》";}
}

public class BSchool implements ISchool {
    @Override
    public String readBook() {return "浏览《代码整洁之道》";}
}

Client 客户类,调用时,只须要传入接口参数即可

public class Client {public void read(ISchool school){System.out.println(school.readBook());
    }
}

看一下测试类

public class Test {public static void main(String[] args) {Client client = new Client();
        client.read(new ASchool());
        client.read(new BSchool());
    }
}

运行后果

浏览《Java 编程思维》
浏览《代码整洁之道》

(四) 繁多职责准则

定义:繁多职责准则规定一个类应该有且仅有一个引起它变动的起因,否则类应该被拆分

一个类,并不应该承当太多的责任,否则当为了引入类中的 A 职责的时候,就不得不把 B 职责 也引入,所以咱们必须满足其高内聚以及细粒度

长处:

  • 升高类的复杂度。一个类只负责一项职责,其逻辑必定要比负责多项职责简略得多。
  • 进步类的可读性。复杂性升高,天然其可读性会进步。
  • 进步零碎的可维护性。可读性进步,那天然更容易保护了。
  • 变更引起的危险升高。变更是必然的,如果繁多职责准则恪守得好,当批改一个性能时,能够显著升高对其余性能的影响。

就比方大学老师,负责很多很多工作,然而不论是辅导员,授课老师,行政老师,尽管都能够统称为老师,然而将大量的内容和职责放到一个类中,显然是不合理的,不如细分开来

例如:

补充:大家可能看过“羊呼吸空气,鱼呼吸水”的例子,这里我不做演示,做一个阐明,有时候,在类简略的状况下,也能够在代码或者办法级别上违反繁多职责准则,因为即便肯定的批改有肯定开销,然而简直能够忽略不计了,不过个别状况,咱们还是要遵循繁多职责准则

(五) 接口隔离准则

定义:

  • 客户端不应该被迫依赖于它不应用的办法
  • 或者——客户端不应该被迫依赖于它不应用的办法

其实这一准则的外围就是“拆”,如果在一个接口内存放过多的办法等内容,就会非常臃肿,竟可能的细化接口,也就是为每个类创立专用接口,毕竟依赖多个专用接口,比依赖一个综合接口更加灵便不便,同时,接口作为对外的一个“入口”,拆散,隔离接口可能放大外来因素导致的问题扩散范畴

还是通过一个例子来开展:

当初有一个“好学生的接口和实现类”,还有一个老师的抽象类和其子类,老师能做的,就是去找到好的学生

好学生 IGoodStudent 接口

public interface IGoodStudent {
    // 学习成绩优秀
    void goodGrades();
    // 道德优良
    void goodMoralCharacter();
    // 良好形象
    void goodLooks();}

好学生 IGoodStudent 接口的实现类 GoodStudentImpl

public class GoodStudentImpl implements IGoodStudent {

    private String name;

    public GoodStudentImpl(String  name) {this.name = name;}

    @Override
    public void goodGrades() {System.out.println("【" +this.name + "】的学习成绩优秀");
    }

    @Override
    public void goodMoralCharacter() {System.out.println("【" +this.name + "】的道德低劣");
    }

    @Override
    public void goodLooks() {System.out.println("【" +this.name + "】的形象良好");
    }
}

老师抽象类 AbstractTeacher

public abstract class AbstractTeacher {
    protected IGoodStudent goodStudent;

    public AbstractTeacher(IGoodStudent goodStudent) {this.goodStudent = goodStudent;}

    public abstract void findGoodStudent();}

老师类 Teacher

public class Teacher extends AbstractTeacher {public Teacher(IGoodStudent goodStudent) {super(goodStudent);
    }

    @Override
    public void findGoodStudent() {super.goodStudent.goodGrades();
        super.goodStudent.goodMoralCharacter();
        super.goodStudent.goodLooks();}
}

测试类 Test

public class Test {public static void main(String[] args) {IGoodStudent goodStudent = new GoodStudentImpl("阿文");
        AbstractTeacher teacher = new Teacher(goodStudent);
        teacher.findGoodStudent();}
}

运行后果:

【阿文】的学习成绩优秀
【阿文】的道德低劣
【阿文】的形象良好

一下子看来是没什么问题的,不过因为每个人的主观意识形态不同,或者每个人对于“好学生”的定义并不同,就例如就我集体而言,我意识为“师者,传道授业解惑也”,学生能学习其为人处世的情理与被动学习更是难能可贵,至于外貌更属于无稽之谈。针对不同人的不同不同定义,这个 IGoodStudent 接口就显得有一些宏大且不合时宜了,所以咱们依据接口隔离准则,将“好学生”的定义进行肯定的拆分隔离

学习的学生接口

public interface IGoodGradesStudent {
    // 学习成绩优秀
    void goodGrades();}

道德优良的学生接口

public interface IGoodMoralCharacterStudent {
    // 道德优良
    void goodMoralCharacter();}

好学生实现多个接口

public class GoodStudent implements IGoodGradesStudent,IGoodMoralCharacterStudent {

    private String name;

    public GoodStudent(String name) {this.name = name;}

    @Override
    public void goodGrades() {System.out.println("【" +this.name + "】的学习成绩优秀");
    }

    @Override
    public void goodMoralCharacter() {System.out.println("【" +this.name + "】的道德低劣");
    }
}

(六) 迪米特法令

定义:如果两个类不必要彼此间接通信,那么这两个类就不该当产生间接的相互作用,如果其中一个类须要调用另一个类的某一个办法的话,能够通过第三者转发这个调用

这句话的意思就是说,一个类对本人依赖的类晓得越少越好,也就是每一个类都应该升高成员的拜访权限,就像封装的概念中提到的,通过 private 暗藏本人的字段或者行为细节

迪米特法令中的“敌人”是指:以后对象自身、以后对象的成员对象、以后对象所创立的对象、以后对象的办法参数等 这些对象 以后对象 存在 关联、聚合或组合关系,能够间接拜访这些对象的办法

留神:请不要过分的应用迪米特法令,因为其会产生过多的两头类,会导致系统复杂性增大,构造不够清晰

上面还是用一个例子来说一下

假如在学校的一个环境中,校长作为最高的职务所有人,必定不会直接参与到对于老师和学生的治理中,而是通过一层一层的管理体系来进行统筹规划,这里的校长,和老师学生之间就能够了解为生疏关系,而校长和中层的教务主任却是敌人关系,毕竟教务主任数量少,也能够间接进行沟通

教务主任类 AcademicDirector

public class AcademicDirector {

    private Principal principal;
    private Teacher teacher;
    private Student student;

    public void setPrincipal(Principal principal) {this.principal = principal;}

    public void setTeacher(Teacher teacher) {this.teacher = teacher;}

    public void setStudent(Student student) {this.student = student;}

    public void meetTeacher() {System.out.println(teacher.getName() + "通过教务主任向" + principal.getName() + "汇报工作");
    }

    public void meetStudents() {System.out.println(student.getName() + "通过教务主任与" + principal.getName() + "见面");
    }

}

校长类 Principal

public class Principal {
    private String name;

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

    public String getName() {return name;}
}

老师类 Teacher

public class Teacher {
    private String name;

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

    public String getName() {return name;}
}

学生类 Student

public class Student {
    private String name;

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

    public String getName() {return name;}
}

测试类 Test

public class Test {public static void main(String[] args) {AcademicDirector a = new AcademicDirector();
        a.setPrincipal(new Principal("【张校长】"));

        a.setTeacher(new Teacher("【王老师】"));
        a.setStudent(new Student("【阿文】"));

        a.meetTeacher();
        a.meetStudents();}
}

补充:迪米特法令在《程序员修炼之道》一书中也有提及到 —— 26 解耦与得墨忒耳法令

函数的得墨忒耳法令试图使任何给定程序中的模块之间的耦合减至起码,它设法阻止你为了取得对第三个对象的办法的拜访而进入某个对象。

通过应用函数的得墨忒耳法令来解耦 编写“羞涩”的代码,咱们能够实现咱们的指标:

Minimize Coupling Between Modules

使模块之间的耦合减至起码

(七) 合成复用准则

定义:在软件复用时,要尽量先应用组合或者聚合等关联关系来实现,其次才思考应用继承关系来实现

这一点和里氏替换准则的目标是统一的,都是解决对于继承的内容,实质都是实现了开闭准则的具体标准

为什么用组合 / 聚合,不必继承

  • 继承毁坏了类的封装性,因为父类对于子类是通明的,而组合 / 聚合则不会
  • 继承父子类之间之间的耦合度比组合 / 聚合新旧类高
  • 从父类继承来的实现是动态的,运行时不会发生变化,而组合 / 聚合的复用灵活性高,复用可在运行时动静进行

如果代码违反了里氏替换准则,补救的形式,一个就是咱们后面说的,退出一个更一般的形象超类,一个就是勾销继承,批改为组合 / 聚合关系

咱们简略回顾一下

  • 继承咱们个别都叫做 Is-a 的关系,即一个类是另一个类的一种,比方,狗是一种动物
  • 组合 / 聚合都叫做 Has-a,即一个角色领有一项责任或者说个性

例如咱们来讨论一下常见的非凡自行车(即变速自行车),首先依照类型能够分为 山地自行车和公路自行车,依照速度搭配又能够分为 21 速自行车,24 速自行车,27 速自行车(简略分)

XX 速山地自行 / 公路车,尽管说咱们口头上可能会这么叫,然而其实这就是将速度这种 Has- a 的关系和 Is-a 的关系搞混了,而且如果通过继承,会带来很多的子类,一旦想要减少批改变速自行车品种以及速度类型,就须要批改源代码,违反了开闭准则,所以批改为组合关系

五 结尾

这篇文章写到这里就完结了,又是一篇 靠近 1W 字的内容,学习到肯定阶段,的确会有一些瓶颈,通过对于相似设计模式等“内功”的学习,也忽然发现开发真不是 CRUD 的一直反复,一段有品质的代码,更能让人有成就感,前面对于常见的设计模式我会始终更新上来,一边学习,一边总结,感激大家的反对。

退出移动版