##S.O.L.I.Disthe first of 5 object-oriented designs(OOD)## An acronym for #Principles, these principles were proposed by Robert C. Martin, better known asUncle Bob.These guidelines make it easier to develop scalable and maintainable software. It also makes the code more streamlined and easier to refactor. Also part of agile development and adaptive software development.
Note:This is not a simple introduction to "Welcome to_S.O.L.I.D", this article wants to clarifyS.O.L.I.DWhat is it. (Related tutorial recommendation:php video tutorial)S.O.L.I.D means:
Single Responsibility Principle
. The content of this principle is:
A class has and can only have one factor that makes it Change, meaning that a class should only have a single responsibility.For example, suppose we have some shapes and want to calculate the total area of these shapes. Yes, it's easy, right?
class Circle { public $radius; public function construct($radius) { $this->radius = $radius; } } class Square { public $length; public function construct($length) { $this->length = $length; } }
First, we create a graphics class, and the constructor of this class initializes the necessary parameters. Next, create the
AreaCalculatorclass, and then write the logic code to calculate the total area of the specified graphic.
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
To use the method, we simply instantiate this class and pass it a graphics array to display the output content at the bottom of the page.
$shapes = array( new Circle(2), new Square(5), new Square(6) ); $areas = new AreaCalculator($shapes); echo $areas->output();The problem with the output method is that AreaCalculator
handles the data output logic. So, what if the user wants to output the data in json or other formats?All logic is handled by the
AreaCalculatorclass, which exactly violates the Single Responsibility Principle (SRP); theAreaCalculatorclass should only be responsible for calculating the total area of the graph, it does not You should care whether the user wants data in json or HTML format.So, to solve this problem, you can create a
SumCalculatorOutputterclass and use it to handle the display logic required to handle how the total area of all graphics should be displayed. The
SumCalculatorOutputterclass works as follows:
$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();Now, whatever format of data you want to output to the user is handled by the SumCalculatorOutputter
class.Opening and closing principle
Objects and entities should be open for extension, but closed for modification.Simply put, a class should be able to easily extend its functionality without modifying itself. Let's take a look at theAreaCalculator
class, specifically thesummethod.
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); }If we want to use the sum
method to calculate the area of more shapes, we have to add moreif/else blocks, but this violates Opening and closing principle.The way to make this
summethod better is to move the code logic that calculates the area of each shape out of the sum method and put it into each shape class:
class Square { public $length; public function __construct($length) { $this->length = $length; } public function area() { return pow($this->length, 2); } }The same operation should be used to handle the Circle
class, adding anareamethod to the class. Now, calculating the sum of the areas of any shape should be as simple as:
public function sum() { foreach($this->shapes as $shape) { $area[] = $shape->area(); } return array_sum($area); }Next we can create another shape class and pass it when calculating the sum without breaking our code. But now another question arises, how can we know that the object passed in AreaCalculator
is actually a shape, or that there is anareamethod in the shape object?Interface coding is part of the practice
S.O.L.I.D. For example, in the following example we create an interface class, and each shape class will implement this interface class:
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); } }In our In the sum method of AreaCalculator
, we can check whether the instance of the provided shape class is an implementation ofShapeInterface, otherwise we will throw an exception:
public function sum() { foreach($this->shapes as $shape) { if(is_a($shape, 'ShapeInterface')) { $area[] = $shape->area(); continue; } throw new AreaCalculatorInvalidShapeException; } return array_sum($area); }Principle of substitution If for every object o1 of type T1, there is an object o2 of type T2, so that all programs P defined with T1 are replaced with o2 in all objects o1 , the behavior of program P does not change, then type T2 is a subtype of type 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 AreaCalculator($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一开始似乎很难掌握,但只要不断地使用和遵守其原则,它将成为你的一部分,使你的代码易被扩展、修改,测试,即使重构也不容易出现问题。
相关PHP面向对象视频教程推荐:《PHP面向对象视频教程》