转载

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

S.O.L.I.D 是面向对象设计(OOD)的头五大基本原则的首字母缩写,由俗称「 鲍勃大叔 」的 Robert C. Martin 提出。

这些原则,结合在一起能够方便程序员开发易于维护和扩展的软件,也让开发人员轻松避免代码异味,易于重构代码,也是敏捷或自适应软件开发的一部分。

注意:这只是一篇“欢迎来到S.O.L.I.D”的简单介绍文章,它只是揭示了S.O.L.I.D是什么。

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

虽然缩略词展开后看似复杂,但其实非常容易掌握。

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

让我们来单独看看每个原则,来理解为什么 S.O.L.I.D 能帮助我们成为更优秀的开发人员。

单一职责原则

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

(伯乐在线配图)

S.R.P(简称)原则指出:

一个类应该有且只有一个去改变它的理由,这意味着一个类应该只有一项工作。

例如,假设我们有一些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();

现在,不管你需要何种逻辑来输出数据给用户,皆由SumCalculatorOutputter类处理。

开放封闭原则

对象或实体应该对扩展开放,对修改封闭。

这就意味着一个类应该无需修改类本身但却容易扩展。让我们看看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 );

}

里氏替换原则

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

(伯乐在线配图)

在对象 x 为类型 T 时 q(x) 成立,那么当 S 是 T 的子类时,对象 y 为类型 S 时 q(y) 也应成立。(即对父类的调用同样适用于子类)

这一切说明的是,每一个子类或派生类应该可以替换它们基类或父类。

还利用AreaCalculator类,我们有一个VolumeCalculator类,它扩展了AreaCalculator类:

1

2

3

4

5

6

7

8

9

10

class VolumeCalculator extends AreaCalulator {

   public function __construct( $shapes = array ()) {

     parent::__construct( $shapes );

   }

   public function sum() {

     // logic to calculate the volumes and then return and array of output

     return array ( $summedData );

   }

}

In the SumCalculatorOutputter class:

在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

class SumCalculatorOutputter {

   protected $calculator ;

   public function __constructor(AreaCalculator $calculator ) {

     $this ->calculator = $calculator ;

   }

   public function JSON() {

     $data = array (

       'sum' => $this ->calculator->sum();

     );

     return json_encode( $data );

   }

   public function HTML() {

     return implode( '' , array (

       '<h1>' ,

         'Sum of the areas of provided shapes: ' ,

         $this ->calculator->sum(),

       '</h1>'

     ));

   }

}

如果我们试图这样来运行一个例子:

1

2

3

4

5

$areas = new AreaCalculator( $shapes );

$volumes = new AreaCalculator( $solidShapes );

$output = new SumCalculatorOutputter( $areas );

$output2 = new SumCalculatorOutputter( $volumes );

程序可以运行,但是当我们在$output2对象调用HTML方法,我们得到一个E_NOTICE错误,提示数组到字符串的转换。

为了解决这个问题,不要从VolumeCalculator类的sum方法返回一个数组,你应该:

1

2

3

4

public function sum() {

   // logic to calculate the volumes and then return and array of output

   return $summedData ;

}

求和的结果作为一个浮点数,双精度或整数。

接口隔离原则

不应强迫客户端实现一个它用不上的接口,或是说客户端不应该被迫依赖它们不使用的方法。

仍然以shape为例,我们知道也有立体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 ;

   }

}

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

结论

老实说,S.O.L.I.D初看起来可能棘手,但只要通过连续使用并遵守其指导方针,它就会变成你和你的代码的一部分,可以让你的代码很容易地扩展、修改、测试和重构,不出任何问题。

正文到此结束
Loading...