当前位置: 首页 > 后端技术 > PHP

S.O.L.I.D:PHP面向对象设计的五个基准原则

时间:2023-03-29 20:05:28 PHP

S.O.L.I.D是RobertC.Martin提出的前5个面向对象设计(OOD)原则的首字母缩写词,他以他的名字It'sUncleBob更为人熟知。这些准则使开发可扩展和可维护的软件变得更加容易。它还使代码更加精简和易于重构。也是敏捷开发和自适应软件开发的一部分。注意:这不是一篇简单的“Welcometo_S.O.L.I.D”文章,这篇文章想澄清什么是S.O.L.I.D。S.O.L.I.D的意思是:扩展的首字母缩略词可能看起来很复杂,但实际上很容易理解。S-单一功能原则O-开闭原则L-里氏代换原则I-接口隔离原则D-依赖倒置原则让我们看看每个原则来理解为什么S.O.L.I.D可以帮助我们成为更好的开发人员。单一职责原则的缩写是S.R.P。该原则的内容是:一个类有且只有一个因素可以使它发生变化,也就是说一个类应该只有单一的职责。例如,假设我们有一些形状,想要计算这些形状的总面积。是的,这很容易,对吧?类圆{公共$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("","提供形状的面积总和:",$this->sum(),""));}}AreaCalculator方法,我们简单地实例化这个类,并传递一个图形数组来在页面底部显示输出。$shapes=array(newCircle(2),newSquare(5),newSquare(6));$areas=newAreaCalculator($shapes);回声$areas->output();的问题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方法中移到每个形状类中:classSquare{public$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的对象实际上是一个shape,或者shape对象中有area方法呢?界面编码是练习S.O.L.I.D的一部分。例如下面的例子,我们创建一个接口类,每个形状类都会实现这个接口类:interfaceShapeInterface{publicfunctionarea();}classCircleimplementsShapeInterface{public$radius;公共函数__construct($radius){$this->radius=$radius;}publicfunctionarea(){returnpi()*pow($this->radius,2);在我们的AreaCalculator的sum方法中,我们可以检查提供的形状类实例是否是ShapeInterface的实现,否则我们抛出异常:publicfunctionsum(){foreach($this->shapesas$shape){if(is_a($shape,'ShapeInterface')){$area[]=$shape->area();继续;抛出新的AreaCalculatorInvalidShapeException;}returnarray_sum($area);}里氏替换原则如果T1类型的每个对象o1的类型都是T2的对象o2,那么当T1定义的所有对象o1都被o2替换时,程序P的行为是不变,则类型T2是类型T1的子类型。这个定义意味着每个子类或派生类都可以毫无问题地替换基类/父类。仍然使用AreaCalculator类,假设我们有一个VolumeCalculator类,它继承了AreaCalculator类:}publicfunctionsum(){//计算体积然后返回和输出数组的逻辑returnarray($summedData);}}SumCalculatorOutputter类:类SumCalculatorOutputter{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错误,告诉我们该数组被用作字符串错误。要解决此问题,只需:publicfunctionsum(){//计算体积然后返回的逻辑和输出数组return$summedData;}而不是让VolumeCalculator类的sum方法返回一个数组。$summedData是浮点数、双精度数或整数。接口隔离原则客户端不应依赖于未使用的接口或未使用的方法。继续使用上面的形状示例,已知我们有一个实心块。如果我们需要计算一个形状的体积,我们可以在ShapeInterface中添加一个方法:interfaceShapeInterface{publicfunctionarea();publicfunctionvolume();}createanyshape必须实现volume方法,但是[Plane]没有volume。实现这个接口会强制[Plane]类实现一个不能单独使用的方法。ISP原理不允许这样,所以我们应该再创建一个带有volume方法的SolidShapeInterface接口来代替这个方法,这样类似立方体的实体就可以实现这个接口了:interfaceShapeInterface{publicfunctionarea();}interfaceSolidShapeInterface{publicfunctionvolume();}classCuboidimplementsShapeInterface,SolidShapeInterface{publicfunctionarea(){//计算长方体的表面积}publicfunctionvolume(){//计算长方体的体积}}This是更好的方法,但是注意在提示类型的时候不要只提示一个ShapeInterface或者SolidShapeInterface。你可以创建其他的接口,比如ManageShapeInterface,在plane类和cube类上实现,这样你很容易看出有管理形状的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的解释:它依赖于抽象而不依赖于实现。上面的代码段违反了这个原则,因为PasswordReminder类被迫依赖于MySQLConnection类。以后要修改数据库驱动,还要修改PasswordReminder类,这样就违反了Open-close原则。这个PasswordReminder类不应该关心您的应用程序使用什么数据库。为了进一步解决这个问题,我们“为接口写代码”。由于高层和低层模块都应该依赖于抽象,我们可以创建一个接口:interfaceDBConnectionInterface{publicfunctionconnect();}这个接口有一个连接数据库的方法。MySQLConnection类实现了这个接口。在PasswordReminder的构造方法中,不要直接给MySQLConnection类设置类型约束,而是给接口类设置,这样无论你的应用使用什么类型的数据库,PasswordReminder类都可以连接数据库,无需任何问题,并且不违反开闭原则。类MySQLConnection实现DBConnectionInterface{publicfunctionconnect(){返回“数据库连接”;}}类ssPasswordReminder{private$dbConnection;公共函数__construct(DBConnectionInterface$dbConnection){$this->dbConnection=$dbConnection;从上面的代码片段中,您现在可以看到高层和低层模块都依赖于抽象总结老实说,S.O.L.I.D乍一看似乎很难掌握,但通过不断使用和坚持其原则,它将成为你的一部分,使你的代码易于扩展、修改、测试,甚至重构时更不容易出现问题。文章转自:https://learnku.com/php/t/28922更多文章:https://learnku.com/php/c/tra...