S.O.L.I.D是RobertC.Martin提出的面向对象设计(OOD)的前五项原则的首字母缩写,更广为人知的是鲍勃大叔。将这些原则结合在一起,可以让程序员轻松开发出易于维护和扩展的软件。它们还允许开发人员轻松避免代码异味,轻松重构代码,并且是敏捷或自适应软件开发的一部分。注意:这只是一个简单的“欢迎来到_S.O.L.I.D”,它只是阐明了什么是S.O.L.I.D。更多学习内容可以在ThePathfromCodertoArchitect找到S.O.L.I.D代表:缩写词展开时可能看起来很复杂,但掌握起来非常容易。S-单一职责原则O-开放封闭原则L-里氏替换原则I-接口隔离原则D-依赖倒置原则让我们分别看看每个原则,并理解为什么S.O.L.I.D可以帮助我们成为更好的开发人员。SingleResponsibilityPrinciple的缩写SRP——该原则指出:一个类具有且只有一个因素可以使它发生变化,也就是说一个类应该只有单一的职责。例如,假设我们有一些形状并且我们想要对形状的所有区域求和。好吧,这很容易,对吧?类圆{公共$radius;publicfunctionconstruct($radius){$this->radius=$radius;}}classSquare{public$length;publicfunctionconstruct($length){$this->length=$length;}}首先,我们创建形状类并让构造函数设置所需的参数。接下来,我们继续创建AreaCalculator类,然后编写逻辑来对所有提供的形状的面积求和。类AreaCalculator{protected$shapes;公共函数\_\_construct($shapes=array()){$this->shapes=$shapes;}publicfunctionsum(){//对面积求和的逻辑}publicfunctionoutput(){returnimplode('',array("","Sumoftheareasofprovidedshapes:",$this->sum(),""));}}要使用AreaCalculator类,我们只需实例化该类并传递形状数组,然后在页面底部显示输出。$shapes=array(newCircle(2),newSquare(5),newSquare(6));$areas=newAreaCalculator($shapes);echo$areas->output();输出方法的问题在于AreaCalculator处理输出数据的逻辑。那如果用户想把数据输出成json什么的怎么办呢?所有这些逻辑都将由AreaCalculator类处理,这正是SRP所反对的。AreaCalculator类应该只提供区域的汇总形状,它不应该关心用户需要JSON还是HTML。因此,要解决此问题,您可以创建一个SumCalculatorOutputter类,并使用它来处理您需要的任何逻辑,以处理应如何显示所有提供的形状的总面积。SumCalculatorOutputter类的工作方式如下:$shapes=array(newCircle(2),newSquare(5),newSquare(6));$areas=newAreaCalculator($shapes);$output=newSumCalculatorOutputter($areas);echo$output->JSON();echo$output->HAML();echo$output->HTML();echo$output->JADE();现在,SumCalculatorOutputter类现在可以处理将数据输出到用户需要的任何逻辑。开闭原则对象和实体应该对扩展开放,对修改关闭。它只是意味着一个类应该易于扩展而无需修改类本身。让我们看一下AreaCalculator类,尤其是sum方法。publicfunctionsum(){foreach($this->shapesas$shape){if(is\_a($shape,'Square')){$area\[\]=pow($shape->length,2);}elseif(is\_a($shape,'Circle')){$area\[\]=pi()\*pow($shape->radius,2);}}returnarray\_sum($area);}如果我们希望sum方法能够对更多形状区域求和,我们将不得不添加更多if/else块,这违反了开闭原则。我们可以使这个sum方法更好的一种方法是从sum方法中删除计算每个形状面积的逻辑,并将其附加到形状的类中。广场类{公共$length;公共函数\_\_construct($length){$this->length=$length;}publicfunctionarea(){returnpow($this->length,2);}}对于Circle类应该做同样的事情并且应该添加一个area方法。现在,计算所提供的任何形状的总和应该很简单:publicfunctionsum(){foreach($this->shapesas$shape){$area\[\]=$shape->area();}returnarray\_sum($area);}现在我们可以创建另一个形状类并在计算总和时传递它而不破坏我们的代码。但是现在另一个问题出现了,我们如何知道传入AreaCalculator的对象实际上是一个形状,或者这个形状是否有一个名为area的方法?编码接口是S.O.L.I.D的一个组成部分,一个简单的例子是我们创建一个每个形状都可以实现的接口:interfaceShapeInterface{publicfunctionarea();}classCircleimplementsShapeInterface{public$radius;公共函数\_\_construct($radius){$this->radius=$radius;}publicfunctionarea(){returnpi()\*pow($this->radius,2);在我们的AreaCalculatorsum方法中,我们可以检查提供的形状是否实际上是ShapeInterface的实例,否则我们抛出异常:publicfunctionsum(){foreach($this->shapesas$shape){if(is\_a($shape,'ShapeInterface')){$area\[\]=$shape->area();继续;抛出新的AreaCalculatorInvalidShapeException;}returnarray\_sum($area);}里氏替换原则如果对于每个T1类型的对象o1,都有T2类型的对象o2,那么当所有对象o1被o2替换时,T1定义的所有程序P都不会改变,则类型T2是类型T1的子类型。所有这一切都是说每个子类/派生类都可以覆盖其基类/父类。仍然使用OutAreaCalculator类,例如我们有一个扩展AreaCalculator类的VolumeCalculator类:}publicfunctionsum(){//计算体积然后返回和输出数组的逻辑returnarray($summedData);}}在SumCalculatorOutputter类中:classSumCalculatorOutputter{protected$calculator;公共函数\_\_constructor(AreaCalculator$calculator){$this->calculator=$calculator;}publicfunctionJSON(){$data=array('sum'=>$this->calculator->sum(););返回json\_encode($data);}publicfunctionHTML(){returnimplode('',array('','所提供形状的面积之和:',$this->calculator->sum(),''));}}如果我们尝试运行这样的示例:$areas=newAreaCalculator($shapes);$volumes=newAreaCalculator($solidShapes);$output=newSumCalculatorOutputter($areas);$output2=newSumCalculatorOutputter($volumes);程序运行良好,但是当我们在$output2对象上调用HTML方法时,您会收到E_NOTICE错误,通知我们数组已转换为字符串。要解决这个问题,而不是从VolumeCalculator类的sum方法返回数组,您应该简单地:publicfunctionsum(){//计算体积然后返回的逻辑和输出数组return$summedData;}求和数据是浮点数、双精度数或整数。接口隔离原则客户端不应依赖强制未使用的接口或未使用的方法。仍然使用形状示例,我们知道我们也有实心形状,所以由于我们还想计算形状的体积,我们可以向ShapeInterface添加另一个契约:publicfunctionvolume();}我们创建的任何形状都必须实现volume方法,但是我们知道正方形是平面形状,它们没有体积,所以这个接口会强制Square类实现一个没有用到的方法。ISP原理不允许这样,所以我们应该再创建一个带有volume方法的SolidShapeInterface接口来代替这个方法,这样类似立方体的实体就可以实现这个接口了:interfaceShapeInterface{publicfunctionarea();}interfaceSolidShapeInterface{publicfunctionvolume();}classCuboidimplementsShapeInterface,SolidShapeInterface{publicfunctionarea(){//计算长方体的表面积}publicfunctionvolume(){//计算长方体的体积}}this是一种更好的方法,但需要注意的是在对这些接口进行类型提示时要小心,而不是使用ShapeInterface或SolidShapeInterface。你可以创建另一个接口,也许是ManageShapeInterface,并在平面和实体形状上实现它,这样你就可以很容易地看到它有一个单一的API来管理形状。例如:interfaceManageShapeInterface{publicfunctioncalculate();}classSquareimplementsShapeInterface,ManageShapeInterface{publicfunctionarea(){/Dostuffhere/}publicfunctioncalculate(){return$this->area();}}classCuboidimplementsShapeInterface,SolidShapeInterface,ManageShapeInterface{publicfunctionarea(){/Dostuffhere/}publicfunctionvolume(){/Dostuffhere/}publicfunctioncalculate(){return$this->area()+}$this->音量();现在在AreaCalculator类中,我们可以轻松地用计算替换对area方法的调用,并检查对象是否是ManageShapeInterface的实例而不是ShapeInterface。依赖倒置原则最后但同样重要的是:实体必须依赖于抽象而非具体。它指出高层模块不能依赖于低层模块,而应该依赖于抽象。这听起来可能让人不知所措,但它真的很容易理解。这个原则允许解耦,这个例子似乎是解释它的最佳方式:classPasswordReminder{private$dbConnection;公共函数\_\_construct(MySQLConnection$dbConnection){$this->dbConnection=$dbConnection;}}首先,MySQLConnection是一个低级模块,而PasswordReminder是一个高级模块,但是根据S.O.L.I.D.中D的解释:Dependsonabstractionandnotonimplementation。上面的代码段违反了这个原则,因为PasswordReminder类被强制依赖于MySQLConnection类。以后如果要换数据库引擎,还要修改PasswordReminder类,违反开闭原则。PasswordReminder类不应该关心应用程序使用什么数据库,为了解决这个问题我们再次“编写接口”,因为高层和低层模块应该依赖于抽象,我们可以创建一个接口:interfaceDBConnectionInterface{publicfunctionconnect();}这个接口有一个connect方法,MySQLConnection类实现了这个接口,而不是直接在PasswordReminder的构造函数中提示MySQLConnection类,而是提示接口,不管你的应用使用哪个数据库类型,PasswordReminder类可以轻松连接到数据库,不会出现任何问题,也不会违反OCP。类MySQLConnection实现DBConnectionInterface{publicfunctionconnect(){返回“数据库连接”;}}classPasswordReminder{private$dbConnection;公共函数\_\_construct(DBConnectionInterface$dbConnection){$this->dbConnection=$dbConnection;}}根据上面的小片段,你现在可以看到高层模块和低层模块都依赖于抽象。结论老实说,乍一看S.O.L.I.D似乎微不足道,但通过不断使用和遵守它的准则,它会成为你和你的代码的一部分,可以轻松地扩展、修改、测试和重构,没有任何问题。更多学习内容,请访问码农到架构师的培养之路
