面向对象设计原则SOLID 参考:
https://scotch.io/bar-talk/s-o-l-i-d-the-first-five-principles-of-object-oriented-design
http://www.cnblogs.com/hellojava/archive/2013/03/18/2966684.html
1. S – Single Responsibility Principle 职责单一原则
对象应该仅具有一种单一功能。 概念上和unix的设计原则 “Do one thing and do it well”很相似。听起来很简单但是实践起来很难。
比如,现在有一些图形要计算所有图形的面积。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Circle { private double radius; public Circle Circle (double radius) { this .radius = radius; } private double getRadius () { return this .radius; } } class Square { private double length; public Square (double length) { this .length = length; } public double getLength () { return this .length; } }
首先,创建图形类,包含构造函数和需要的属性参数。 接下来创建面积计算类AreaCalculator,然后编写方法计算提供的图形参数的面积总和。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class AreaCalculator { private List shapes; public AreaCalculator (List shapes) { this .shapes = shapes; } public double sum () { } public void output () { return System.out.println(String.format("Sum of the areas of provided shapes: %s" , this .sum()); } }
简单实例化AreaCalculator对象,然后传入一个图形集合,然后调用output方法显示面积总和。
1 2 AreaCalculator areaCalculator = new AreaCalculator(Arrays.asList(new Circle(10.0f ), new Square(12.0f ))); areaCalculator.output()
这里output方法的问题是AreaCalculator处理计算逻辑然后输出字符串结果。那么,如果用户想用json或者其他格式输出计算结果呢? 所有的操作都放到AreaCalculator中做违背了职责单一原则。AreaCalculator应该只计算提供的图形的面积总和,而不用关心如何输入结果。
因此,需要新建一个SumCalculatorOutputter类用来处理统计结果输出操作。
SumCalculatorOutputter类如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class SumCalculatorOutputter { private AreaCalculator areaCalculator; public SumCalculatorOutputter (AreaCalculator areaCalculator) { this .areaCalculator = areaCalculator; } public void outputJson () { double sum = this .areaCalculator.sum(); } public void outputHtml () { } public void outputString () { } } AreaCalculator areaCalculator = new AreaCalculator(Arrays.asList(new Circle(10.0f ), new Square(12.0f ))); SumCalculatorOutputter output = new SumCalculatorOutputter(areaCalculator); output.outputJson(); output.outputHtml(); output.outputString();
现在不管你要把结果按照何种方式输出都放在SumCalculatorOutputter中处理。
2. O – Open-Closed Principle 开放封闭原则
对象应该是对于扩展开放的,但是对于修改封闭的。 这意味着一个类应该可以容易地扩展,而不需要修改类本身。
我们来看下AreaCalculator类的sum方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 public double sum () { double sum = 0.0f ; for (Object shape : this .shapes) { if (shape instanceof Circle) { Circle shape = (Circle)shape; sum += Math.pi * Math.sqrt(shape.getRadius()); } else if (shape instanceof Square) { Square shape = (Square)shape; sum += Math.sqrt(shape.getLength()); } } return sum; }
如果我们要支持更多的图形就需要添加更多的“else if”代码块儿,这就违背了开闭原则。 一种解决方式是把面积计算的逻辑迁移到图形类中,AreaCalculator类的sum方法调用图形类的面积计算方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class Circle { private double radius; public Circle Circle (double radius) { this .radius = radius; } public double area () { return Math.PI * Math.sqrt(this .radius); } private double getRadius () { return this .radius; } } class Square { private double length; public Square (double length) { this .length = length; } public double area () { return Math.sqrt(this .length); } public double getLength () { return this .length; } }
然后我们抽取公共方法area作为接口ShapeInterface的方法,这样新增图形的时候只要实现ShapeInterface接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 interface ShapeInterface { double area () ; } class Circle implements ShapeInterface { private double radius; public Circle Circle (double radius) { this .radius = radius; } @override public double area () { return Math.PI * Math.sqrt(this .radius); } private double getRadius () { return this .radius; } } class Square implements ShapeInterface { private double length; public Square (double length) { this .length = length; } @override public double area () { return Math.sqrt(this .length); } public double getLength () { return this .length; } }
现在修改AreaCalculator类的sum方法:
1 2 3 4 5 6 7 8 9 10 11 12 class AreaCalculator { private List<ShapeInterface> shapes; public AreaCalculator (List<ShapeInterface> shapes) { this .shapes = shapes; } public double sum () { return this .shapes.stream.map(ShapeInterface::sum).mapToDouble(Double::doubleValue).sum(); } }
3. L – Liskov Substitution Principle 里氏替换原则
对象应该是可以在不改变程序正确性的前提下被它的子类所替换的。
子类可以扩展父类的功能,但不能改变父类原有的功能。
子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
子类中可以增加自己特有的方法。
当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
违背里氏替换原则的例子,我们新增一个体积计算器类VolumeCalculator继承面积计算器类,覆盖sum方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 class VolumeCalculator extends AreaCalulator { public VolumeCalculator (List<ShapeInterface> shapes) { super (shapes); } @override public double sum () { return this .shapes.stream.map(ShapeInterface::sum).mapToDouble(Double::doubleValue).sum(); } }
遵循里氏替换原则的例子如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class VolumeCalculator extends AreaCalulator { public VolumeCalculator (List<ShapeInterface> shapes) { super (shapes); } public double volume () { return 0.0d ; } }
4. I – Interface Segregation Principle 接口隔离原则
多个特定客户端接口要好于一个宽泛用途的接口。
客户端不应该强行依赖它不需要的接口。
类间的依赖关系应该建立在最小的接口上。
客户端不应该依赖它不需要的接口,意思就是说客户端只要依赖它需要的接口,它需要什么接口,就提供什么接口,不提供多余的接口。
“类间的依赖关系应该建立在最小的接口上”也表达这一层意思。
通俗的讲就是:接口中的方法应该尽量少,不要使接口过于臃肿,不要有很多不相关的逻辑方法。
我们继续以形状举例,形状有立体形状所以给形状接口ShapeInterface提供体积计算方法volume:
1 2 3 4 interface ShapeInterface { double area () ; double volume () ; }
所有实现ShapeInterface接口的形状类都要实现volume方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 class Circle implements ShapeInterface { private double radius; public Circle Circle (double radius) { this .radius = radius; } @override public double area () { return Math.PI * Math.sqrt(this .radius); } @override public double volume () { return 0.0d ; } private double getRadius () { return this .radius; } } class Square implements ShapeInterface { private double length; public Square (double length) { this .length = length; } @override public double area () { return Math.sqrt(this .length); } @override public double volume () { return 0.0d ; } public double getLength () { return this .length; } } class Cube implements ShapeInterface { private double length; public Cube (double length) { this .length = length; } @override public double area () { return 6 * Math.sqrt(this .length); } @override public double volume () { return Math.pow(this .length, 3 ); } public double getLength () { return this .length; } }
这么做会导致Circle和Square这样的平面图形必须要实现一个不必要的volume方法。 可以提供一个立体形状接口SolidShapeInterface,提供volume供立体形状来实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 interface ShapeInterface { double area () ; } interface SolidShapeInterface { double volume () ; } class Circle implements ShapeInterface { private double radius; public Circle Circle (double radius) { this .radius = radius; } @override public double area () { return Math.PI * Math.sqrt(this .radius); } private double getRadius () { return this .radius; } } class Square implements ShapeInterface { private double length; public Square (double length) { this .length = length; } @override public double area () { return Math.sqrt(this .length); } public double getLength () { return this .length; } } class Cube implements ShapeInterface , SolidShapeInterface { private double length; public Cube (double length) { this .length = length; } @override public double area () { return 6 * Math.sqrt(this .length); } @override public double volume () { return Math.pow(this .length, 3 ); } public double getLength () { return this .length; } }
5. D – Dependency Inversion Principle 依赖倒置原则
一个方法应该遵从“依赖于抽象而不是一个实例”
高层模块不应该依赖低层模块,两者都应该依赖其抽象;
抽象不应该依赖细节;
细节应该依赖抽象。
抽象:即抽象类或接口,两者是不能够实例化的。
细节:即具体的实现类,实现接口或者继承抽象类所产生的类,两者可以通过关键字new直接被实例化。
类A直接依赖于类B,假如要将类A修改为依赖类C,则必须通过修改类A的代码来达成。
这种场景下,类A一般是高层模块,负责复杂的业务逻辑。
类B和C是底层模块,负责基本的原子操作。
假如修改类A,将会给程序带来不必要的风险。
而遵循依赖倒置原则的程序设计可以解决这一问题。
下面以代码示例说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Jim { public void eat (Apple apple) { System.out.println("Jim eat " + apple.getName()); } } public class Apple { public String getName () { return "apple" ; } } public class Client { public static void main (String[] args) { Jim jim = new Jim(); Apple apple = new Apple(); jim.eat(apple); } }
上面代码看起来比较简单,但其实是一个非常脆弱的设计。现在Jim可以吃苹果了,但是不能只吃苹果而不吃别的水果啊,这样下去肯定会造成营养失衡。现在想让Jim吃香蕉了(好像香蕉里含钾元素比较多,吃点比较有益),突然发现Jim是吃不了香蕉的,那怎么办呢?看来只有修改代码了啊,由于上面代码中Jim类依赖于Apple类,所以导致不得不去改动Jim类里面的代码。那如果下次Jim又要吃别的水果了呢?继续修改代码?这种处理方式显然是不可取的,频繁修改会带来很大的系统风险,改着改着可能就发现Jim不会吃水果了。
上面的代码之所以会出现上述难堪的问题,就是因为Jim类依赖于Apple类,两者是紧耦合的关系,其导致的结果就是系统的可维护性大大降低。要增加香蕉类却要去修改Jim类代码,这是不可忍受的,你改你的代码为什么要动我的啊,显然Jim不乐意了。我们常说要设计一个健壮稳定的系统,而这里只是增加了一个香蕉类,就要去修改Jim类,健壮和稳定还从何谈起。
而根据依赖倒置原则,我们可以对上述代码做些修改,提取抽象的部分。首先我们提取出两个接口:People和Fruit,都提供各自必需的抽象方法,这样以后无论是增加Jim人类,还是增加Apple、Banana等各种水果,都只需要增加自己的实现类就可以了。由于遵循依赖倒置原则,只依赖于抽象,而不依赖于细节,所以增加类无需修改其他类。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public interface People { public void eat (Fruit fruit) ; } public interface Fruit { public String getName () ; } public class Jim implements People { public void eat (Fruit fruit) { System.out.println("Jim eat " + fruit.getName()); } } public class Apple implements Fruit { public String getName () { return "apple" ; } } public class Banana implements Fruit { public String getName () { return "banana" ; } } public class Client { public static void main (String[] args) { People jim = new Jim(); Fruit apple = new Apple(); Fruit Banana = new Banana(); jim.eat(apple); jim.eat(Banana); } }
People类是复杂的业务逻辑,属于高层模块,而Fruit是原子模块,属于低层模块。People依赖于抽象的Fruit接口,这就做到了:高层模块不应该依赖低层模块,两者都应该依赖于抽象(抽象类或接口)。
Pople和Fruit接口与各自的实现类没有关系,增加实现类不会影响接口,这就做到了:抽象(抽象类或接口)不应该依赖于细节(具体实现类)。
Jim、Apple、Banana实现类都要去实现各自的接口所定义的抽象方法,所以是依赖于接口的。这就做到了:细节(具体实现类)应该依赖抽象。 通过上面的代码段我们可以看到,高级和低级模块都取决于抽象。
到了这里,我们对依赖倒置原则的“依赖”就很好理解了,但是什么是“倒置”呢。是这样子的,刚开始按照正常人的一般思维方式,我想吃香蕉就是吃香蕉,想吃苹果就吃苹果,编程也是这样,都是按照面向实现的思维方式来设计。而现在要倒置思维,提取公共的抽象,面向接口(抽象类)编程。不再依赖于具体实现了,而是依赖于接口或抽象类,这就是依赖的思维方式“倒置”了。
依赖倒置常用的三种实现方式:
依赖接口作为参数使用
依赖接口通过构造函数传递
依赖接口通过setter方法传递
总结 面向对象设计的这五条基本原则是设计模式和重构的基础。 只有充分理解这五条原则才能更好的理解设计模式的思想,更好的进行代码的重构。