S.O.L.I.D是Robert C. Martin提出的前五个面向对象设计(OOD)原则的首字母缩写,他更为人所熟知的名字是Uncle Bob。
将这些原理结合在一起,可使程序员轻松开发易于维护和扩展的软件。它们还使开发人员可以轻松避免代码异味,轻松重构代码,并且是敏捷或自适应软件开发的一部分。
注意:这只是一篇简单的“欢迎使用_ S.O.L.I.D ”,它只是阐明了S.O.L.I.D是什么.。
更多学习内容可以访问
从码农成为架构师的修炼之路
S.O.L.I.D代表:
首字母缩略词在扩展时可能看起来很复杂,但是却很容易掌握。
-
S - 单一责任原则
-
O - 开闭原理
-
L - Liskov替代原理
-
I - 接口隔离原理
-
D - 依赖倒置原则
让我们分别看一下每个原理,了解一下S.O.L.I.D为什么可以帮助使我们成为更好的开发人员。
单一责任原则
SRP的简称-此原则指出:
一个类有且只能有一个因素使其改变,意思是一个类只应该有单一职责.
例如,假设我们有一些形状,我们想对形状的所有区域求和。好吧,这很简单对吧?
class Circle { public $radius; public function construct($radius) { $this->radius = $radius; } } class Square { public $length; public function construct($length) { $this->length = $length; } }
首先,我们创建形状类,并让构造函数设置所需的参数。接下来,我们继续创建AreaCalculator类,然后编写逻辑以总结所有提供的形状的面积。
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块,这违背了Open-closed原理。
我们可以使这种求和方法更好的一种方法是从求和方法中删除用于计算每个形状的面积的逻辑,并将其附加到形状的类中。
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); } }
在我们的AreaCalculatorsum方法中,我们可以检查所提供的形状是否实际上是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); }
Liskov替代原则
如果对每一个类型为 T1 的对象 o1,都有类型为 T2 的对象 o2,使得以 T1 定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。
所有这一切都说明,每个子类/派生类都可以替代其基类/父类。
仍然使用OutAreaCalculator类,例如我们有一个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 AreaCalculator($solidShapes); $output = new SumCalculatorOutputter($areas); $output2 = new SumCalculatorOutputter($volumes);
该程序不会出问题,但是当我们在$ output2对象上调用HTML方法时,会收到E _ NOTICE错误,通知我们数组转换为字符串。
若要解决此问题,而不是从VolumeCalculator类的sum方法返回数组,你应该简单地:
public function sum() { // logic to calculate the volumes and then return and array of output return $summedData; }
求和后的数据为浮点,双精度或整数。
接口隔离原理
使用方(client)不应该依赖强制实现不使用的接口,或不应该依赖不使用的方法。
仍然使用形状示例,我们知道我们也有实体形状,因此由于我们还想计算形状的体积,因此可以向ShapeInterface添加另一个协定:
interface ShapeInterface { public function area(); public function volume(); }
我们创建的任何形状都必须实现volume方法,但是我们知道正方形是扁平形状并且它们没有体积,因此此接口将强制Square类实现一种不使用的方法。
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原理。
该PasswordReminder类不应该关心什么数据库应用程序使用,以解决这个问题,我们再次“代码的接口”,因为高层次和低层次的模块应该依赖于抽象,我们可以创建一个界面:
interface DBConnectionInterface { public function connect(); }
该接口具有一个connect方法,而MySQLConnection类实现了此接口,而且也没有直接在PasswordReminder的构造函数中直接提示MySQLConnection类,而是改为提示该接口,无论您的应用程序使用哪种数据库类型,PasswordReminder类可以轻松连接到数据库而不会出现任何问题,并且不会违反OCP。
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似乎很少,但是通过不断使用和遵守其准则,它成为你和你的代码的一部分,可以轻松地对其进行扩展,修改,测试和重构,而不会出现任何问题。
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(); }}