【注】本文节译自:https://www.digitalocean.com/community/conceptual_articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-design#toc-single-responsibility-principle
S.O.L.I.D 是 Robert C. Martin(俗称 Bob 叔叔)的前五个面向对象设计(OOD)**原则的缩写。
将这些原则结合在一起,可使程序员轻松地开发易于维护和扩展的软件。它们还可以使开发人员轻松避免代码异味,很容易重构代码,并且还是敏捷或自适应软件开发的一部分。
S.O.L.I.D 代表:让我们逐一查看每个原则,以了解为什么 S.O.L.I.D 可以帮助我们成为更好的开发人员。
单一责任原则简称 S.R.P --- 此原则指出:
一个类应该有且只有一个改变理由,这意味着类应该只有一份工作。
例如,假设我们有一些形状,我们想对形状的所有面积求和。 好吧,这很简单吧?
classCircle{
public$radius;
publicfunctionconstruct($radius){
$this->radius=$radius;
}
}
classSquare{
public$length;
publicfunctionconstruct($length){
$this->length=$length;
}
}
首先,我们创建形状类,并让构造函数设置所需的参数。 接下来,我们继续创建 AreaCalculator 类,然后编写逻辑来对所提供的所有形状的面积求和。
classAreaCalculator{
protected$shapes;
publicfunction__construct($shapes=array()){
$this->shapes=$shapes;
}
publicfunctionsum(){
//logictosumtheareas
}
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 块,这违背了Open-closed原理。
我们可以使这种 sum 方法更好的一种方法是从 sum 方法中删除用于计算每个形状的面积的逻辑,并将其附加到形状的类中。
classSquare{
public$length;
publicfunction__construct($length){
$this->length=$length;
}
publicfunctionarea(){
returnpow($this->length,2);
}
}
对 Circle 类应该做同样的事情,应该添加一个 area 方法。 现在,要计算提供的任何形状的总和应该很简单:
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);
}
现在,我们可以创建另一个形状(shap)类,并在计算总和时传入它,而不会破坏我们的代码。但是,现在又出现了另一个问题,我们如何知道传递到 AreaCalculator 中的对象实际上是一个形状,或者该形状是否具有名为 area 的方法?
基于接口的编程是 S.O.L.I.D 不可或缺的一部分,一个简单的例子是我们创建一个接口,每种形状都可以实现:
interfaceShapeInterface{
publicfunctionarea();
}
classCircleimplementsShapeInterface{
public$radius;
publicfunction__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();
continue;
}
thrownewAreaCalculatorInvalidShapeException;
}
returnarray_sum($area);
}
里斯科夫替代原则
设 q(x) 是类型 T 的对象 x 可证明的属性,那么 q(y) 应该是类型 S 的对象 y 的可证明属性,其中 S 是 T 的子类型。
所有这一切都说明,每个子类/派生类都可以替代其基类/父类。
仍然利用 AreaCalculator 类,我们有一个 VolumeCalculator 类扩展了 AreaCalculator 类:
classVolumeCalculatorextendsAreaCalulator{
publicfunctionconstruct($shapes=array()){
parent::construct($shapes);
}
publicfunctionsum(){
//logictocalculatethevolumesandthenreturnandarrayofoutput
returnarray($summedData);
}
}
在 SumCalculatorOutputter 类中:
classSumCalculatorOutputter{
protected$calculator;
publicfunction__constructor(AreaCalculator$calculator){
$this->calculator=$calculator;
}
publicfunctionJSON(){
$data=array(
'sum'=>$this->calculator->sum();
);
returnjson_encode($data);
}
publicfunctionHTML(){
returnimplode('',array(
'',
'Sumoftheareasofprovidedshapes:',
$this->calculator->sum(),
''
));
}
}
如果我们尝试运行这样的示例:
$areas = new AreaCalculator($shapes);
$volumes = new VolumeCalculator($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;
}
求和后的数据为浮点,双精度或整数。
接口隔离原则绝不应该强迫客户端实现不使用的接口,也不应该强迫客户端依赖不使用的方法。
仍然使用形状示例,我们知道我们也有实体形状,因此由于我们还想计算形状的体积,因此可以向 ShapeInterface 添加另一个契约:
interfaceShapeInterface{
publicfunctionarea();
publicfunctionvolume();
}
我们创建的任何形状都必须实现 volume 方法,但是我们知道正方形是扁平形状并且它们没有体积,因此此接口将强制 Square 类实现一种不使用的方法。
ISP对此表示拒绝,相反,您可以创建另一个名为 SolidShapeInterface 的接口,该接口具有 volume 契约,那么诸如立方体这样在实体形状可以实现此接口:
interfaceShapeInterface{
publicfunctionarea();
}
interfaceSolidShapeInterface{
publicfunctionvolume();
}
classCuboidimplementsShapeInterface,SolidShapeInterface{
publicfunctionarea(){
//calculatethesurfaceareaofthecuboid
}
publicfunctionvolume(){
//calculatethevolumeofthecuboid
}
}
这是一种更好的方法,但是要注意在使用 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->volume();
}
}
现在在 AreaCalculator 类中,我们可以轻松地用 calculate 替换对 area 方法的调用,还可以检查对象是否是 ManageShapeInterface 的实例,而不是 ShapeInterface 的实例。
依赖倒置原则最后,但并非最不重要的一点是:
实体必须依赖抽象而不依赖具体。它指出高级模块一定不能依赖于低级模块,而应该依赖于抽象。
这听起来可能很夸张,但确实很容易理解。该原理允许去耦,这个例子似乎是解释该原理的最佳方法:
classPasswordReminder{
private$dbConnection;
publicfunction__construct(MySQLConnection$dbConnection){
$this->dbConnection=$dbConnection;
}
}
首先,MySQLConnection 是低级别的模块,而PasswordReminder是高级别的模块,但是根据 S.O.L.I.D 中 D 的定义。声明依赖于抽象而不依赖于具体内容,上面的此代码段违反了该原理,因为 PasswordReminder 类被强制依赖于 MySQLConnection 类。
以后,如果您要更改数据库引擎,则还必须编辑 PasswordReminder 类,从而违反了打-闭原则。
PasswordReminder 类不必关心你的应用程序使用什么数据库,为解决此问题,我们再次“为接口编码”,因为高级和低级模块应依赖抽象,因此我们可以创建接口:
interface DBConnectionInterface {
public function connect();
}
该接口具有一个 connect 方法,而 MySQLConnection 类实现了此接口,并且也没有直接在 PasswordReminder 的构造函数中直接提示 MySQLConnection 类,而是改为提示该接口,无论您的应用程序使用哪种数据库类型,PasswordReminder 类可以轻松连接到数据库而不会出现任何问题,并且不会违反 OCP。
classMySQLConnectionimplementsDBConnectionInterface{
publicfunctionconnect(){
return"Databaseconnection";
}
}
classPasswordReminder{
private$dbConnection;
publicfunction__construct(DBConnectionInterface$dbConnection){
$this->dbConnection=$dbConnection;
}
}
根据上面的小片段,您现在可以看到高级模块和低级模块都依赖于抽象。
结论S.O.L.I.D 乍看起来似乎有点太抽象了,但是随着 S.O.L.I.D 原则在现实世界的每一次应用中,遵守其准则的好处将变得更加明显。遵循 S.O.L.I.D. 原则的代码可以更轻松地与协作者共享、扩展、修改、测试和重构,而不会出现任何问题。
Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved