本文主要参考Robert C. Martin. Design Principles and Design Patterns1和butUncleBob.com2.
僵化(Rigidity)
软件变得难以修改, 每次修改都会造成对应依赖模块的修改. 换句话说, 模块之间耦合性太严重, 因此在中大型项目中多人合作难以协同.
脆弱(Fragility)
每次修改软件之后在许多地方发生崩溃, 而且崩溃发生的地方往往是看起来与修改部分并无直接的关联. 这样的软件不仅难以维护, 而且容易给用户造成不可靠的印象.
低复用(Immobility)
现有的项目或模块很难在其它项目中得到复用. 例如, 一个常发生的场景是项目A中的某个模块与项目B中的某模块类似, 但移植过来需要做较多的修改. 那么项目A的工程师可能更倾向于重写该模块. 这样容易重复造轮子, 影响开发效率.
高粘性(Viscosity)
它包含两个方面: 设计的粘性与开发环境的粘性.
设计的粘性: 修改软件时有多种方式, 其中有些方式能保持最初的设计, 有些则不能, 例如破解(hacks). 如果保持设计的修改方式比破解(hacks)困难, 那么设计的粘性高.
开发环境的粘性: 开发环境低效, 例如如果编译时间太长或IDE的开发效率低下, 工程师会避免做一些需要耗时长的修改.
总结: 上述四个特点是相关的, 但它们体现了软件问题的不同层次. 其中僵化体现了设计的不合理;脆弱体现了软件的质量不可靠; 低复用体现了软件开发的低效率; 高粘性体现了软件开发过程的不友好.
为了避免上述问题, Robert C. Martin12从方法论的角度总结了软件开发的原则, 它们是:
A class should have only one reason to change.
一个类只有一个修改的理由.
一个职责(Responsibility)可以理解为一个修改的理由. 考虑一个Rectangle类, 它包含两个方法:
draw()
在图形界面上画出矩形(由图形程序和操作界面(GUI)调用).area()
计算矩形的面积(由几何计算程序调用).从上图的依赖关系我们发现, 有两个程序调用Rectangle类: 一个是几何计算程序, 它需要计算矩形的面积, 但不会用来画图; 另一个是图形程序, 用来画图(或许也会计算矩形的面积). 因此, Rectangle类实际上负担了两个职责: 计算和画图, 因而不符合单一职责原则. 在这种情况下, 如果图形程序的修改导致Rectangle类需要修改, 那么我们需要重新编译, 测试和部署 几何计算程序.
Software entities (classes, modules, functions etc.) should be open for extension, but closed for modification.
软件实体(类, 模块, 函数等)对扩展是开放的, 对修改是封闭的.
当我们改变软件的功能时, 应该尽量去扩展代码, 而不是去修改原有的代码. 下面我们举个例子来说明. 实现一个画图形的模块, 支持画圆形和矩形. 下面的代码是违反开闭原则的实现: 定义Shape基类, 令Rectangle类和Circle类继承Shape, 并各自实现画图的功能.
// Shape.java
public class Shape {
public String type;
}
// Cirlce.java
public class Circle extends Shape {
Circle() {
type = "circle";
}
void drawCircle() { System.out.println("Circle is drawn."); }
}
// Rectangle.java
public class Rectangle extends Shape {
Rectangle() {
type = "rectangle";
}
void drawRectangle() {System.out.println("Rectangle is drawn."); };
}
// DrawShapes.java
class DrawShapes {
public void draw(Shape shape) {
if (shape.type.equals("circle")) {
Circle circle = (Circle) shape;
circle.drawCircle();
} else if (shape.type.equals("rectangle")) {
Rectangle rectangle = (Rectangle) shape;
rectangle.drawRectangle();
}
}
}
// Test.java
public class Test {
public static void main(String[] args)
{
DrawShapes drawShapes = new DrawShapes();
drawShapes.draw(new Rectangle());
drawShapes.draw(new Circle());
}
}
如果我们要增加画三角形的功能, 首先要增加Triangle的类(继承Shape), 其次要修改DrawShape’s类, 这是违反开闭原则的(如下所示).
// Triangle.java
public class Triangle extends Shape {
public Triangle() {
type = "triangle";
}
public void drawTriangle() { System.out.println("Triangle is drawn."); };
}
// DrawShapes.java
class DrawShapes {
public void draw(Shape shape) {
if (shape.type.equals("circle")) {
Circle circle = (Circle) shape;
circle.drawCircle();
} else if (shape.type.equals("rectangle")) {
Rectangle rectangle = (Rectangle) shape;
rectangle.drawRectangle();
} else if (shape.type.equals("triangle")) {
Triangle triangle = (Triangle) shape;
triangle.drawTriangle();
}
}
}
相比上面的实现相比, 更好的做法是遵循开闭原则.
//ShapeDrawer.java
public interface ShapeDrawer {
public void draw(); // 画几何形状
}
// CircleShapeDrawerImpl.java
public class CircleShapeDrawerImpl implements ShapeDrawer {
@Override
public void draw() {
System.out.println("Circle is drawn.");
}
}
// TriangleShapeDrawerImpl.java
public class TriangleShapeDrawerImpl implements ShapeDrawer {
@Override
public void draw() {
System.out.println("Triangle is drawn.");
}
}
// RectangleShapeDrawerImpl.java
public class RectangleShapeDrawerImpl implements ShapeDrawer {
@Override
public void draw() {
System.out.println("Rectangle is drawn.");
}
}
class DrawShapes {
public void useDrawer(ShapeDrawer shapeDrawer) {
shapeDrawer.draw();
}
}
调用方式如下.
// Test.java
public class Test {
public static void main(String[] args)
{
DrawShapes drawShapes = new DrawShapes();
drawShapes.useDrawer(new CircleShapeDrawerImpl()); // 画圆形
drawShapes.useDrawer(new TriangleShapeDrawerImpl()); // 画三角形
drawShapes.useDrawer(new RectangleShapeDrawerImpl()); // 画矩形
}
}
Functions that use pointers or references to base classes must be able to use objects of derived classes without knownig it.
函数中对基类的引用必须能用其派生类的对象替代(且不会影响程序的运行和功能).
一个经典的例子是: 正方形不是长方形的子类(否则违反里氏替换原则), 因为长方形和正方形有不同的结构, 即: 长方形有长和宽, 而正方形只有边长. 如果在程序中把长方形对象替换成正方形对象则可能产生错误.
定义Rectangle类代表长方形.
public class Rectangle {
private double length;
private double width;
public double getLength() {
return length;
}
public void setLength(double length) {
this.length = length;
}
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
}
令Square继承Rectangle, 并重写所有set和get方法.
public class Square extends Rectangle {
private double side;
@Override
public void setLength(double length) {
side = length;
}
@Override
public void setWidth(double width) {
side = width;
}
@Override
public double getLength() {
return side;
}
@Override
public double getWidth() {
return side;
}
}
上述实现是违反里氏替换原则的(参考下面的代码).
public class Test {
// 设置矩形的长和宽, 并判断设置是否正确
private static void runRectangle() {
Rectangle rectangle = new Rectangle();
rectangle.setLength(10);
rectangle.setWidth(5);
assert(rectangle.getLength() == 10);
assert(rectangle.getWidth() == 5);
}
// 把Rectangle对象替换成Square对象
private static void runSquare() {
Square square = new Square();
square.setLength(10);
square.setWidth(5);
assert square.getLength() == 10;
assert square.getWidth() == 5;
}
public static void main(String[] args)
{
runRectangle(); // OK
runSquare(); // 失败
}
}
遵循里氏替换原则的好处是防止滥用继承, 并保持代码的健壮(派生类替换基类对程序的功能没有影响).
Clients should not be forced to depend upon interfaces that do not use.
客户端不应该被强制依赖它不需要的接口.
客户端(Clients)可以理解为应用程序或模块, 而服务(Service)可以理解为接口功能的实现. 考虑三个客户端(ABC):
下面的设计把所有的方法通过一个服务来实现, 并提供给三个客户端调用. 这样设计的弊端是对接口的一次修改导致三个客户端的修改.
更好的设计是把三个客户端的接口分开, 由三个服务分别实现.
A. High level modules should not depend upon low level modules. Both should depend upon abstractions.
B. Abstractions should not depend upon details. Details should depend upon abstractions.
A. 上层模块不应该依赖下层模块. 两者都应当依赖抽象.
B. 抽象不应该依赖细节. 细节应该依赖抽象.
抽象指的是抽象类(abstract class)或接口(例如Java, Golang中的Interface). 细节指的是抽象类或接口的实现类(Implementation class). 例如我们需要实现画图的功能, 支持画三角形/圆形/矩形. 下面是接口和实现类的示意图:
说明
draw()
是抽象出的画图形的方法.Robert C. Martin. Design Principles and Design Patterns. https://fi.ort.edu.uy/innovaportal/file/2032/1/design_principles.pdf ↩︎ ↩︎
http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod ↩︎ ↩︎