设计模式的分类
整体来讲设计模式分为3大类:
-
创建型模式,共5种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
-
结构型模式,共7种:适配器模式、装潢器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
-
行动型模式,共101种:策略模式、模板方法模式、视察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
6大原则
单1职责原则Single Responsibility Principle
定义:1个类或1个接口,最好只负责1项职责。
问题由来:类T负责两个不同的职责P1和P2。由于职责P1需要产生改变而需要修改T类时,有可能致使原来运行正常的职责P2功能产生故障。
解决方法:遵守单1职责原则。分别建立两个类T1和T2,使类T1负责职责P1,类T2负责职责P2。这样,当修改类T1也不会影响职责P2;同理,当修改类T2时不会影响职责P1。
有时候也会有背背这1原则的代码存在。由于有职责分散,就是由于某种缘由,职责P被分化为粒度更细的职责P1和P2。比如:类T只负责1个职责P,这样设计是符合单1职责原则的。后来由于某种缘由,或许是需求变更了,或许是程序的设计者境地提高了,需要将职责P细分为粒度更细的职责P1,P2,这时候如果要使程序遵守单1职责原则,需要将类T也分解为两个类T1和T2,分别负责P1、P2两个职责。但是在程序已写好的情况下,这样做简直太费时间了。所以,简单的修改类T,用它来负责两个职责是1个比较不错的选择,虽然这样做有悖于单1职责原则。这样做的风险在于职责分散的不肯定性,由于我们不会想到这个职责P,在未来可能会分散为P1,P2,P3,P4,Pn。所以记住,在职责分散到我们没法控制的程度之前,立刻对代码进行重构。
举例说明,用1个类描写动物呼吸这个场景:
public class SingleResponsibilityPrinciple { public static void main(String[] args) {
Animal animal = new Animal();
animal.breathe("牛");
animal.breathe("羊");
animal.breathe("猪");
}
}
class Animal { public void breathe(String animal) {
System.out.println(animal + "呼吸空气");
}
}
程序上线后,发现问题了,其实不是所有的动物都呼吸空气的,比如鱼就是呼吸水的。修改时如果遵守单1职责原则,需要Animal类细分为陆生动物类Terrestrial,水生动物Aquatic,代码以下:
public class SingleResponsibilityPrinciple { public static void main(String[] args) {
Terrestrial terrestrial = new Terrestrial();
terrestrial.breathe("牛");
terrestrial.breathe("羊");
terrestrial.breathe("猪");
Aquatic aquatic = new Aquatic();
aquatic.breathe("鱼");
}
}
class Terrestrial { public void breathe(String animal) {
System.out.println(animal + "呼吸空气");
}
}
class Aquatic { public void breathe(String animal) {
System.out.println(animal + "呼吸水");
}
}
我们会发现如果这样修改花消是很大的,除将原来的类分解以外,还需要修改客户端。而直接修改类Animal来达成目的虽然背背了单1职责原则,但花消却小的多,代码以下:
public class SingleResponsibilityPrinciple { public static void main(String[] args) {
Animal animal = new Animal();
animal.breathe("牛");
animal.breathe("羊");
animal.breathe("猪");
animal.breathe("鱼");
}
}
class Animal { public void breathe(String animal) { if ("鱼".equals(animal)) {
System.out.println(animal + "呼吸水");
} else {
System.out.println(animal + "呼吸空气");
}
}
}
可以看到,这类修改方式要简单的多。但是却存在着隐患:有1天需要将鱼分为呼吸淡水的鱼和呼吸海水的鱼,则又需要修改Animal类的breathe方法,而对原有代码的修改会对调用猪,牛,羊等相干功能带来风险,或许某1天你会发现程序运行的结果变成牛呼吸水了。
这类修改方式直接在代码级别上背背了单1职责原则,虽然修改起来最简单,但隐患却是最大的。还有1种修改方式:
public class SingleResponsibilityPrinciple { public static void main(String[] args) {
Animal animal = new Animal();
animal.breathe("牛");
animal.breathe("羊");
animal.breathe("猪");
animal.breathe2("鱼");
}
}
class Animal { public void breathe(String animal) {
System.out.println(animal + "呼吸空气");
} public void breathe2(String animal) {
System.out.println(animal + "呼吸水");
}
}
可以看到,这类修改方式没有改动原来的方法,而是在类中新加了1个方法,这样虽然也背背了单1职责原则,但在方法级别上却是符合单1职责原则的,由于它并没有动原来方法的代码。这3种方式各有优缺点,那末在实际编程中,需要根据实际情况来肯定。我的原则是:只有逻辑足够简单,才可以在代码级别上违背单1职责原则;只有类中方法数量足够少,才可以在方法级别上违背单1职责原则;
遵守单1职责原的优点有:类的复杂性将下降,简单明细的代码将使可读性将大大提高,自但是然可保护性亦将同步提高。变更引发的风险下降,变更是必定的,如果单1职责原则遵照的好,当修改1个功能时,可以显著下降对其他功能的影响。
里氏替换原则Liskov Substitution Principle
肯定有很多人跟我刚看到这项原则的时候1样,对这个原则的名字充满疑惑。其实缘由就是这项原则最早是在1988年,由麻省理工学院的1位姓里的女士(Barbara Liskov)提出来的。
里氏替换原则的核心精神是:在使用基类的的地方可以任意使用其子类,能保证子类完善替换基类;这1精神实际上是对继承机制束缚规范的体现。在父类和子类的具体实现中,严格控制继承层次中的关系特点,以保证用子类替换基类时,程序行动不产生问题,且能正常进行下去。
里氏替换原则主要发力点是继承基础上的抽象和多态,具体就是子类必须实现父类的方法,是重写;这里要注意重写(Override)与重载(Overload)的辨别,即便参数的数据范围产生变化,也能将重写变成重载!而你本来只是想把所继承的方法完善的具体点儿!如果是这样的话绝对会引发以后业务逻辑的混乱。
里氏替换原则是关于继承机制的设计原则,违背里氏替换原则将会使继承变的1塌胡涂;而遵守里氏替换原则能够保证系统具有良好的的拓展性,我们可以随时根据需要增改不同的子类,这将大大增强程序的硬朗性,让版本的升级可以做到非常好的兼容;同时基于多态的抽象机制,能够很好的减少代码冗余,避免运行期的类型辨别等;而在项目的实行中不同的子类对应着不同的业务,使用父类做参数,不同子类可以轮番上阵,必定强大!
定义2:所有援用基类的地方必须能透明地使用其子类的对象。
问题由来:有1功能P1,由类A完成。现需要将功能P1进行扩大,扩大后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会致使原有功能P1产生故障。
解决方案:当使用继承时,遵守里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽可能不要重写父类A的方法,也尽可能不要重载父类A的方法。
继承包括这样1层含义:父类中凡是已实现好的方法(相对抽象方法而言),实际上是在设定1系列的规范和契约,虽然它不强迫要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对全部继承体系造成破坏。而里氏替换原则就是表达了这1层含义。
继承作为面向对象3大特性之1,在给程序设计带来巨大便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性下降,增加了对象间的耦合性,如果1个类被其他的类所继承,则当这个类需要修改时,必须斟酌到所有的子类,并且父类修改后,所有触及到子类的功能都有可能会产生故障。
举例说明继承的风险,我们需要完成1个两数相减的功能,由类A来负责。
public class LiskovSubstitutionPrinciple { public static void main(String[] args) {
A a = new A();
System.out.println("100⑸0=" + a.func1(100, 50));
System.out.println("100⑻0=" + a.func1(100, 80));
}
}
class A { public int func1(int a, int b) { return a - b;
}
}
后来,我们需要增加1个新的功能:完成两数相加,然后再与100求和,由类B来负责。即类B需要完成两个功能:
public class LiskovSubstitutionPrinciple { public static void main(String[] args) {
B b = new B();
System.out.println("100⑸0=" + b.func1(100, 50));
System.out.println("100⑻0=" + b.func1(100, 80));
System.out.println("100+20+100=" + b.func2(100, 20));
}
} class A { public int func1(int a, int b) { return a - b;
}
} class B extends A { @Override public int func1(int a, int b) { return a + b;
} public int func2(int a, int b) { return func1(a, b) + 100;
}
}
我们发现本来运行正常的相减功能产生了毛病。缘由就是类B在给方法起名时无意中重写了父类的方法,造成所有运行相减功能的代码全部调用了类B重写后的方法,造成本来运行正常的功能出现了毛病。在本例中,援用基类A完成的功能,换成子类B以后,产生了异常。在实际编程中,我们常常会通太重写父类的方法来完成新的功能,这样写起来虽然简单,但是全部继承体系的可复用性会比较差,特别是应用多态比较频繁时,程序运行出错的概率非常大。如果非要重写父类的方法,比较通用的做法是:原来的父类和子类都继承1个更通俗的基类,原本的继承关系去掉,采取依赖、聚合,组合等关系代替。
里氏替换原则通俗的来说就是:子类可以扩大父类的功能,但不能改变父类原本的功能。它包括以下4层含义:
-
子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
-
子类中可以增加自己独有的方法。
-
当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
-
当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
看上去很不可思议,由于我们会发现在自己编程中常常会违背里氏替换原则,程序照样跑的好好的。所以大家都会产生这样的疑问,假设我非要不遵守里氏替换原则,你写的代码出问题的概率将会大大增加。
依赖颠倒原则Dependence Inversion Principle
定义:高层模块不应当依赖低层模块,2者都应当依赖其抽象;抽象不应当依赖细节;细节应当依赖抽象。其核心思想是:依赖于抽象。
问题由来:类A直接依赖类B,假设要将类A改成依赖类C,则必须通过修改类A的代码来达成。这类场景下,类A1般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假设修改类A,会给程序带来没必要要的风险。
解决方案:将类A修改成依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或类C产生联系,则会大大下降修改类A的概率。
依赖颠倒原则基于这样1个事实:相对细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。
在java中,抽象指的是接口或抽象类,细节就是具体的实现类,使用接口或抽象类的目的是制定好规范和契约,而不去触及任何具体的操作,把展现细节的任务交给他们的实现类去完成。
依赖颠倒原则的核心思想是面向接口编程,我们照旧用1个例子来讲明面向接口编程比相对面向实现编程好在甚么地方。场景是这样的,母亲给孩子讲故事,只要给她1本书,她就能够照着书给孩子讲故事了。代码以下:
public class DependenceInversionPrinciple { public static void main(String[] args) {
Mother mother = new Mother();
mother.narrate(new Book());
}
}
class Book { public String getContent() { return "很久很久之前有1个阿拉伯的故事……";
}
}
class Mother { public void narrate(Book book) {
System.out.println("妈妈开始讲故事");
System.out.println(book.getContent());
}
}
运行良好,假设有1天,需求变成这样:不是给书而是给1份报纸,让这位母亲讲1下报纸上的故事,报纸的代码以下:
class Newspaper{ public String getContent(){ return "林书豪38+7领导尼克斯击败湖人……";
}
}
这位母亲却办不到,由于她竟然不会读报纸上的故事,这太荒唐了,只是将书换成报纸,竟然必须要修改Mother才能读。假设以后需求换成杂志呢?换成网页呢?还要不断地修改Mother,这明显不是好的设计。缘由就是Mother与Book之间的耦合性太高了,必须下降他们之间的耦合度才行。我们引入1个抽象的接口IReader。读物,只要是带字的都属于读物:
public class DependenceInversionPrinciple { public static void main(String[] args) {
Mother mother = new Mother();
mother.narrate(new Book());
mother.narrate(new Newspaper());
}
} interface IReader { public String getContent();
} class Newspaper implements IReader { public String getContent() { return "林书豪17+9助尼克斯击败老鹰……";
}
} class Book implements IReader { public String getContent() { return "很久很久之前有1个阿拉伯的故事……";
}
} class Mother { public void narrate(IReader reader) {
System.out.println("妈妈开始讲故事");
System.out.println(reader.getContent());
}
}
这样修改后,不管以后怎样扩大IReader类,都不需要再修改Mother类了。这只是1个简单的例子,实际情况中,代表高层模块的Mother类将负责完成主要的业务逻辑,1旦需要对它进行修改,引入毛病的风险极大。所以遵守依赖颠倒原则可以下降类之间的耦合性,提高系统的稳定性,下降修改程序酿成的风险。
采取依赖颠倒原则给多人并行开发带来了极大的便利,比如上例中,本来Mother类与Book类直接耦合时,Mother类必须等Book类编码完成后才可以进行编码,由于Mother类依赖于Book类。修改后的程序则可以同时开工,互不影响,由于Mother与Book类1点关系也没有。参与协作开发的人越多、项目越庞大,采取依赖致使原则的意义就越重大。
传递依赖关系有3种方式,以上的例子中使用的方法是接口传递,另外还有两种传递方式:构造方法传递和setter方法传递,相信誉过Spring框架的,对依赖的传递方式1定不会陌生。
在实际编程中,我们1般需要做到以下3点:
-
低层模块尽可能都要有抽象类或接口,或二者都有。
-
变量的声明类型尽可能是抽象类或接口。
-
使用继承时遵守里氏替换原则。
依赖颠倒原则的核心就是要我们面向接口编程,理解了面向接口编程,也就理解了依赖颠倒。
接口隔离原则Interface Segregation Principle
定义:客户端不应当依赖它不需要的接口;1个类对另外一个类的依赖应当建立在最小的接口上。 否则将会造成接口污染。类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对类A和类B来讲不是最小接口,则类B和类D必须去实现他们不需要的方法。
解决方案:将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采取接口隔离原则。
举例来讲明接口隔离原则:
public class InterfaceSegregationPrinciple { public static void main(String[] args) {
A a = new A();
a.depend1(new B());
a.depend2(new B());
a.depend3(new B());
C c = new C();
c.depend1(new D());
c.depend2(new D());
c.depend3(new D());
}
} interface I { public void method1(); public void method2(); public void method3(); public void method4(); public void method5();
}
class A { public void depend1(I i) {
i.method1();
} public void depend2(I i) {
i.method2();
} public void depend3(I i) {
i.method3();
}
}
class B implements I { public void method1() {
System.out.println("类B实现接口I的方法1");
} public void method2() {
System.out.println("类B实现接口I的方法2");
} public void method3() {
System.out.println("类B实现接口I的方法3");
} public void method4() {
} public void method5() {
}
}
class C { public void depend1(I i) {
i.method1();
} public void depend2(I i) {
i.method4();
} public void depend3(I i) {
i.method5();
}
}
class D implements I { public void method1() {
System.out.println("类D实现接口I的方法1");
} public void method2() {
} public void method3() {
} public void method4() {
System.out.println("类D实现接口I的方法4");
} public void method5() {
System.out.println("类D实现接口I的方法5");
}
}
可以看到,如果接口过于臃肿,只要接口中出现的方法,不管对依赖于它的类有无用途,实现类中都必须去实现这些方法,这明显不是好的设计。如果将这个设计修改成符合接口隔离原则,就必须对接口I进行拆分。在这里我们将原本的接口I拆分为3个接口,代码以下:
public class InterfaceSegregationPrinciple { public static void main(String[] args) {
A a = new A();
a.depend1(new B());
a.depend2(new B());
a.depend3(new B());
C c = new C();
c.depend1(new D());
c.depend2(new D());
c.depend3(new D());
}
} interface I1 { public void method1();
} interface I2 { public void method2(); public void method3();
} interface I3 { public void method4(); public void method5();
}
class A { public void depend1(I1 i) {
i.method1();
} public void depend2(I2 i) {
i.method2();
} public void depend3(I2 i) {
i.method3();
}
}
class B implements I1, I2 { public void method1() {
System.out.println("类B实现接口I1的方法1");
} public void method2() {
System.out.println("类B实现接口I2的方法2");
} public void method3() {
System.out.println("类B实现接口I2的方法3");
}
}
class C { public void depend1(I1 i) {
i.method1();
} public void depend2(I3 i) {
i.method4();
} public void depend3(I3 i) {
i.method5();
}
}
class D implements I1, I3 { public void method1() {
System.out.println("类D实现接口I1的方法1");
} public void method4() {
System.out.println("类D实现接口I3的方法4");
} public void method5() {
System.out.println("类D实现接口I3的方法5");
}
}
接口隔离原则的含义是:建立单1接口,不要建立庞大臃肿的接口,尽可能细化接口,接口中的方法尽可能少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立1个很庞大的接口供所有依赖它的类去调用。本文例子中,将1个庞大的接口变更加3个专用的接口所采取的就是接口隔离原则。在程序设计中,依赖几个专用的接口要比依赖1个综合的接口更灵活。接口是设计时对外部设定的契约,通过分散定义多个接口,可以预防外来变更的分散,提高系统的灵活性和可保护性。
说到这里,很多人会觉的接口隔离原则跟之前的单1职责原则很类似,其实不然。其1,单1职责原则原重视的是职责;而接口隔离原则重视对接口依赖的隔离。其2,单1职责原则主要是束缚类,其次才是接口和方法,它针对的是程序中的实现和细节;而接口隔离原则主要束缚接口,主要针对抽象,针对程序整体框架的构建。
采取接口隔离原则对接口进行束缚时,要注意以下几点:
接口尽可能小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果太小,则会造成接口数量过量,使设计复杂化。所以1定要适度。为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为1个模块提供定制服务,才能建立最小的依赖关系。提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
应用接口隔离原则,1定要适度,接口设计的过大或太小都不好。设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这1原则。
现实中,如何掌控接口越小越好,这个度很难界定,颗粒度小固然灵活,但同时会造成结构的复杂化,以下有几个掌控规则可以参考:
-
1个接口只服务于1个子模块或业务逻辑,服务定制;
-
通过业务逻辑紧缩接口中的public方法,让接口看起来精悍;
-
已被污染了的接口,尽可能修改,如果变更风险太大,则用适配器模式进行转化处理;
-
根据具体的业务,深入了解逻辑,用心感知去控制设计思路。
具体如何实行接口隔离,主要有两种方法:
1. 拜托分离,通过增加1个新的接口类型来拜托客户的要求,隔离客户和接口的直接依赖,注意这同时也会增加系统的开消;
2. 多重继承分离,通过接口的多重继承来实现客户的需求,这类方式相对较好。具体的使用,视情况而定。
迪米特法则Demeter Principle
定义:1个对象应当对其他对象保持最少的了解。其核心精神是:不和陌生人说话,通俗之意是1个对象对自己需要耦合关联调用的类应当知道的更少。这样会致使类之间的耦合度下降,每一个类都尽可能减少对其他类的依赖,因此,这也很容易使得系统的功能模块相互独立,之间不存在很强的依赖关系。
问题由来:类与类之间的关系越密切,耦合度越大,当1个类产生改变时,对另外一个类的影响也越大。
解决方案:尽可能下降类与类之间的耦合。
自从我们接触编程开始,就知道了软件编程的总的原则:低耦合,高内聚。不管是面向进程编程还是面向对象编程,只有使各个模块之间的耦合尽可能的低,才能提高代码的复用率。低耦合的优点不言而喻,但是怎样样编程才能做到低耦合呢?那正是迪米特法则要去完成的。迪米特法则又叫最少知道原则,最早是在1987年由美国Northeastern University的Ian Holland提出。通俗的来说,就是1个类对自己依赖的类知道的越少越好。也就是说,对被依赖的类来讲,不管逻辑多么复杂,都尽可能地的将逻辑封装在类的内部,对外除提供的public方法,不对外泄漏任何信息。迪米特法则还有1个更简单的定义:只与直接的朋友通讯。首先来解释1下甚么是直接的朋友:每一个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的情势出现在类的内部。
举1个例子:有1个团体公司,下属单位有分公司和直属部门,现在要求打印出所有下属单位的员工ID。先来看1下违背迪米特法则的设计。
public class LowOfDemeter { public static void main(String[] args) {
CompanyManager e = new CompanyManager();
e.printAllEmployee(new SubCompanyManager());
}
} class Employee { private String id; public void setId(String id) { this.id = id;
} public String getId() { return id;
}
} class SubEmployee { private String id; public void setId(String id) { this.id = id;
} public String getId() { return id;
}
}
class SubCompanyManager { public ListgetAllEmployee() {
Listlist = new ArrayList(); for (int i = 0; i < 5; i++) {
SubEmployee emp = new SubEmployee(); emp.setId("分公司" + i);
list.add(emp);
} return list;
}
}
class CompanyManager { public ListgetAllEmployee() {
Listlist = new ArrayList(); for (int i = 0; i < 10; i++) {
Employee emp = new Employee(); emp.setId("总公司" + i);
list.add(emp);
} return list;
} public void printAllEmployee(SubCompanyManager sub) {
Listlist1 = sub.getAllEmployee(); for (SubEmployee e : list1) {
System.out.println(e.getId());
}
Listlist2 = this.getAllEmployee(); for (Employee e : list2) {
System.out.println(e.getId());
}
}
}
现在这个设计的主要问题出在CompanyManager中,根据迪米特法则,只与直接的朋友产生通讯,而SubEmployee类其实不是CompanyManager类的直接朋友(以局部变量出现的耦合不属于直接朋友),从逻辑上讲总公司只与他的分公司耦合就好了,与分公司的员工并没有任何联系,这样设计明显是增加了没必要要的耦合。依照迪米特法则,应当避免类中出现这样非直接朋友关系的耦合。修改后的代码以下:
public class LowOfDemeter { public static void main(String[] args) {
CompanyManager e = new CompanyManager();
e.printAllEmployee(new SubCompanyManager());
}
} class Employee { private String id; public void setId(String id) { this.id = id;
} public String getId() { return id;
}
} class SubEmployee { private String id; public void setId(String id) { this.id = id;
} public String getId() { return id;
}
}
class SubCompanyManager { public ListgetAllEmployee() {
Listlist = new ArrayList(); for (int i = 0; i < 5; i++) {
SubEmployee emp = new SubEmployee(); emp.setId("分公司" + i);
list.add(emp);
} return list;
} public void printEmployee() {
Listlist = this.getAllEmployee(); for (SubEmployee e : list) {
System.out.println(e.getId());
}
}
}
class CompanyManager { public ListgetAllEmployee() {
Listlist = new ArrayList(); for (int i = 0; i < 10; i++) {
Employee emp = new Employee(); emp.setId("总公司" + i);
list.add(emp);
} return list;
} public void printAllEmployee(SubCompanyManager sub) {
sub.printEmployee();
Listlist2 = this.getAllEmployee(); for (Employee e : list2) {
System.out.println(e.getId());
}
}
}
修改后,为分公司增加了打印人员ID的方法,总公司直接调用来打印,从而避免了与分公司的员工产生耦合。迪米特法则的初衷是下降类之间的耦合,由于每一个类都减少了没必要要的依赖,因此的确可以下降耦合关系。但是凡事都有度,虽然可以免与非直接的类通讯,但是要通讯,必定会通过1个”中介”来产生联系,例如本例中,总公司就是通过分公司这个”中介”来与分公司的员工产生联系的。过分的使用迪米特原则,会产生大量这样的中介和传递类,致使系统复杂度变大。所以在采取迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。
合成复用原则Composite Reuse Principle
原则是尽可能使用合成/聚合的方式,而不是使用继承。
开闭原则Open Close Principle
定义:1个软件实体如类、模块和函数应当对扩大开放,对修改关闭。
问题由来:在软件的生命周期内,由于变化、升级和保护等缘由需要对软件原有代码进行修改时,可能会给旧代码中引入毛病,也可能会使我们不能不对全部功能进行重构,并且需要原有代码经太重新测试。
解决方案:当软件需要变化时,尽可能通过扩大软件实体的行动来实现变化,而不是通过修改已有的代码来实现变化。
开闭原则是面向对象设计中最基础的设计原则,它指点我们如何建立稳定灵活的系统。开闭原则多是设计模式6项原则中定义最模糊的1个了,它只告知我们对扩大开放,对修改关闭,可是到底如何才能做到对扩大开放,对修改关闭,并没有明确的告知我们。之前,如果有人告知我”你进行设计的时候1定要遵照开闭原则”,我会觉的他甚么都没说,但貌似又甚么都说了。由于开闭原则真的太虚了。
在仔细思考和仔细浏览很多设计模式的文章后,终究对开闭原则有了1点认识。其实,我们遵守设计模式前面5大原则,和使用23种设计模式的目的就是遵守开闭原则。也就是说,只要我们对前面5项原则遵照的好了,设计出的软件自然是符合开闭原则的,这个开闭原则更像是前面5项原则遵照程度的”平均得分”,前面5项原则遵照的好,平均分自然就高,说明软件设计开闭原则遵照的好;如果前面5项原则遵照的不好,则说明开闭原则遵照的不好。
其实笔者认为,开闭原则不过就是想表达这样1层意思:用抽象构建框架,用实现扩大细节。由于抽象灵活性好,适应性广,只要抽象的公道,可以基本保持软件架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩大,当软件需要产生变化时,我们只需要根据需求重新派生1个实现类来扩大就能够了。固然条件是我们的抽象要公道,要对需求的变更有前瞻性和预感性才行。
说到这里,再回想1下前面说的5项原则,恰正是告知我们用抽象构建框架,用实现扩大细节的注意事项而已:
-
单1职责原则告知我们实现类要职责单1;
-
里氏替换原则告知我们不要破坏继承体系;
-
依赖颠倒原则告知我们要面向接口编程;
-
接口隔离原则告知我们在设计接口的时候要精简单1;
-
迪米特法则告知我们要下降耦合。
-
而开闭原则是总纲,他告知我们要对扩大开放,对修改关闭。
最后说明1下如何去遵照这6个原则。对这6个原则的遵照其实不是是和否的问题,而是多和少的问题,也就是说,我们1般不会说有无遵照,而是说遵照程度的多少。任何事都是过犹不及,设计模式的6个设计原则也是1样,制定这6个原则的目的其实不是要我们呆板的遵照他们,而需要根据实际情况灵活应用。对他们的遵照程度只要在1个公道的范围内,就算是良好的设计。