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

介绍

SOLID是 Robert C. Martin(也称为Uncle Bob的前五个面向对象设计 (OOD) 原则的首字母缩写词

注意:虽然这些原则适用于各种编程语言,但本文中包含的示例代码将使用 PHP。

这些原则建立了有助于开发软件的实践,同时考虑到随着项目的发展而维护和扩展。采用这些实践还有助于避免代码异味、重构代码以及敏捷或自适应软件开发。

固体代表:

在本文中,您将分别介绍每个原则,以了解 SOLID 如何帮助您成为更好的开发人员。

单一职责原则

单一职责原则 (SRP) 规定:

一个班级应该只有一个改变的理由,这意味着一个班级应该只有一个工作。

例如,考虑一个应用程序,它采用一组形状(圆形和正方形)并计算该集合中所有形状的面积总和。

首先,创建形状类并让构造函数设置所需的参数。

对于正方形,您需要知道length边的 :

class Square
{
    public $length;

    public function construct($length)
    {
        $this->length = $length;
    }
}

对于圈子,您需要知道radius

class Circle
{
    public $radius;

    public function construct($radius)
    {
        $this->radius = $radius;
    }
}

接下来,创建AreaCalculator类,然后编写逻辑来总结所有提供的形状的面积。正方形的面积是用长度的平方来计算的。圆的面积是用圆周率乘以半径的平方来计算的。

class AreaCalculator
{
    protected $shapes;

    public function __construct($shapes = [])
    {
        $this->shapes = $shapes;
    }

    public function sum()
    {
        foreach ($this->shapes as $shape) {
            if (is_a($shape, 'Square')) {
                $area[] = pow($shape->length, 2);
            } elseif (is_a($shape, 'Circle')) {
                $area[] = pi() * pow($shape->radius, 2);
            }
        }

        return array_sum($area);
    }

    public function output()
    {
        return implode('', [
          '',
              'Sum of the areas of provided shapes: ',
              $this->sum(),
          '',
      ]);
    }
}

要使用AreaCalculator该类,您需要实例化该类并传入一组形状并在页面底部显示输出。

这是一个包含三个形状集合的示例:

  • 一个半径为 2 的圆
  • 一个长为5的正方形
  • 长度为 6 的第二个正方形
$shapes = [
  new Circle(2),
  new Square(5),
  new Square(6),
];

$areas = new AreaCalculator($shapes);

echo $areas->output();

输出方法的问题在于AreaCalculator处理输出数据的逻辑。

考虑将输出转换为另一种格式(如 JSON)的场景。

所有的逻辑都将由AreaCalculator处理这将违反单一责任原则。AreaCalculator班只应提供形状的面积之和关注。它不应该关心用户是想要 JSON 还是 HTML。

为了解决这个问题,您可以创建一个单独的SumCalculatorOutputter类并使用该新类来处理将数据输出给用户所需的逻辑:

class SumCalculatorOutputter
{
    protected $calculator;

    public function __constructor(AreaCalculator $calculator)
    {
        $this->calculator = $calculator;
    }

    public function JSON()
    {
        $data = [
          'sum' => $this->calculator->sum(),
      ];

        return json_encode($data);
    }

    public function HTML()
    {
        return implode('', [
          '',
              'Sum of the areas of provided shapes: ',
              $this->calculator->sum(),
          '',
      ]);
    }
}

SumCalculatorOutputter班会的工作是这样的:

$shapes = [
  new Circle(2),
  new Square(5),
  new Square(6),
];

$areas = new AreaCalculator($shapes);
$output = new SumCalculatorOutputter($areas);

echo $output->JSON();
echo $output->HTML();

现在,您需要向用户输出数据的逻辑由SumCalculatorOutputter处理

这满足单一责任原则。

开闭原则

开闭原则 (OCP) 规定:

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

这意味着一个类应该是可扩展的,而无需修改类本身。

让我们重温这个AreaCalculator类,并关注sum方法:

class AreaCalculator
{
    protected $shapes;

    public function __construct($shapes = [])
    {
        $this->shapes = $shapes;
    }

    public function sum()
    {
        foreach ($this->shapes as $shape) {
            if (is_a($shape, 'Square')) {
                $area[] = pow($shape->length, 2);
            } elseif (is_a($shape, 'Circle')) {
                $area[] = pi() * pow($shape->radius, 2);
            }
        }

        return array_sum($area);
    }
}

考虑这样一个场景,用户想要sum其他形状,如三角形、五边形、六边形等。您必须不断编辑此文件并添加更多if/else块。这将违反开闭原则。

可以改进此sum方法的一种方法是从AreaCalculator类方法中删除计算每个形状面积的逻辑,并将其附加到每个形状的类中。

这是在 中area定义方法Square

class Square
{
    public $length;

    public function __construct($length)
    {
        $this->length = $length;
    }

    public function area()
    {
        return pow($this->length, 2);
    }
}

这是在 中area定义方法Circle

class Circle
{
    public $radius;

    public function construct($radius)
    {
        $this->radius = $radius;
    }

    public function area()
    {
        return pi() * pow($shape->radius, 2);
    }
}

然后可以将sumfor方法AreaCalculator重写为:

class AreaCalculator
{
    // ...

    public function sum()
    {
        foreach ($this->shapes as $shape) {
            $area[] = $shape->area();
        }

        return array_sum($area);
    }
}

现在,您可以在不破坏代码的情况下创建另一个形状类并在计算总和时将其传入。

然而,出现了另一个问题。你怎么知道传入的对象AreaCalculator实际上是一个形状,或者这个形状是否有一个名为 的方法area

接口进行编码是 SOLID 不可或缺的一部分。

创建一个ShapeInterface支持area

interface ShapeInterface
{
    public function area();
}

将您的形状类修改implementShapeInterface.

以下是更新Square

class Square implements ShapeInterface
{
    // ...
}

这是更新Circle

class Circle implements ShapeInterface
{
    // ...
}

sum方法中AreaCalculator,您可以检查提供的形状是否实际上是 的实例ShapeInterface否则,抛出异常:

 class AreaCalculator
{
    // ...

    public function sum()
    {
        foreach ($this->shapes as $shape) {
            if (is_a($shape, 'ShapeInterface')) {
                $area[] = $shape->area();
                continue;
            }

            throw new AreaCalculatorInvalidShapeException();
        }

        return array_sum($area);
    }
}

满足开闭原则。

里氏替换原则

Liskov 替换原则指出:

令 q(x) 是 T 类型的 x 对象的可证明属性。 那么 q(y) 应该对于 S 类型的对象 y 是可证明的,其中 S 是 T 的子类型。

这意味着每个子类或派生类都应该可以替代它们的基类或父类。

构建示例AreaCalculator类,考虑一个VolumeCalculator扩展AreaCalculator的新类:

class VolumeCalculator extends AreaCalculator
{
    public function construct($shapes = [])
    {
        parent::construct($shapes);
    }

    public function sum()
    {
        // logic to calculate the volumes and then return an array of output
        return [$summedData];
    }
}

回想一下这个SumCalculatorOutputter类是这样的:

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(
            '',
                'Sum of the areas of provided shapes: ',
                $this->calculator->sum(),
            ''
        ));
    }
}

