PHP中的SOLID原理

SOLID是一组面向对象的设计原则,旨在使代码更具可维护性和灵活性。它们是罗伯特·“鲍伯叔叔”马丁在2000年的论文《设计原理和设计模式》中创造的 。SOLID原理适用于任何面向对象的语言,但在本文中,我将重点介绍它们在PHP应用程序中的含义。

SOLID是首字母缩写,代表以下内容:

  • 单一责任原则

  • 打开/关闭原则

  • Liskov替换原则

  • 接口隔离原则

  • 依赖性倒置原则

我将依次解决它们。

单一责任原则

这说明一个类应该有一个单一的职责,但更重要的是,一个类应该只有一个改变的理由。

以一个名为Page的(简单)类为例。

class Page {
  protected $title;
 
  public getPage($title) {
    return $this->title;
  }
 
  public function formatJson() {
    return json_encode($this->getTitle());
  }
}

此类了解title属性,并允许通过get()方法检索此title属性。我们还可以在此类中使用一种称为的方法,formatJson()以JSON字符串的形式返回页面。这似乎是一个好主意,因为该类负责其自身的格式设置。

但是,如果我们想更改JSON字符串的输出或向该类添加另一种类型的输出,会发生什么情况?我们需要更改类以添加其他方法或更改现有方法以适合。对于这样简单的类来说,这很好,但是如果它包含更多的属性,则更改格式将更加复杂。

更好的方法是修改Page类,以便仅知道数据是句柄。然后,我们创建一个名为JsonPageFormatter的辅助类,该类用于将Page对象格式化为JSON。

class Page {
  protected $title;
 
  public getPage($title){
    return $this->title;
  }
}
 
class JsonPageFormatter {
    public function format(Page $page) {
        return json_encode($page->getTitle());
    }
}

这样做意味着,如果我们要创建XML格式,则只需添加一个名为XmlPageFormatter的类,并编写一些简单的代码即可输出XML。现在,我们只有一个理由来更改Page类。

开/关原则

在开放/封闭原则中,类应为扩展而开放,但应 为修改而封闭。本质上意味着应该扩展类以更改功能,而不是对其进行更改。

例如,采用以下两个类。 

class Rectangle {
  public $width;
  public $height;
}
 
class Board {
  public $rectangles = [];
  public function calculateArea() {
    $area = 0;
    foreach ($this->rectangles as $rectangle) {
      $area += $rectangle->width * $rectangle->height;
    }
  }
}

我们有一个Rectangle类,它包含一个矩形的数据,还有一个Board类,它用作Rectangle对象的集合。通过这种设置,我们可以通过遍历$rectangles集合中的项目并计算其面积来轻松地找到木板的面积。

这种设置的问题在于我们受到我们可以传递给Board类的对象类型的限制。例如,如果我们想将Circle对象传递给Board类,则需要编写条件语句和代码以检测和计算Board的面积。

解决此问题的正确方法是将面积计算代码移到形状类中,并让所有形状类都扩展一个Shape接口。现在,我们可以创建一个RectangleCircle形状类,当需要时将计算它们的面积。

interface Shape {
   public function area();
}
 
class Rectangle implements Shape {
  public function area() {
    return $this->width * $this->height;
  }
}
 
class Circle implements Shape {
  public function area() {
    return $this->radius * $this->radius * pi();
  }
}

现在,可以对 Board类进行重新设计,以使其不关心传递给它的形状类型是什么,只要它们实现了该area()方法即可。

class Board {
  public $shapes;
 
  public function calculateArea() {
    $area = 0;
    foreach ($this->shapes as $shape) {
      $area+= $shape->area();
    }
    return $area;
  }
}

现在,我们以某种方式设置这些对象,这意味着如果我们具有不同类型的对象,则无需更改Board类。我们只是创建实现Shape的对象,然后将其传递给集合,方法与其他类相同。

Liskov替换原则

它由Barbara Liskov于1987年创建,它指出对象应可以用其子类型替换,而不改变程序的工作方式。换句话说,派生类必须可以替代其基类,而不会引起错误。

以下代码定义了一个Rectangle类,可用于创建和计算矩形的面积。

class Rectangle {
  public function setWidth($w) { 
      $this->width = $w;
  }
 
  public function setHeight($h) {
      $this->height = $h;
  }
 
  public function getArea() {
      return $this->height * $this->width;
  }
}

使用它,我们可以将其扩展为Square类。因为正方形与矩形有些不同,所以我们需要重写一些代码以允许正方形正确存在。

class Square extends Rectangle {
  public function setWidth($w) {
    $this->width = $w;
    $this->height = $w;
  }
 
  public function setHeight($h) {
    $this->height = $h;
    $this->width = $h;
  }
}

看起来不错,但最终正方形不是矩形,因此我们添加了代码来强制这种情况发生。

我曾经读过的一个很好的类比是考虑以班级为代表的鸭子和橡皮鸭。尽管可以将Duck类扩展为Rubber Duck类,但我们需要重写许多Duck功能以适合Rubber Duck。例如,一只鸭子嘎嘎叫,但橡皮鸭却没有(好吧,也许有点吱吱声),一只鸭子还活着,但一只橡皮鸭却没有。

在类中重写大量代码以适合特定情况会导致维护问题。

解决矩形与正方形情况的一种解决方案是创建一个称为“四边形”的接口,并在单独的RectangleSquare 类中实现该接口。在这种情况下,我们允许类负责自己的数据,但是强制需要某些可用的方法资源。

