SOLID:面向对象设计的前五项原则

S.O.L.I.D是Robert C. Martin提出的前五个面向对象设计(OOD)原则的首字母缩写,他更为人所熟知的名字是Uncle Bob。程序员

将这些原理结合在一块儿,可以使程序员轻松开发易于维护和扩展的软件。它们还使开发人员能够轻松避免代码异味,轻松重构代码,而且是敏捷或自适应软件开发的一部分。数据库

注意:这只是一篇简单的“欢迎使用_ S.O.L.I.D ”,它只是阐明了S.O.L.I.D是什么。json

更多学习内容能够访问从码农成为架构师的修炼之路数组

S.O.L.I.D表明:

首字母缩略词在扩展时可能看起来很复杂,可是却很容易掌握。架构

  • S - 单一责任原则
  • O - 开闭原理
  • L - Liskov替代原理
  • I - 接口隔离原理
  • D - 依赖倒置原则

让咱们分别看一下每一个原理,了解一下S.O.L.I.D为何能够帮助使咱们成为更好的开发人员。ide

单一责任原则

SRP的简称-此原则指出:函数

一个类有且只能有一个因素使其改变,意思是一个类只应该有单一职责.

例如,假设咱们有一些形状,咱们想对形状的全部区域求和。好吧,这很简单对吧?学习

class Circle {
    public $radius;

    public function construct($radius) {
        $this->radius = $radius;
    }
}

class Square {
    public $length;

    public function construct($length) {
        $this->length = $length;
    }
}

首先,咱们建立形状类,并让构造函数设置所需的参数。接下来,咱们继续建立AreaCalculator类,而后编写逻辑以总结全部提供的形状的面积。测试

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类,咱们只需实例化该类并传递形状数组,而后在页面底部显示输出。this

$shapes = array(
    new Circle(2),
    new Square(5),
    new Square(6)
);

$areas = new AreaCalculator($shapes);

echo $areas->output();

输出方法的问题在于AreaCalculator处理逻辑以输出数据。所以,若是用户但愿将数据输出为json或其余内容怎么办呢?

全部这些逻辑都将由AreaCalculator类处理,这是SRP所反对的。在AreaCalculator类应该只提供总结形状的区域,它不该该关心用户是否但愿JSON或HTML。

所以,要解决此问题,你能够建立一个SumCalculatorOutputter类,并使用它来处理处理全部提供的形状的总面积如何显示所需的任何逻辑。

该SumCalculatorOutputter类会的工做是这样的:

$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();

如今,SumCalculatorOutputter类如今能够处理将数据输出到用户所需的任何逻辑。

开闭原理

对象和实体应该对扩展开放,可是对修改关闭。

这只是意味着一个类应该易于扩展,而无需修改类自己。让咱们看一下AreaCalculator类,尤为是sum方法。

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);
}

若是咱们但愿sum方法可以对更多形状的区域求和,则必须添加更多if / else块,这违背了Open-closed原理。

咱们可使这种求和方法更好的一种方法是从求和方法中删除用于计算每一个形状的面积的逻辑,并将其附加到形状的类中。

class Square {
    public $length;

    public function \_\_construct($length) {
        $this->length = $length;
    }

    public function area() {
        return pow($this->length, 2);
    }
}

对Circle类应该作一样的事情,应该添加一个area方法。如今,要计算提供的任何形状的总和应该很简单:

public function sum() {
    foreach($this->shapes as $shape) {
        $area\[\] = $shape->area();
    }

    return array\_sum($area);
}

如今,咱们能够建立另外一个形状类,并在计算总和时传递它,而不会破坏咱们的代码。可是,如今又出现了另外一个问题,咱们如何知道传递到AreaCalculator中的对象其实是一个形状,或者该形状是否具备名为area的方法?

编码接口是S.O.L.I.D不可或缺的一部分,一个简单的示例是咱们建立一个接口,每种形状均可以实现:

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);
    }
}

在咱们的AreaCalculatorsum方法中,咱们能够检查所提供的形状是否其实是ShapeInterface的实例,不然咱们抛出异常:

public function sum() {
    foreach($this->shapes as $shape) {
        if(is\_a($shape, 'ShapeInterface')) {
            $area\[\] = $shape->area();
            continue;
        }

        throw new AreaCalculatorInvalidShapeException;
    }

    return array\_sum($area);
}

Liskov替代原则

若是对每个类型为 T1 的对象 o1,都有类型为 T2 的对象 o2,使得以 T1 定义的全部程序 P 在全部的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。

全部这一切都说明,每一个子类/派生类均可以替代其基类/父类。

仍然使用OutAreaCalculator类,例如咱们有一个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错误,通知咱们数组转换为字符串。

若要解决此问题,而不是从VolumeCalculator类的sum方法返回数组,你应该简单地:

public function sum() {
    // logic to calculate the volumes and then return and array of output
    return $summedData;
}

求和后的数据为浮点,双精度或整数。

接口隔离原理

使用方(client)不该该依赖强制实现不使用的接口,或不该该依赖不使用的方法。

仍然使用形状示例,咱们知道咱们也有实体形状,所以因为咱们还想计算形状的体积,所以能够向ShapeInterface添加另外一个协定:

interface ShapeInterface {
    public function area();
    public function volume();
}

咱们建立的任何形状都必须实现volume方法,可是咱们知道正方形是扁平形状而且它们没有体积,所以此接口将强制Square类实现一种不使用的方法。

ISP 原则不容许这么去作,因此咱们应该建立另一个拥有 volume 方法的 SolidShapeInterface 接口去代替这种方式,这样相似立方体的实心体就能够实现这个接口了:

interface ShapeInterface {
    public function area();
}

interface SolidShapeInterface {
    public function volume();
}

class Cuboid implements ShapeInterface, SolidShapeInterface {
    public function area() {
        // calculate the surface area of the cuboid
    }

    public function volume() {
        // calculate the volume of the cuboid
    }
}

这是一种更好的方法,但要注意的是在类型提示这些接口时要注意,而不是使用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原理。

该PasswordReminder类不该该关心什么数据库应用程序使用,以解决这个问题,咱们再次“代码的接口”,由于高层次和低层次的模块应该依赖于抽象,咱们能够建立一个界面:

interface DBConnectionInterface {
    public function connect();
}

该接口具备一个connect方法,而MySQLConnection类实现了此接口,并且也没有直接在PasswordReminder的构造函数中直接提示MySQLConnection类,而是改成提示该接口,不管您的应用程序使用哪一种数据库类型,PasswordReminder类能够轻松链接到数据库而不会出现任何问题,而且不会违反OCP。

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彷佛不多,可是经过不断使用和遵照其准则,它成为你和你的代码的一部分,能够轻松地对其进行扩展,修改,测试和重构,而不会出现任何问题。

更多学习内容请访问从码农成为架构师的修炼之路

相关文章
相关标签/搜索