如果您尝试运行这样的示例:

$areas = new AreaCalculator($shapes);
$volumes = new VolumeCalculator($solidShapes);

$output = new SumCalculatorOutputter($areas);
$output2 = new SumCalculatorOutputter($volumes);

当您HTML$output2对象调用该方法时,您将收到一条E_NOTICE错误消息,通知您将数组转换为字符串。

为了解决这个问题,不是从VolumeCalculator类 sum 方法返回一个数组,而是返回$summedData

class VolumeCalculator extends AreaCalculator
{
    public function construct($shapes = [])
    {
        parent::construct($shapes);
    }

    public function sum()
    {
        // logic to calculate the volumes and then return a value of output
        return $summedData;
    }
}

$summedData可以是浮点,双精度或整数。

这满足 Liskov 替换原则。

接口隔离原则

接口隔离原则规定:

永远不应该强迫客户端实现它不使用的接口,或者不应该强迫客户端依赖它们不使用的方法。

从以前的尚筑ShapeInterface例如,您将需要支持的新的三维形状CuboidSpheroid,以及这些形状需要也算volume

让我们考虑一下如果您要修改ShapeInterface以添加另一个合同会发生什么

interface ShapeInterface
{
    public function area();

    public function volume();
}

现在,您创建的任何形状都必须实现该volume方法,但是您知道正方形是平面形状并且它们没有体积,因此该接口将强制Square该类实现一个它没有使用的方法。

