SOLID 是 Robert C. Martin(也称为 Uncle Bob)提出的前五个面向对象设计 (OOD) 原则的首字母缩写词。
这些原则建立在开发软件的实践经验中,并考虑到了随着项目的发展进行维护和扩展的需求。采用这些原则也有助于避免代码质量差,重构代码,以及敏捷或适应性软件开发。
- S – 单一职责原则
- O – 开闭原则
- L – Liskov 替换原则
- I – 接口隔离原则
- D – 依赖倒置原则
现在来说说第一个:单一职责原则S
“一个类应该有一个,而且只有一个职责”
这意味着如果我们的类承担了多个职责,我们将具有高度耦合。原因是我们的代码在任何更改时都会变得脆弱。
假设我们有一个 User 类,如下所示:
<?php
class User {
private $email;
// Getter and setter...
public function store() {
// Store attributes into a database...
}
}
在这种情况下,该方法store
超出了范围,该职责应该属于管理数据库的类。
这里的解决方案是创建两个类,每个类都有适当的职责。
<?php
class User {
private $email;
// Getter and setter...
}
class UserDB {
public function store(User $user) {
// Store the user into a database...
}
}
现在让我们继续讨论 SOLID开闭原则 O
对象或实体应该对扩展开放但对修改关闭。
根据这一原则,软件实体必须易于使用新功能进行扩展,而无需修改其使用中的现有代码。
假设我们必须计算一些对象的总面积,为此我们需要一个AreaCalculator
类,它只计算每个形状面积的总和。
这里的问题是每个形状都有不同的方法来计算自己的面积。
<?php
class Rectangle {
public $width;
public $height;
public function __construct($width, $height) {
$this->width = $width;
$this->height = $height;
}
}
class Square {
public $length;
public function __construct($length) {
$this->length = $length;
}
}
class AreaCalculator {
protected $shapes;
public function __construct($shapes = array()) {
$this->shapes = $shapes;
}
public function sum() {
$area = [];
foreach($this->shapes as $shape) {
if($shape instanceof Square) {
$area[] = pow($shape->length, 2);
} else if($shape instanceof Rectangle) {
$area[] = $shape->width * $shape->height;
}
}
return array_sum($area);
}
}
如果我们添加像 a 这样的另一个形状,Circle
我们必须更改AreaCalculator
才能计算新的形状区域,这是不可持续的。
这里的解决方案是创建一个Shape
具有 area 方法的接口,并将由所有其他形状实现。
<?php
interface Shape {
public function area();
}
class Rectangle implements Shape {
private $width;
private $height;
public function __construct($width, $height) {
$this->width = $width;
$this->height = $height;
}
public function area() {
return $this->width * $this->height;
}
}
class Square implements Shape {
private $length;
public function __construct($length) {
$this->length = $length;
}
public function area() {
return pow($this->length, 2);
}
}
class AreaCalculator {
protected $shapes;
public function __construct($shapes = array()) {
$this->shapes = $shapes;
}
public function sum() {
$area = [];
foreach($this->shapes as $shape) {
$area[] = $shape->area();
}
return array_sum($area);
}
}
这样,我们将只使用一种方法来计算总和,如果我们需要添加一个新形状,实现Shape
接口即可。
SOLID 中的第三个原则L:Liskov 替换原则
令 q(x) 是关于类型 T 的 x 对象可证明的属性。那么 q(y) 应该对于类型 S 的对象 y 是可证明的,其中 S 是 T 的子类型。
该原则说,对象必须可以被其子类型的实例替换,而不会改变我们系统的正确功能。
我知道这很难理解,所以我将含义分为 5 个部分。
- 子函数的参数必须与父函数的参数相匹配
- 子函数的返回类型必须与父函数的返回类型一致
- 子函数的前置条件不能大于父函数的前置条件
- 子函数的后置条件不能小于父函数的后置条件。
- 子方法抛出的异常必须与父方法抛出的异常相同或继承于父方法的异常。
为了完全理解这一点,这里有一个场景:
想象一下管理两种类型的咖啡机。根据用户计划,我们将使用基本或高级咖啡机,唯一的区别是高级机器比基本机器制作的香草咖啡好。
<?php
interface CoffeeMachineInterface {
public function brewCoffee($selection);
}
class BasicCoffeeMachine implements CoffeeMachineInterface {
public function brewCoffee($selection) {
switch ($selection) {
case 'ESPRESSO':
return $this->brewEspresso();
default:
throw new CoffeeException('Selection not supported');
}
}
protected function brewEspresso() {
// Brew an espresso...
}
}
class PremiumCoffeeMachine extends BasicCoffeeMachine {
public function brewCoffee($selection) {
switch ($selection) {
case 'ESPRESSO':
return $this->brewEspresso();
case 'VANILLA':
return $this->brewVanillaCoffee();
default:
throw new CoffeeException('Selection not supported');
}
}
protected function brewVanillaCoffee() {
// Brew a vanilla coffee...
}
}
function getCoffeeMachine(User $user) {
switch ($user->getPlan()) {
case 'PREMIUM':
return new PremiumCoffeeMachine();
case 'BASIC':
default:
return new BasicCoffeeMachine();
}
}
function prepareCoffee(User $user, $selection) {
$coffeeMachine = getCoffeeMachine($user);
return $coffeeMachine->brewCoffee($selection);
}
两台机器的主程序行为必须相同。
I代表的第四个原则:接口隔离原则
永远不应强迫客户端实现它不使用的接口,也不应强迫客户端依赖于它们不使用的方法。
这个原则定义了一个类永远不应该实现一个不去使用的接口。
在这种情况下,意味着在我们的实现中我们将拥有不需要的方法。
解决方案是开发特定接口而不是通用接口。
这是一个场景,想象一下我们发明了FutureCar
一种既能飞行又能驱动的东西……
<?php
interface VehicleInterface {
public function drive();
public function fly();
}
class FutureCar implements VehicleInterface {
public function drive() {
echo 'Driving a future car!';
}
public function fly() {
echo 'Flying a future car!';
}
}
class Car implements VehicleInterface {
public function drive() {
echo 'Driving a car!';
}
public function fly() {
throw new Exception('Not implemented method');
}
}
class Airplane implements VehicleInterface {
public function drive() {
throw new Exception('Not implemented method');
}
public function fly() {
echo 'Flying an airplane!';
}
}
如您所见,主要问题是 Car 并Airplane
具有不使用的方法。
解决方案是将其拆分VehicleInterface
为两个更具体的接口,仅在必要时使用,如下所示:
<?php
interface CarInterface {
public function drive();
}
interface AirplaneInterface {
public function fly();
}
class FutureCar implements CarInterface, AirplaneInterface {
public function drive() {
echo 'Driving a future car!';
}
public function fly() {
echo 'Flying a future car!';
}
}
class Car implements CarInterface {
public function drive() {
echo 'Driving a car!';
}
}
class Airplane implements AirplaneInterface {
public function fly() {
echo 'Flying an airplane!';
}
}
最后但并非最不重要的是 D,它代表:依赖倒置原则
实体必须依赖于抽象而不是具体。它指出高级模块不能依赖于低级模块,但它们应该依赖于抽象。
这个原则意味着一个特定的类不应该直接依赖于另一个类,而应该依赖于这个类的抽象。
这个原则允许解耦和更多的代码可重用性。
让我们获取该类的第一个示例UserDB
。此类可能依赖于数据库连接:
<?php
class UserDB {
private $dbConnection;
public function __construct(MySQLConnection $dbConnection) {
$this->$dbConnection = $dbConnection;
}
public function store(User $user) {
// Store the user into a database...
}
}
在这种情况下,UserDB
该类直接依赖于 MySQL 数据库。
这意味着如果我们要更改正在使用的数据库引擎,我们需要重写这个类并违反开闭原则。
解决方案是开发一个数据库连接的抽象:
<?php
interface DBConnectionInterface {
public function connect();
}
class MySQLConnection implements DBConnectionInterface {
public function connect() {
// Return the MySQL connection...
}
}
class UserDB {
private $dbConnection;
public function __construct(DBConnectionInterface $dbConnection) {
$this->dbConnection = $dbConnection;
}
public function store(User $user) {
// Store the user into a database...
}
}
此代码确定高级和低级模块都依赖于抽象。
了解了面向对象编程的 5 SOLID 原则之后,您可以使用这些原则来构建最先进的代码并保证优雅的质量,遵循它们有助于您编写易于扩展、可重用和重构的软件。
遵循 SOLID 原则的项目可以与合作者共享、扩展、修改、测试和重构,从而减少复杂性。