介绍
_SOLID 是罗伯特·C.马丁(Robert C. Martin)的第一个五个面向对象设计原则(OOD)的缩写(也称为 [Bob Uncle])。
<$>[注] **注:**虽然这些原则可以应用于各种编程语言,但本文中的示例代码将使用PHP。
这些原则建立了实践,借助于软件开发,并考虑在项目增长时进行维护和扩展,采用这些实践还可以有助于避免代码臭味,重构代码,以及敏捷或适应性软件开发。
坚实代表:
- S - Single-responsiblity Principle
- O - Open-closed Principle
- L - Liskov Substitution Principle
- I - Interface Segregation Principle
- D - Dependency Inversion Principle
在本文中,您将单独介绍每个原则,以了解 SOLID 如何帮助您成为一个更好的开发者。
单一责任原则
单一责任原则(SRP)规定:
一个班级应该有一个改变的理由,也就是说,一个班级应该只有一份工作。
例如,考虑一个应用程序,该应用程序收集了形状(圆形和方块)的集合,并计算了集合中的所有形状的面积总和。
首先,创建形状类,并让构建者设置所需的参数。
对于方块,你需要知道一侧的长度
:
1class Square
2{
3 public $length;
4
5 public function construct($length)
6 {
7 $this->length = $length;
8 }
9}
对于圆圈,你需要知道半径
:
1class Circle
2{
3 public $radius;
4
5 public function construct($radius)
6 {
7 $this->radius = $radius;
8 }
9}
接下来,创建AreaCalculator
类,然后写下逻辑来总结所有提供的形状的区域. 一个方块的面积由长度方块计算。
1class AreaCalculator
2{
3 protected $shapes;
4
5 public function __construct($shapes = [])
6 {
7 $this->shapes = $shapes;
8 }
9
10 public function sum()
11 {
12 foreach ($this->shapes as $shape) {
13 if (is_a($shape, 'Square')) {
14 $area[] = pow($shape->length, 2);
15 } elseif (is_a($shape, 'Circle')) {
16 $area[] = pi() * pow($shape->radius, 2);
17 }
18 }
19
20 return array_sum($area);
21 }
22
23 public function output()
24 {
25 return implode('', [
26 '',
27 'Sum of the areas of provided shapes: ',
28 $this->sum(),
29 '',
30 ]);
31 }
32}
要使用AreaCalculator
类,您需要实例化类,并通过一系列形状,并在页面底部显示输出。
以下是具有三种形状的集合的例子:
- 半径为 2 的圆圈* 长度为 5 的方块* 长度为 6 的第二个方块
1$shapes = [
2 new Circle(2),
3 new Square(5),
4 new Square(6),
5];
6
7$areas = new AreaCalculator($shapes);
8
9echo $areas->output();
输出方法的问题是AreaCalculator
处理输出数据的逻辑。
考虑一个场景,输出应该转换为其他格式,如JSON。
所有逻辑都将由AreaCalculator
类处理,这会违反单一责任原则,而AreaCalculator
类只应关注提供的形状区域的总和。
要解决此问题,您可以创建一个单独的SumCalculatorOutputter
类,并使用该新类来处理输出数据给用户所需的逻辑:
1class SumCalculatorOutputter
2{
3 protected $calculator;
4
5 public function __constructor(AreaCalculator $calculator)
6 {
7 $this->calculator = $calculator;
8 }
9
10 public function JSON()
11 {
12 $data = [
13 'sum' => $this->calculator->sum(),
14 ];
15
16 return json_encode($data);
17 }
18
19 public function HTML()
20 {
21 return implode('', [
22 '',
23 'Sum of the areas of provided shapes: ',
24 $this->calculator->sum(),
25 '',
26 ]);
27 }
28}
SumCalculatorOutputter
类会像这样工作:
1$shapes = [
2 new Circle(2),
3 new Square(5),
4 new Square(6),
5];
6
7$areas = new AreaCalculator($shapes);
8$output = new SumCalculatorOutputter($areas);
9
10echo $output->JSON();
11echo $output->HTML();
现在,您需要输出数据给用户的逻辑由SumCalculatorOutputter
类处理。
这符合单一责任原则。
开放封闭原则
开放封闭原则(OCP)规定:
对象或实体应该是开放的扩展,但关闭的修改。
这意味着一个类应该可以扩展,而不会改变类本身。
让我们重新审视AreaCalculator
类,并专注于sum
方法:
1class AreaCalculator
2{
3 protected $shapes;
4
5 public function __construct($shapes = [])
6 {
7 $this->shapes = $shapes;
8 }
9
10 public function sum()
11 {
12 foreach ($this->shapes as $shape) {
13 if (is_a($shape, 'Square')) {
14 $area[] = pow($shape->length, 2);
15 } elseif (is_a($shape, 'Circle')) {
16 $area[] = pi() * pow($shape->radius, 2);
17 }
18 }
19
20 return array_sum($area);
21 }
22}
考虑一个场景,用户希望总
的额外的形状,如三角形,五角形,六角形等你将不得不不断编辑这个文件,并添加更多的如果
/ else
块。
您可以改进此总
方法的一种方法是从AreaCalculator
类方法中删除计算每个形状面积的逻辑,并将其附加到每个形状的类中。
以下是广场
中定义的区域
方法:
1class Square
2{
3 public $length;
4
5 public function __construct($length)
6 {
7 $this->length = $length;
8 }
9
10 public function area()
11 {
12 return pow($this->length, 2);
13 }
14}
以下是圈
中定义的区域
方法:
1class Circle
2{
3 public $radius;
4
5 public function construct($radius)
6 {
7 $this->radius = $radius;
8 }
9
10 public function area()
11 {
12 return pi() * pow($shape->radius, 2);
13 }
14}
AreaCalculator
的总
方法可以重写为:
1class AreaCalculator
2{
3 // ...
4
5 public function sum()
6 {
7 foreach ($this->shapes as $shape) {
8 $area[] = $shape->area();
9 }
10
11 return array_sum($area);
12 }
13}
现在,您可以创建另一个形状类,并在计算总数时传输它,而不打破代码。
然而,还会出现另一个问题:如何知道被传入AreaCalculator
的对象实际上是一个形状,或者形状有一个名为area
的方法?
编码到一个 接口是固定的组成部分。
创建一个支持区域
的ShapeInterface
:
1interface ShapeInterface
2{
3 public function area();
4}
更改您的形状类以实施``ShapeInterface
。
以下是广场
的更新:
1class Square implements ShapeInterface
2{
3 // ...
4}
以下是圆圈
的更新:
1class Circle implements ShapeInterface
2{
3 // ...
4}
在AreaCalculator
的总
方法中,您可以检查所提供的形状是否实际上是ShapeInterface
的实例;否则,扔出一个例外:
1class AreaCalculator
2{
3 // ...
4
5 public function sum()
6 {
7 foreach ($this->shapes as $shape) {
8 if (is_a($shape, 'ShapeInterface')) {
9 $area[] = $shape->area();
10 continue;
11 }
12
13 throw new AreaCalculatorInvalidShapeException();
14 }
15
16 return array_sum($area);
17 }
18}
这符合开放的原则。
利斯科夫替代原理
利斯科夫替代原理说:
让 q(x) 是对 T 类型 x 的对象可验证的属性,然后 q(y) 应该是对 S 类型 y 的对象可验证的属性,其中 S 是 T 的子类型。
这意味着每个子类或衍生类应该可以替代其基础类或母类。
根据示例AreaCalculator
类,考虑一个新的VolumeCalculator
类,延伸了AreaCalculator
类:
1class VolumeCalculator extends AreaCalculator
2{
3 public function construct($shapes = [])
4 {
5 parent::construct($shapes);
6 }
7
8 public function sum()
9 {
10 // logic to calculate the volumes and then return an array of output
11 return [$summedData];
12 }
13}
请记住,‘SumCalculatorOutputter’类似于此:
1class SumCalculatorOutputter {
2 protected $calculator;
3
4 public function __constructor(AreaCalculator $calculator) {
5 $this->calculator = $calculator;
6 }
7
8 public function JSON() {
9 $data = array(
10 'sum' => $this->calculator->sum();
11 );
12
13 return json_encode($data);
14 }
15
16 public function HTML() {
17 return implode('', array(
18 '',
19 'Sum of the areas of provided shapes: ',
20 $this->calculator->sum(),
21 ''
22 ));
23 }
24}
如果你试图运行这样的例子:
1$areas = new AreaCalculator($shapes);
2$volumes = new VolumeCalculator($solidShapes);
3
4$output = new SumCalculatorOutputter($areas);
5$output2 = new SumCalculatorOutputter($volumes);
当您在$output2
对象上调用HTML
方法时,您将收到E_NOTICE
错误,通知您将数组转换为字符串。
若要修复此问题,而不是从VolumeCalculator
类合计方法返回一个数组,则返回$summedData
:
1class VolumeCalculator extends AreaCalculator
2{
3 public function construct($shapes = [])
4 {
5 parent::construct($shapes);
6 }
7
8 public function sum()
9 {
10 // logic to calculate the volumes and then return a value of output
11 return $summedData;
12 }
13}
$summedData
可以是浮点、双数或整数。
这符合利斯科夫替代原理。
接口分离原则
界面隔离原则规定:
客户端不应该被迫实施它不使用的界面,或者客户端不应该被迫依赖他们不使用的方法。
仍然从以前的ShapeInterface
示例中建立起,您需要支持Cuboid
和Spheroid
的新三维形状,这些形状还需要计算体积
。
让我们来考虑如果您修改ShapeInterface
以添加另一个合同会发生什么:
1interface ShapeInterface
2{
3 public function area();
4
5 public function volume();
6}
现在,你创建的任何形状都必须执行体积
方法,但你知道平方是平的形状,并且它们没有体积,所以这个界面会迫使平方
类实现它没有用处的方法。
相反,您可以创建另一个名为ThreeDimensionalShapeInterface
的界面,该界面具有卷
合同,并且三维形状可以实现此界面:
1interface ShapeInterface
2{
3 public function area();
4}
5
6interface ThreeDimensionalShapeInterface
7{
8 public function volume();
9}
10
11class Cuboid implements ShapeInterface, ThreeDimensionalShapeInterface
12{
13 public function area()
14 {
15 // calculate the surface area of the cuboid
16 }
17
18 public function volume()
19 {
20 // calculate the volume of the cuboid
21 }
22}
这是一个更好的方法,但在键入这些接口时要注意的一个陷阱,而不是使用ShapeInterface
或ThreeDimensionalShapeInterface
,您可以创建另一个接口,也许是ManageShapeInterface
,并在平面和三维形状上实现它。
这样,你可以有一个单一的 API 来管理形状:
1interface ManageShapeInterface
2{
3 public function calculate();
4}
5
6class Square implements ShapeInterface, ManageShapeInterface
7{
8 public function area()
9 {
10 // calculate the area of the square
11 }
12
13 public function calculate()
14 {
15 return $this->area();
16 }
17}
18
19class Cuboid implements ShapeInterface, ThreeDimensionalShapeInterface, ManageShapeInterface
20{
21 public function area()
22 {
23 // calculate the surface area of the cuboid
24 }
25
26 public function volume()
27 {
28 // calculate the volume of the cuboid
29 }
30
31 public function calculate()
32 {
33 return $this->area();
34 }
35}
现在在AreaCalculator
类中,您可以用计算
代替区域
方法的调用,并检查对象是否是ManageShapeInterface
的实例,而不是ShapeInterface
。
这符合界面分离原则。
依赖性逆转原则
依赖性逆转原则是:
实体必须依赖抽象,而不是具体,它指出高级模块不应该依赖低级模块,而应该依赖抽象。
这个原则允许分离。
以下是连接到MySQL数据库的PasswordReminder
的例子:
1class MySQLConnection
2{
3 public function connect()
4 {
5 // handle the database connection
6 return 'Database connection';
7 }
8}
9
10class PasswordReminder
11{
12 private $dbConnection;
13
14 public function __construct(MySQLConnection $dbConnection)
15 {
16 $this->dbConnection = $dbConnection;
17 }
18}
首先,MySQLConnection
是低级别模块,而PasswordReminder
是高级别,但根据SOLID中的 D的定义,该定义是依赖抽象,而不是具体性
。
后来,如果您要更改数据库引擎,您还需要编辑PasswordReminder
类,这会违反开放关闭
原则。
PasswordReminder
类别不应该在乎您的应用程序使用哪个数据库. 为了解决这些问题,您可以编码到一个界面,因为高级和低级模块应该取决于抽象性:
1interface DBConnectionInterface
2{
3 public function connect();
4}
接口有一个连接方法,而MySQLConnection
类实现了这个接口. 此外,而不是直接在PasswordReminder
的构建器中键入MySQLConnection
类,您代替键入DBConnectionInterface
,并且无论您的应用程序使用的数据库类型如何,PasswordReminder
类可以连接到数据库,而不会出现任何问题,并且不会违反开放密封原则。
1class MySQLConnection implements DBConnectionInterface
2{
3 public function connect()
4 {
5 // handle the database connection
6 return 'Database connection';
7 }
8}
9
10class PasswordReminder
11{
12 private $dbConnection;
13
14 public function __construct(DBConnectionInterface $dbConnection)
15 {
16 $this->dbConnection = $dbConnection;
17 }
18}
这个代码确立了高级和低级模块都取决于抽象。
结论
在本文中,您被介绍了SOLID Code的五项原则:遵守SOLID原则的项目可以与合作者共享,扩展,修改,测试和重塑,并具有更少的复杂性。