Fork me on GitHub

S.O.L.I.D面向对象设计的5大原则


专注、极致、大道至简。

S.O.L.I.D 代表什么

  • S – 单一职责原则
  • O – 开放封闭原则
  • L – 里氏替换原则
  • I – 接口隔离原则
  • D – 依赖倒置原则

单一职责原则

一个类应该有且只有一个去改变它的理由,这意味着一个类应该只有一项工作。
例如,假设我们有一些shape(形状),并且我们想求所有shape的面积的和。这很简单对吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Circle {
public $radius;
public function __construct($radius) {
$this->radius = $radius;
}
}
class Square {
public $length;
public function __construct($length) {
$this->length = $length;
}
}

首先,我们创建shape类,让构造函数设置需要的参数。接下来,我们继续通过创建AreaCalculator类,然后编写求取所提供的shape面积之和的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class AreaCalculator {
protected $shapes;
public function __construct($shapes = array()) {
$this->shapes = $shapes;
}
public function sum() {
// logic to sum the areas
}
public function output() {
return implode('', array(
"<h1>",
"Sum of the areas of provided shapes: ",
$this->sum(),
"</h1>"
));
}
}

使用AreaCalculator类,我们简单地实例化类,同时传入一个shape数组,并在页面的底部显示输出。

1
2
3
4
5
6
7
8
9
$shapes = array(
new Circle(2),
new Square(5),
new Square(6)
);
$areas = new AreaCalculator($shapes);
echo $areas->output();

输出方法的问题在于,AreaCalculator处理了输出数据的逻辑。因此,如果用户想要以json或其他方式输出数据该怎么办?
所有的逻辑将由AreaCalculator类处理,这是违反单一职责原则(SRP)的;AreaCalculator类应该只对提供的shape进行面积求和,它不应该关心用户是需要json还是HTML。
因此,为了解决这个问题,你可以创建一个SumCalculatorOutputter类,使用这个来处理你所需要的逻辑,即对所提供的shape进行面积求和后如何显示。
SumCalculatorOutputter类按如下方式工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
$shapes = array(
new Circle(2),
new Square(5),
new Square(6)
);
$areas = new AreaCalculator($shapes);
$output = new SumCalculatorOutputter($areas);
echo $output->JSON();
echo $output->HAML();
echo $output->HTML();
echo $output->JADE();

开放封闭原则

对象或实体应该对扩展开放,对修改封闭。
这就意味着一个类应该无需修改类本身但却容易扩展。让我们看看AreaCalculator类,尤其是它的sum方法。

1
2
3
4
5
6
7
8
9
10
11
public function sum() {
foreach($this->shapes as $shape) {
if(is_a($shape, 'Square')) {
$area[] = pow($shape->length, 2);
} else if(is_a($shape, 'Circle')) {
$area[] = pi() * pow($shape->radius, 2);
}
}
return array_sum($area);
}

如果我们希望sum方法能够对更多的shape进行面积求和,我们会添加更多的If / else块,这违背了开放封闭原则。
能让这个sum方法做的更好的一种方式是,将计算每个shape面积的逻辑从sum方法中移出,将它附加到shape类上。

1
2
3
4
5
6
7
8
9
10
11
class Square {
public $length;
public function __construct($length) {
$this->length = $length;
}
public function area() {
return pow($this->length, 2);
}
}

对Circle类应该做同样的事情,area方法应该添加。现在,计算任何所提的shape的面积的和的方法应该和如下简单:

1
2
3
4
5
6
7
public function sum() {
foreach($this->shapes as $shape) {
$area[] = $shape->area;
}
return array_sum($area);
}

现在我们可以创建另一个shape类,并在计算和时将其传递进来,这不会破坏我们的代码。然而,现在另一个问题出现了,我们怎么知道传递到AreaCalculator上的对象确实是一个shape,或者这个shape具有一个叫做area的方法?

对接口编程是S.O.L.I.D不可或缺的一部分,一个快速的例子是我们创建一个接口,让每个shape实现它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface ShapeInterface {
public function area();
}
class Circle implements ShapeInterface {
public $radius;
public function __construct($radius) {
$this->radius = $radius;
}
public function area() {
return pi() * pow($this->radius, 2);
}
}

