SOLID:面向对象设计的前 5 项原则

介绍

_SOLID 是罗伯特·C.马丁(Robert C. Martin)的第一个五个面向对象设计原则(OOD)的缩写(也称为 [Bob Uncle])。

<$>[注] **注:**虽然这些原则可以应用于各种编程语言,但本文中的示例代码将使用PHP。

这些原则建立了实践,借助于软件开发,并考虑在项目增长时进行维护和扩展,采用这些实践还可以有助于避免代码臭味,重构代码,以及敏捷或适应性软件开发。

坚实代表:

在本文中,您将单独介绍每个原则,以了解 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示例中建立起,您需要支持CuboidSpheroid的新三维形状,这些形状还需要计算体积

让我们来考虑如果您修改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}

这是一个更好的方法,但在键入这些接口时要注意的一个陷阱,而不是使用ShapeInterfaceThreeDimensionalShapeInterface,您可以创建另一个接口,也许是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原则的项目可以与合作者共享,扩展,修改,测试和重塑,并具有更少的复杂性。

继续学习,阅读有关其他实践的 敏捷适应性软件开发

Published At
Categories with 技术
Tagged with
comments powered by Disqus