这将违反接口隔离原则。相反,您可以创建另一个名为ThreeDimensionalShapeInterface具有volume契约的接口并且三维形状可以实现此接口:

interface ShapeInterface
{
    public function area();
}

interface ThreeDimensionalShapeInterface
{
    public function volume();
}

class Cuboid implements ShapeInterface, ThreeDimensionalShapeInterface
{
    public function area()
    {
        // calculate the surface area of the cuboid
    }

    public function volume()
    {
        // calculate the volume of the cuboid
    }
}

这是一种更好的方法,但需要注意的一个陷阱是在对这些接口进行类型提示时。除了使用 aShapeInterface或 a ThreeDimensionalShapeInterface,您还可以创建另一个接口,例如ManageShapeInterface,并在平面和三维形状上实现它。

通过这种方式,您可以使用单个 API 来管理形状:

interface ManageShapeInterface
{
    public function calculate();
}

class Square implements ShapeInterface, ManageShapeInterface
{
    public function area()
    {
        // calculate the area of the square
    }

    public function calculate()
    {
        return $this->area();
    }
}

class Cuboid implements ShapeInterface, ThreeDimensionalShapeInterface, ManageShapeInterface
{
    public function area()
    {
        // calculate the surface area of the cuboid
    }

    public function volume()
    {
        // calculate the volume of the cuboid
    }

    public function calculate()
    {
        return $this->area();
    }
}

现在在AreaCalculator课堂上,您可以用 替换对area方法的调用calculate,还可以检查对象是否是 的实例ManageShapeInterface而不是ShapeInterface

满足接口隔离原则。

依赖倒置原则

依赖倒置原则指出:

实体必须依赖于抽象,而不是具体。它指出高级模块不能依赖于低级模块,但它们应该依赖于抽象。

这个原则允许解耦。

以下是PasswordReminder连接到 MySQL 数据库的示例

class MySQLConnection
{
    public function connect()
    {
        // handle the database connection
        return 'Database connection';
    }
}

class PasswordReminder
{
    private $dbConnection;

    public function __construct(MySQLConnection $dbConnection)
    {
        $this->dbConnection = $dbConnection;
    }
}

首先,theMySQLConnection是低级模块,而thePasswordReminder是高级模块,但是根据SOLIDD的定义,它声明依赖于抽象,而不是具体化上面的这个片段违反了这个原则,因为PasswordReminder类被迫依赖于MySQLConnection类。

之后,如果要更改数据库引擎,则还必须编辑PasswordReminder类,这将违反开闭原则

PasswordReminder类不应该关心什么数据库应用程序使用。为了解决这些问题,您可以对接口进行编码,因为高级和低级模块应该依赖于抽象:

interface DBConnectionInterface
{
    public function connect();
}

该接口有一个 connect 方法,MySQLConnection该类实现了这个接口。此外,不是MySQLConnection在 的构造函数中直接类型提示PasswordReminder,而是类型提示DBConnectionInterface,无论您的应用程序使用什么类型的数据库,PasswordReminder该类都可以毫无问题地连接到数据库,并且不违反开闭原则.

class MySQLConnection implements DBConnectionInterface
{
    public function connect()
    {
        // handle the database connection
        return 'Database connection';
    }
}

class PasswordReminder
{
    private $dbConnection;

    public function __construct(DBConnectionInterface $dbConnection)
    {
        $this->dbConnection = $dbConnection;
    }
}

此代码确定高级和低级模块都依赖于抽象。

结论

在本文中,您了解了 SOLID Code 的五项原则。遵循 SOLID 原则的项目可以与合作者共享、扩展、修改、测试和重构,减少复杂性。

通过阅读有关敏捷自适应软件开发的其他实践来继续学习

觉得文章有用?

点个广告表达一下你的爱意吧 !😁