简介
SOLID是Robert C. Martin(又称Bob叔叔)提出的前五条面向对象设计(OOD)原则的缩写。
**注意:**虽然这些原则可以适用于各种编程语言,但本文中的示例代码将使用PHP。
这些原则确立了开发软件的做法,并考虑到了随着项目的发展而进行维护和扩展。采用这些实践也有助于避免代码臭味,重构代码,以及敏捷或适应性软件开发。
SOLID代表。
在这篇文章中,我们将分别介绍每个原则,以了解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 类方法中移除,并将其附加到每个形状的类中。
这里是定义在Square 的area 方法。
class Square
{
public $length;
public function __construct($length)
{
$this->length = $length;
}
public function area()
{
return pow($this->length, 2);
}
}
这里是定义在Circle 中的area 方法。
class Circle
{
public $radius;
public function construct($radius)
{
$this->radius = $radius;
}
public function area()
{
return pi() * pow($shape->radius, 2);
}
}
然后,AreaCalculator 的sum 方法可以被改写为。
class AreaCalculator
{
// ...
public function sum()
{
foreach ($this->shapes as $shape) {
$area[] = $shape->area();
}
return array_sum($area);
}
}
现在,你可以创建另一个形状类,并在计算总和时传入它而不破坏代码。
然而,另一个问题出现了。你怎么知道传入AreaCalculator 的对象实际上是一个形状,或者这个形状是否有一个名为area 的方法?
对接口进行编码是SOLID的一个组成部分。
创建一个支持area 的ShapeInterface 。
interface ShapeInterface
{
public function area();
}
修改你的形状类,以implement ,ShapeInterface 。
这里是对Square 的更新。
class Square implements ShapeInterface
{
// ...
}
这里是对Circle 的更新。
class Circle implements ShapeInterface
{
// ...
}
在AreaCalculator 的sum 方法中,你可以检查所提供的形状是否真的是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);
当你在$output2 对象上调用HTML 方法时,你会得到一个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 为例,你需要支持新的三维形状Cuboid 和Spheroid ,这些形状也需要计算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
}
}
这是一个更好的方法,但需要注意的是在对这些接口进行类型提示时的一个隐患。与其使用ShapeInterface 或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 类中,你可以用calculate 替换对area 方法的调用,同时检查对象是否是ManageShapeInterface 的实例而不是ShapeInterface 。
这就满足了接口隔离原则。
依赖反转原则
依赖性反转原则指出。
实体必须依赖抽象,而不是依赖具体化。它指出,高层模块不能依赖低层模块,但它们应该依赖抽象物。
这一原则允许解耦。
下面是一个连接到MySQL数据库的PasswordReminder 的例子。
class MySQLConnection
{
public function connect()
{
// handle the database connection
return 'Database connection';
}
}
class PasswordReminder
{
private $dbConnection;
public function __construct(MySQLConnection $dbConnection)
{
$this->dbConnection = $dbConnection;
}
}
首先,MySQLConnection 是低级模块,而PasswordReminder 是高级模块,但根据SOLID中D的定义,它指出要依赖抽象,而不是依赖凝结。上面的这个片段违反了这一原则,因为PasswordReminder 类被强迫依赖于MySQLConnection 类。
之后,如果你要改变数据库引擎,你也必须编辑PasswordReminder 类,这就违反了开放-封闭原则。
PasswordReminder 类不应该关心你的应用程序使用什么数据库。为了解决这些问题,你可以对一个接口进行编码,因为高层和低层的模块应该依赖于抽象。
interface DBConnectionInterface
{
public function connect();
}
该接口有一个连接方法,MySQLConnection 类实现了这个接口。另外,在PasswordReminder 的构造函数中,不要直接对MySQLConnection 类进行类型提示,而是对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代码的五个原则。遵循SOLID原则的项目可以与合作者共享、扩展、修改、测试和重构,并减少复杂情况。