设计模式它是一种代码编程长期发展的经验和解决某种问题的通用的思想,并且中所周知的一套代码方法和理念。也是我们编写程序的基石。我日常写代码就好比建造房子一样,首先我们需要搭建好架子。然后根据这个架子慢慢的将整个大厦建起来。同样的,我们在编写程序的时候,也需要遵循一定的原则和框架,这样我们写出来的程序才更见健壮,开发起来也会更加省时间,提高代码的可读性,拓展性,重用性,灵活性等,减少开发成本。设计模式原则其实就是程序员在编程时应当遵守的原则,也是设计模式的基础,即设计模式为什么要这样设计的原因。
设计模式六大原则:
(一)、单一职责原则
(二)、接口隔离原则
(三)、依赖倒置原则
(四)、里氏替换原则
(五)、开闭原则
(六)、迪米特法则
下面我将用说明+代码的方式,尽量地将6大原则说明白。
一、单一职责原则
对于一个类/接口/方式而言之负责一个职责或职能。比如说A类负责两个不同的职责,职责1和职责2,当职责1发生需求变更而修改时,有可能会造成职责2执行错误,这是后需要将A类拆分为A1和A2两个。这样做的有点:1.降低了类的复杂性。2.提高了类的可读性,因为一个类只负责一个职责,看起来比较有目的性。3.提高系统的可维护性,降低了当需求变更时修改程序带来的风险。但是,如果一位的最求单一职责原则,有时候可能会造成类爆炸的问题,所以使用时需要谨慎的看待这一点,不过,接口和方法必须要遵守这一原则。
应用实例
我们已交通工具为例,每种交通工具的出行方式都有可能不一样,比如飞机是在天上飞的,汽车在公路上跑,轮船在水上航行。所以我们可以用以下方式来实现:
方式1:
public class Test { public static void main(String[] args) { Vehicle vehicle = new Vehicle(); vehicle.run("汽车"); vehicle.run("飞机"); vehicle.run("轮船"); } } class Vehicle { public void run(String vehicle) { System.out.println(vehicle + " 在公路上跑"); } }
//运行结果
汽车 在公路上跑
飞机 在公路上跑
轮船 在公路上跑
方式1中run方法既负责在公路上跑,也负责在天上飞,也可以在水上航行,很显然违反了单一职责原则。解决的方法:将每种出行方式单独做成一个类,每个类中都有各自的运行方式,所以我们引出了方式2
方式2:
public class SingleResponsible2 { public static void main(String[] args) { RoadVehicle roadVehicle = new RoadVehicle(); roadVehicle.run("汽车"); AirVehicle airVehicle = new AirVehicle(); airVehicle.run("飞机"); WaterVehicle waterVehicle = new WaterVehicle(); waterVehicle.run("邮轮"); } } 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 + " 在水上航行 "); } }
方式2遵循了单一职责原则,每个类的run方法都只负责各自的类型。但是,我们可以看到这样做需要很大的改动,对于客户端也需要改动很大。因此,我们可以考虑第三种方式,改动Vehicle类型。
方式3:
public class SimgleResponsible3 {
public static void main(String[] args) {
Vehicle vehicle = new Vehicle();
vehicle.airRun("飞机");
vehicle.roadRun("汽车");
vehicle.waterRun("邮轮");
}
}
class Vehicle{
public void roadRun(String vehicle) {
System.out.println(vehicle + "在路上跑");
}
public void airRun(String vehicle) {
System.out.println(vehicle + " 在天空中飞");
}
public void waterRun(String vehicle) {
System.out.println(vehicle + " 水上航行 ");
}
}
这种方式原来的类修改的地方比较少,只是在原来的基础上添加的不同的方法。这种方式虽然在类型不遵循单一职责原则,但是在方式是遵循了这个原则的。
二、接口隔离原则
客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上(Clients should not be forced to depend upon interfaces that they don’t use.)。
类间的依赖关系应该建立在最小的接口上(The dependency of one class to another one should depend on the smallest possible interface)。
问题:类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法。接口I中有op1,op2,op3,op4和op5这5个方法,类B和类D都实现了接口I,但是类A只需要类B的op1,op2和op3这3个方法,类C只需要类D的op1,op4和op5这3个方法。
在没有遵循接口隔离原则,是这样设计的:
public interface Interface1 { void op1(); void op2(); void op3(); void op4(); void op5(); }
public class A { private Interface1 interface1; public A(Interface1 interface1) { this.interface1 = interface1; } public void depend1() { interface1.op1(); } public void depend2() { interface1.op2(); } public void depend3() { interface1.op3(); } } public class B implements Interface1 { @Override public void op1() { System.out.println("op1"); } @Override public void op2() { System.out.println("op2"); } @Override public void op3() { System.out.println("op3"); } @Override public void op4() { } @Override public void op5() { } } public class C { public C(Interface1 interface1) { this.interface1 = interface1; } private Interface1 interface1; public void depend1() { interface1.op1(); } public void depend4() { interface1.op4(); } public void depend5() { interface1.op5(); } } public class D implements Interface1 { @Override public void op1() { System.out.println("op1"); } @Override public void op2() { System.out.println("op2"); } @Override public void op3() { System.out.println("op3"); } @Override public void op4() { System.out.println("op4"); } @Override public void op5() { System.out.println("op5"); } }
public class Test { public static void main(String[] args) { Interface1 b = new B(); Interface1 d = new D(); A a = new A(b); C c = new C(d); a.depend1(); a.depend2(); a.depend3(); c.depend1(); c.depend4(); c.depend5(); } }
从上面的代码中可以看到,A类通过接口1依赖的B类,但是A类只用到了B的1,2和3的方法,4和5是无用的。同样的,C类只用到了D类的1,4和5的方法。这样就违背了接口隔离的原则。那么接下来就使用接口隔离的原则改进上面代码;
分析:A类和C类都使用到了op1,因此我们可以将op1单独抽奖成一个接口1,然后把A类依赖的op2和op3抽象成接口2,C类依赖的op4和op5抽象成接口3,然后B类实现接口1和接口2,D类实现接口1和接口3,A类依赖B类,C类依赖D类,这样就可以将不用的接口隔离开,代码如下:
public interface Interface1 { void op1(); } public interface Interface2 { void op2(); void op3(); } public interface Interface3 { void op4(); void op5(); } public class A { public void depend1(Interface1 interface1) { interface1.op1(); } public void depend2(Interface2 interface2) { interface2.op2(); } public void depend3(Interface2 interface2) { interface2.op3(); } } public class B implements Interface1,Interface2 { @Override public void op1() { System.out.println("B 类实现了 Interface1"); } @Override public void op2() { System.out.println("B 类实现了Interface2"); } @Override public void op3() { System.out.println("B 类实现了Interface2"); } } public class C { public void depend1(Interface1 interface1) { interface1.op1(); } public void depend4(Interface3 interface3) { interface3.op4(); } public void depend5(Interface3 interface3) { interface3.op5(); } } public class D implements Interface1,Interface3 { @Override public void op1() { System.out.println("D 类实现了 Interface1"); } @Override public void op4() { System.out.println("D 类实现了Interface3"); } @Override public void op5() { System.out.println("D 类实现了Interface3"); } } public class Test { 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(); D d = new D(); c.depend1(d); c.depend4(d); c.depend5(d); } }
接口隔离使用注意事项:使用过程我们需要注意把控接口 的细粒度,过度的最求接口隔离,会导致接口数量爆炸,系统中接口泛滥,不利于系统的维护。接口也不能太大,违背了接口隔离的原则,灵活性比较差。适度的控制接口的细粒度,不仅让接口变得更加灵活,而且能够降低系统的耦合度等
三、依赖倒置原则
高层模块不应该依赖底层模块,两者都应该依赖抽象,抽象不应依赖具体,具体应该要依赖于抽象,其核心思想就是面向接口编程。
依赖关系传递的三种方式:
1、接口传递;2、构造方法传递;3、setter方式传递
应用案例:
有一天小王的领导要小王对项目中的商品按照价格从高到低排序,小王接到任务后立即开始写代码了,而且很快就写出来:
public class Sort { public void doSort() { System.out.println("sort by price"); } }
public class Item { public void sort() { new Sort().doSort(); } }
public class Client { public static void main(String[] args) { Item item = new Item(); item.sort(); } }
小王经过测试后,能够运行无误,于是提交了代码。但是,随着需求的变更,领导要求小王添加按照销量排序商品。小王接到任务后又开始敲代码了:
public class SaleSort { public void doSort() { System.out.println("sort by sale amount"); } }
public class Item { public void sort(int type) { if (type == 0) { new Sort().doSort(); }else if (type == 1) { new SaleSort().doSort(); } } }
public class Client { public static void main(String[] args) { Item item = new Item(); item.sort(1); } }
小王通过加班加点后,终于实现了领导要求了,不仅可以按照价格排序,也可以按照销量来排序。突然有一天小王的领导跟他说,现在要添加多一种排序方式,小王听到后内心是崩溃的,心里都有删库跑路的想法了。
问题分析:小王的代码中,每次要添加新的排序方式的时候,都需要修改商品类,并且也需要修改客户端,所以修改量很大,在修改的过程中很容易出现问题,影响其他的功能。这种设计方式就是违反了依赖倒置的原则,高层模块依赖低层模块。
代码改进:
public interface ISort { void doSort(); }
public class PriceSort implements ISort{ @Override public void doSort() { System.out.println("sort by price"); } }
public class SaleSort implements ISort{ @Override public void doSort() { System.out.println("sort by sale amount"); } }
public class Item { private ISort sort; public Item(ISort sort) { this.sort = sort; } public void sort() { sort.doSort(); } public void setSort(ISort sort) { this.sort = sort; } }
public class Client { public static void main(String[] args) { ISort sort = new PriceSort(); Item item = new Item(sort); item.sort(); sort = new SaleSort(); item.setSort(sort); item.sort(); } }
代码分析:重构了代码之后,商品来不在依赖具体的类,而是依赖了ISort接口,现在不管领导需要添加什么样的排序方式,只需要新增一个ISort的实现类就可以了,然后在客户端只需要简单的修改一点可以实现新增排序方式,对原来的代码几乎不用修改。重构后的代码变得更具灵活性,可拓展性。
四、里氏替换原则
定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。
定义2:所有引用基类的地方必须能透明地使用其子类的对象。
继承包含这样一层含义:父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。
继承作为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加了对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能会产生故障。
应用案例:
public class A { public int func1(int a,int b) { return a+b; } }
public class B extends A { //这里无意识的重写了父类的方法 public int func1(int a,int b) { return a-b; } public int func2(int a,int b) { return a+b+10; } }
public class Client { public static void main(String[] args) { A a = new A(); System.out.println("10+20=" + a.func1(10,20)); B b = new B(); System.out.println("10+20=" + b.func1(10,20));//这里本意是想10+20 System.out.println("1 + 2 + 10=" + b.func2(1,2)); } }
我们发现原本运行正常的相减功能发生了错误。原因就是类B在给方法起名时无意中重写了父类的方法,造成所有运行相减功能的代码全部调用了类B重写后的方法,造成原本运行正常的功能出现了错误。在本例中,引用基类A完成的功能,换成子类B之后,发生了异常。在实际编程中,我们常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。如果非要重写父类的方法,比较通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合,组合等关系代替。
五、开闭原则
定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
开闭原则是面向对象设计中最基础的设计原则,它指导我们如何建立稳定灵活的系统。开闭原则可能是设计模式六项原则中定义最模糊的一个了,它只告诉我们对扩展开放,对修改关闭,可是到底如何才能做到对扩展开放,对修改关闭,并没有明确的告诉我们。以前,如果有人告诉我“你进行设计的时候一定要遵守开闭原则”,我会觉的他什么都没说,但貌似又什么都说了。因为开闭原则真的太虚了。
应用实例
public class Shape { int type; } public class Circle extends Shape { Circle() { super.type = 1; } } public class Rectangle extends Shape { Rectangle() { super.type = 2; } } public class Triangle extends Shape { Triangle() { super.type = 3; } } public class GraphicEditor { public void drawShape(Shape shape) { if (shape.type == 1) { drawCircle(); }else if (shape.type == 2) { drawRectangle(); }else { drawTriangle(); } } private void drawRectangle() { System.out.println("画矩形"); } private void drawCircle() { System.out.println("画圆形"); } private void drawTriangle() { System.out.println("画三角形"); } } public class Client { public static void main(String[] args) { GraphicEditor graphicEditor = new GraphicEditor(); graphicEditor.drawShape(new Circle()); graphicEditor.drawShape(new Triangle()); graphicEditor.drawShape(new Rectangle()); } }
上面的示例中优点是比较容易理解,代码简单。但是,违背了开闭原则,即对扩展开发,对修改关闭,也就是当我们在给类添加新的功能的时候,不要改动现有的代码,或者尽量少量改动。上面的代码中,如果我们要添加一个画椭圆形的功能,那么需要修改的地方比较多,在修改的过程中有可能会影响到已有的功能。因此,我们需要对现有的代码进行修改,使之符合ocp原则:
思路:将Shape类做成抽象类,并提供一个抽象的方法draw,让子类去实现就可以了,这样我们在新增新的画图工具是,只需要实现Shape接口,客户端的代码就不需要修改,这样就达到了ocp原则。
public interface Shape { void draw(); } public class GraphicEditor { public void drawShape(Shape shape) { shape.draw(); } } public class Rectangle implements Shape { @Override public void draw() { System.out.println("画矩形"); } } public class Circle implements Shape { @Override public void draw() { System.out.println("画圆形"); } } public class Triangle implements Shape { @Override public void draw() { System.out.println("画三角形"); } } public class Client { public static void main(String[] args) { GraphicEditor graphicEditor = new GraphicEditor(); graphicEditor.drawShape(new Circle()); graphicEditor.drawShape(new Triangle()); graphicEditor.drawShape(new Rectangle()); } }
六、迪米特法则
迪米特法则的定义是:只与你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)。其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。
注意事项:
- 在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标。
- 在类的结构设计上,尽量降低类成员的访问权限。
- 在类的设计上,优先考虑将一个类设置成不变类。
- 在对其他类的引用上,将引用其他对象的次数降到最低。
- 不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。
- 谨慎使用序列化(Serializable)功能。