interface Quadrilateral {
  public function setHeight($h);
 
  public function setWidth($w);
 
  public function getArea();
}
 
class Rectangle implements Quadrilateral;
 
class Square implements Quadrilateral;

最重要的是,如果您发现您要覆盖大量代码,则可能是您的体系结构是错误的,您应该看Liskov替换原理。

接口隔离原理

这说明许多特定于客户机的接口要比一个通用接口更好。换句话说,不应强迫类实现其不使用的接口。

让我们以一个Worker接口为例。这定义了几种可以应用于典型开发机构的工人的不同方法。

interface Worker {
 
  public function takeBreak()
 
  public function code()
 
  public function callToClient()
 
  public function attendMeetings()
 
  public function getPaid()
}

问题在于,由于此接口过于通用,因此我们不得不在实现该接口的类中创建方法以使其恰好适合该接口。

例如,如果我们创建一个Manager类,那么我们就必须实现一个code()方法,因为这是接口所需要的。由于管理人员通常不编写代码,因此我们实际上无法使用此方法执行任何操作,因此我们只返回false。

class Manager implements Worker {
  public function code() {
    return false;
  }
}

另外,如果我们有一个实现WorkerDeveloper类,那么我们将被迫实现一个方法,因为这是接口所需要的。callToClient()

class Developer implements Worker {
  public function callToClient() {
    echo "I'll ask my manager.";
  }
}

具有胖胖的界面意味着必须实现什么都不做的方法。

正确的解决方案是将我们的界面分成单独的部分,每个部分都处理特定的功能。在这里,我们从通用的Worker接口中分离了CoderClientFacer接口。

interface Worker {
  public function takeBreak()
  public function getPaid()
}
 
interface Coder {
  public function code()
}
 
interface ClientFacer {
  public function callToClient()
  public function attendMeetings()
}

有了这个,我们可以实现我们的子类,而不必编写我们不需要的代码。因此我们的DeveloperManager类看起来像这样。

class Developer implements Worker, Coder {
}
 
class Manager implements Worker, ClientFacer {
}

拥有许多特定的接口意味着我们不必只为了支持一个接口而编写代码。

依赖倒置原则

也许这是最简单的原理,它表明类应该依赖于抽象,而不是依赖于具体。本质上,不依赖于具体的类,而是依赖于接口。

以使用MySqlConnection类从数据库加载页面的PageLoader类为例,我们可以创建这些类,以便将连接类传递给PageLoader类的构造函数。

class MySqlConnection {
    public function connect() {}
}
 
class PageLoader {
    private $dbConnection;
    public function __construct(MySqlConnection $dbConnection) {
        $this->dbConnection = $dbConnection;
    }
}

这种结构意味着我们基本上在数据库层使用MySQL。如果我们想将其换成其他数据库适配器,会发生什么?我们可以扩展MySqlConnection类以创建与Memcache或其他内容的连接,但这会违反Liskov替换原理。可能会使用备用数据库管理器来加载页面,因此我们需要找到一种方法来执行此操作。

此处的解决方案是创建一个名为DbConnectionInterface的接口,然后在MySqlConnection类中实现此接口。然后,我们不依赖于传递给PageLoader类的MySqlConnection对象,而是依赖于实现DbConnectionInterface接口的任何类。

interface DbConnectionInterface {
    public function connect();
} 
 
class MySqlConnection implements DbConnectionInterface {
    public function connect() {}
}
 
class PageLoader {
    private $dbConnection;
    public function __construct(DbConnectionInterface $dbConnection) {
        $this->dbConnection = $dbConnection;
    }
}

有了这个,我们现在可以创建一个MemcacheConnection类,只要它实现了DbConnectionInterface,就可以在PageLoader类中使用它来加载页面。

这种方法还迫使我们以防止在不关心它的类中的特定实现细节的方式编写代码。因为我们已经将MySqlConnection类传递给了PageLoader 类,所以我们不应该再在PageLoader类中编写SQL查询。这意味着,当我们传递MemcacheConnection对象时,其行为将与任何其他类型的连接类相同。

在考虑接口而不是类时,它迫使我们将特定的域代码移出PageLoader类,并移入MySqlConnection类。

如何发现它?

一个更大的问题可能是,如果需要在代码中应用SOLID原理,或者正在编写非SOLID的代码,该如何发现。

了解这些原则只是其中的一半,您还需要知道何时应该退后一步并考虑应用SOLID原则。我想出了一个清单,您需要密切注意这些“诉说”,表明您的代码可能需要重新架构。

  • 您正在编写许多“ if”语句来处理目标代码中的不同情况。

  • 您正在编写很多代码,它们实际上并不能满足界面设计的要求。

  • 您继续打开相同的类来更改代码。

  • 您在与该类没有任何关系的类中编写代码。例如,将SQL查询放入数据库连接类之外的类中。

结论

SOLID并不是一种完美的方法,它会导致包含许多活动部件的复杂应用程序,有时会导致编写代码,以防万一。使用SOLID意味着编写更多的类并创建更多的接口,但是许多现代的IDE都可以解决该问题。

也就是说,这确实迫使您将关注点分开,考虑继承,防止重复代码,并谨慎地编写应用程序。毕竟,考虑对象如何在应用程序中组合在一起,到底是关于什么是面向对象的代码。