在我们AreaCalculator的求和中,我们可以检查所提供的shape确实是ShapeInterface的实例,否则我们抛出一个异常:

1
2
3
4
5
6
7
8
9
10
11
12
public function sum() {
foreach($this->shapes as $shape) {
if(is_a($shape, 'ShapeInterface')) {
$area[] = $shape->area();
continue;
}
throw new AreaCalculatorInvalidShapeException;
}
return array_sum($area);
}

里氏替换原则

每一个子类或派生类应该可以替换它们基类或父类。

接口隔离原则

不应强迫客户端实现一个它用不上的接口,或是说客户端不应该被迫依赖它们不使用的方法。
我们知道也有立体shape,如果我们也想计算shape的体积,我们可以添加另一个合约到ShapeInterface:

1
2
3
4
interface ShapeInterface {
public function area();
public function volume();
}

任何我们创建的shape必须实现volume的方法,但是我们知道正方形是平面形状没有体积,所以这个接口将迫使正方形类实现一个它没有使用的方法。

接口隔离原则(ISP)不允许这样,你可以创建另一个名为SolidShapeInterface的接口,它有一个volume合约,对于立体形状比如立方体等等,可以实现这个接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface ShapeInterface {
public function area();
}
interface SolidShapeInterface {
public function volume();
}
class Cuboid implements ShapeInterface, SolidShapeInterface {
public function area() {
// calculate the surface area of the cuboid
}
public function volume() {
// calculate the volume of the cuboid
}
}

这是一个更好的方法,但小心一个陷阱,当这些接口做类型提示时,不要使用ShapeInterface或SolidShapeInterface。

你可以创建另一个接口,可以是ManageShapeInterface,平面和立体shape都可用,这样你可以很容易地看到它有一个管理shape的单一API。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface ManageShapeInterface {
public function calculate();
}
class Square implements ShapeInterface, ManageShapeInterface {
public function area() { /*Do stuff here*/ }
public function calculate() {
return $this->area();
}
}
class Cuboid implements ShapeInterface, SolidShapeInterface, ManageShapeInterface {
public function area() { /*Do stuff here*/ }
public function volume() { /*Do stuff here*/ }
public function calculate() {
return $this->area() + $this->volume();
}
}

现在AreaCalculator类中,我们可以轻易用calculate替代area调用,同时可以检查一个对象是ManageShapeInterface而不是ShapeInterface的实例。

依赖反转原则

实体必须依靠抽象而不是具体实现。它表示高层次的模块不应该依赖于低层次的模块,它们都应该依赖于抽象(接口)。

1
2
3
4
5
6
7
class PasswordReminder {
private $dbConnection;
public function __construct(MySQLConnection $dbConnection) {
$this->dbConnection = $dbConnection;
}
}

首先MySQLConnection是低层次模块,而PasswordReminder处于高层次,但根据S.O.L.I.D.中D的定义,即依赖抽象而不是具体实现,上面这段代码违反这一原则,PasswordReminder类被迫依赖于MySQLConnection类。

以后如果你改变数据库引擎,你还必须编辑PasswordReminder类,因此违反了开闭原则。

PasswordReminder类不应该关心你的应用程序使用什么数据库,为了解决这个问题我们又一次“对接口编程”,因为高层次和低层次模块应该依赖于抽象,我们可以创建一个接口:

1
2
3
interface DBConnectionInterface {
public function connect();
}

接口有一个connect方法,MySQLConnection类实现该接口,在PasswordReminder类的构造函数不使用MySQLConnection类,而是使用接口替换,不用管你的应用程序使用的是什么类型的数据库,PasswordReminder类可以很容易地连接到数据库,没有任何问题,且不违反OCP。

1
2
3
4
5
6
7
8
9
10
11
12
13
class MySQLConnection implements DBConnectionInterface {
public function connect() {
return "Database connection";
}
}
class PasswordReminder {
private $dbConnection;
public function __construct(DBConnectionInterface $dbConnection) {
$this->dbConnection = $dbConnection;
}
}

根据上面的代码片段,你现在可以看到,高层次和低层次模块依赖于抽象。

http://blog.jobbole.com/86267/
https://juejin.im/entry/587f1c331b69e6005853ecfa