S.O.L.I.D 是首个 5 个面向对象设计 (OOD) 准则的首字母缩写,这些准则是由 Robert C. Martin 提出的,他更为人所熟知的名字是 Uncle Bob。php
这些准则使得开发出易扩展、可维护的软件变得更容易。也使得代码更精简、易于重构。一样也是敏捷开发和自适应软件开发的一部分。数据库
扩展出来的首字母缩略词看起来可能很复杂,实际上它们很容易理解。json
接下来让咱们看看每一个原则,来了解为何 S.O.L.I.D 能够帮助咱们成为更好的开发人员。api
缩写是 S.R.P ,该原则内容是:数组
一个类有且只能有一个因素使其改变,意思是一个类只应该有单一职责。ide
例如,假设咱们有一些图形,而且想要计算这些图形的总面积。是的,这很简单对不对?测试
class Circle { public $radius; public function construct($radius) { $this->radius = $radius; } } class Square { public $length; public function construct($length) { $this->length = $length; } }
首先,咱们建立图形类,该类的构造方法初始化必要的参数。接下来,建立 AreaCalculator 类,而后编写计算指定图形总面积的逻辑代码。this
class AreaCalculator { protected $shapes; public function __construct($shapes = array()) { $this->shapes = $shapes; } public function sum() { // logic to sum the areas } public function output() { return implode('', array( "", "Sum of the areas of provided shapes: ", $this->sum(), "" )); } }
AreaCalculator 使用方法,咱们只需简单的实例化这个类,而且传递一个图形数组,在页面底部展现输出内容。编码
$shapes = array( new Circle(2), new Square(5), new Square(6) ); $areas = new AreaCalculator($shapes); echo $areas->output();
输出方法的问题在于,AreaCalculator 处理了数据输出逻辑。所以,假如用户但愿将数据以 json 或者其余格式输出呢?设计
全部逻辑都由 AreaCalculator 类处理,这偏偏违反了单一职责原则 (SRP);AreaCalculator 类应该只负责计算图形的总面积,它不该该关心用户是想要 json 仍是 HTML 格式数据。
所以,要解决这个问题,能够建立一个 SumCalculatorOutputter 类,并使用它来处理所需的显示逻辑,以处理全部图形的总面积该如何显示。
SumCalculatorOutputter 类的工做方式以下:
$shapes = array( new Circle(2), new Square(5), new Square(6) ); $areas = new AreaCalculator($shapes); $output = new SumCalculatorOutputter($areas); echo $output->JSON(); echo $output->HAML(); echo $output->HTML(); echo $output->JADE();
如今,不管你想向用户输出什么格式数据,都由 SumCalculatorOutputter 类处理。
对象和实体应该对扩展开放,可是对修改关闭。
简单的说就是,一个类应该不用修改其自身就能很容易扩展其功能。让咱们看一下 AreaCalculator 类,特别是 sum 方法。
public function sum() { foreach($this->shapes as $shape) { if(is_a($shape, 'Square')) { $area[] = pow($shape->length, 2); } else if(is_a($shape, 'Circle')) { $area[] = pi() * pow($shape->radius, 2); } } return array_sum($area); }
若是咱们想用 sum 方法能计算更多图形的面积,咱们就不得不添加更多的 if/else,然而这违背了开闭原则。
让这个 sum 方法变得更好的方式是将计算每一个形状面积的代码逻辑移出 sum 方法,将其放进各个形状类中:
class Square { public $length; public function __construct($length) { $this->length = $length; } public function area() { return pow($this->length, 2); } }
相同的操做应该被用来处理 Circle 类, 在类中添加一个 area 方法。如今,计算任何形状面积之和应该像下边这样简单:
public function sum() { foreach($this->shapes as $shape) { $area[] = $shape->area(); } return array_sum($area); }
接下来咱们能够建立另外一个形状类并在计算总和时传递它而不破坏咱们的代码。 然而如今又出现了另外一个问题,咱们怎么能知道传入 AreaCalculator 的对象其实是一个形状,或者形状对象中有一个 area 方法?
接口编码是实践 S.O.L.I.D 的一部分,例以下面的例子中咱们建立一个接口类,每一个形状类都会实现这个接口类:
interface ShapeInterface { public function area(); } class Circle implements ShapeInterface { public $radius; public function __construct($radius) { $this->radius = $radius; } public function area() { return pi() * pow($this->radius, 2); } }
在咱们的 AreaCalculator 的 sum 方法中,咱们能够检查提供的形状类的实例是不是 ShapeInterface 的实现,不然咱们就抛出一个异常:
public function sum() { foreach($this->shapes as $shape) { if(is_a($shape, 'ShapeInterface')) { $area[] = $shape->area(); continue; } throw new AreaCalculatorInvalidShapeException; } return array_sum($area); }
若是对每个类型为 T1 的对象 o1,都有类型为 T2 的对象 o2,使得以 T1 定义的全部程序 P 在全部的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。
这句定义的意思是说:每一个子类或者衍生类能够毫无问题地替代基类 / 父类。
依然使用 AreaCalculator 类,假设咱们有一个 VolumeCalculator 类,这个类继承了 AreaCalculator 类:
class VolumeCalculator extends AreaCalulator { public function construct($shapes = array()) { parent::construct($shapes); } public function sum() { // logic to calculate the volumes and then return and array of output return array($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 错误,提示咱们数组被当作字符串使用的错误。
为了修复这个问题,只需:
public function sum() { // logic to calculate the volumes and then return and array of output return $summedData; }
而不是让 VolumeCalculator 类的 sum 方法返回数组。
$summedData 是一个浮点数、双精度浮点数或者整型。
使用方(client)不该该依赖强制实现不使用的接口,或不该该依赖不使用的方法。
继续使用上面的 shapes 例子,已知拥有一个实心块,若是咱们须要计算形状的体积,咱们能够在 ShapeInterface 中添加一个方法:
interface ShapeInterface { public function area(); public function volume(); }
任何形状建立的时候必须实现 volume 方法,可是「平面」是没有体积的,实现这个接口会强制的让「平面」类去实现一个本身用不到的方法。
ISP 原则不容许这么去作,因此咱们应该建立另一个拥有 volume 方法的 SolidShapeInterface 接口去代替这种方式,这样相似立方体的实心体就能够实现这个接口了:
interface ShapeInterface { public function area(); } interface SolidShapeInterface { public function volume(); } class Cuboid implements ShapeInterface, SolidShapeInterface { public function area() { //计算长方体的表面积 } public function volume() { // 计算长方体的体积 } }
这是一个更好的方式,可是要注意提示类型时不要仅仅提示一个 ShapeInterface 或 SolidShapeInterface。
你能建立其它的接口,好比 ManageShapeInterface , 并在平面和立方体的类上实现它,这样你能很容易的看到有一个用于管理形状的 api。例:
interface ManageShapeInterface { public function calculate(); } class Square implements ShapeInterface, ManageShapeInterface { public function area() { /Do stuff here/ } public function calculate() { return $this->area(); } } class Cuboid implements ShapeInterface, SolidShapeInterface, ManageShapeInterface { public function area() { /Do stuff here/ } public function volume() { /Do stuff here/ } public function calculate() { return $this->area() + $this->volume(); } }
如今在 AreaCalculator 类中,咱们能够很容易地用 calculate 替换对 area 方法的调用,并检查对象是不是 ManageShapeInterface 的实例,而不是 ShapeInterface。
最后,但毫不是最不重要的:
实体必须依赖抽象而不是具体的实现。即高等级模块不该该依赖低等级模块,他们都应该依赖抽象。
这也许听起来让人头大,可是它很容易理解。这个原则可以很好的解耦,举个例子彷佛是解释这个原则最好的方法:
class PasswordReminder { private $dbConnection; public function __construct(MySQLConnection $dbConnection) { $this->dbConnection = $dbConnection; } }
首先 MySQLConnection 是低等级模块,PasswordReminder 是高等级模块,可是根据 S.O.L.I.D 中 D 的解释:依赖于抽象而不依赖与实现, 上面的代码段违背了这一原则,由于 PasswordReminder 类被强制依赖于 MySQLConnection 类。
稍后,若是你但愿修改数据库驱动,你也不得不修改 PasswordReminder 类,所以就违背了 开闭原则(Open-close principle)。
此 PasswordReminder 类不该该关注你的应用使用了什么数据库,为了进一步解决这个问题,咱们「面向接口写代码」,因为高等级和低等级模块都应该依赖于抽象,咱们能够建立一个接口:
interface DBConnectionInterface { public function connect(); }
这个接口有一个链接数据库的方法,MySQLConnection 类实现该接口,在 PasswordReminder 的构造方法中不要直接将类型约束设置为 MySQLConnection 类,而是设置为接口类,这样不管你的应用使用什么类型的数据库,PasswordReminder 类都能毫无问题地链接数据库,且不违背 开闭原则。
class MySQLConnection implements DBConnectionInterface { public function connect() { return "Database connection"; } } class PasswordReminder { private $dbConnection; public function __construct(DBConnectionInterface $dbConnection) { $this->dbConnection = $dbConnection; } }
从上面一小段代码,你如今能看出高等级和低等级模块都依赖于抽象了。
说实话,S.O.L.I.D 一开始彷佛很难掌握,但只要不断地使用和遵照其原则,它将成为你的一部分,使你的代码易被扩展、修改,测试,即便重构也不容易出现问题。