【PHP】类设计原则SOLID (object-oriented design)【译】

原文: S.O.L.I.D: The First 5 Principles of Object Oriented Design

译者:10xjzheng

1.单一职责 Single responsibility principle

  • A class should have one and only one reason to change, meaning that a class should have only one job.
    一个类应该只有单一职责。
  • For example, say we have some shapes and we wanted to sum all the areas of the shapes. Well this is pretty simple right? 举个例子,现在有一些图形,我们的目的是计算这些图形的面积之和,这很简单对吧?
    类设计代码:
class Circle {
    public $radius;

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

class Square {
    public $length;

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

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(),
            ""
        ));
    }
}

业务代码

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

$areas = new AreaCalculator($shapes);

echo $areas->output();
  • The problem with the output method is that the AreaCalculator handles the logic to output the data. Therefore, what if the user wanted to output the data as json or something else?
    问题在于类AreaCalculator处理面积输出的output方法,因为,当我们需要输出的结果是json或者其它格式时该如何调整呢?

  • All of that logic would be handled by the AreaCalculator class, this is what SRP frowns against; the AreaCalculator class should only sum the areas of provided shapes, it should not care whether the user wants json or HTML.
    现在是所有的逻辑(sum,output)都由类AreaCalculator处理,这违反了单一职责原则。类AreaCalculator应该只处理计算面积之和,而不应该关心输出结果是json或者html。

业务代码应该是这样,即增加一个类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(); 

2. 开闭原则 Open-closed Principle

  • Objects or entities should be open for extension, but closed for modification.
    对象或实体应该对扩展开放,对修改关闭。
  • This simply means that a class should be easily extendable without modifying the class itself. Let's take a look at the AreaCalculator class, especially it's sum method.这表明一个类应该尽可能的容易被扩展并且不需要修改类的代码,来看看上面说到的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);
}   
  • If we wanted the sum method to be able to sum the areas of more shapes, we would have to add more if/else blocks and that goes against the Open-closed principle. 如果我们需要计算包含其它形状的图形的面积,我们就必须添加更多if/else条件代码,这就违反了开闭原则。

  • A way we can make this sum method better is to remove the logic to calculate the area of each shape out of the sum method and attach it to the shape's class. 一个好的办法是我们将计算这一块计算面积的代码移出来,将它放到图形类里面。

修改后是这样的:

class Square {
    public $length;

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

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

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

    return array_sum($area);
}
  • Now we can create another shape class and pass it in when calculating the sum without breaking our code. However, now another problem arises, how do we know that the object passed into the AreaCalculator is actually a shape or if the shape has a method named area? 现在我们可以新建一个图形类并将计算面积之和而不需要改动代码了。然而,又有新的问题,我们如何保证被传进去的对象就是一个图形类并且有一个area方法呢?

  • Coding to an interface is an integral part of S.O.L.I.D, a quick example is we create an interface, that every shape implements: 面向接口编程是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);
    }
}  
  • In our AreaCalculator sum method we can check if the shapes provided are actually instances of the ShapeInterface, otherwise we throw an exception: 在我们的类AreaCalculator的sum方法我们检查对象是否是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);
}    

3. 里氏替换原则 Liskov substitution principle

  • Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.
    类S作为类T的子类,x、y分别是T、S的一个实例,让q(x) 是对象x的一个属性,q(y)是y对应的属性,则q(y)可以替代q(x)。

  • All this is stating is that every subclass/derived class should be substitutable for their base/parent class. 上述意思即所有的子类对父类应该具有可替代性。

  • Still making use of out AreaCalculator class, say we have a VolumeCalculator class that extends the AreaCalculator class: 还是以AreaCalculator为例,我们有另外个VolumeCalculator类继承了它:

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

In the SumCalculatorOutputter class:

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);
  • The program does not squawk, but when we call the HTML method on the output2 object we get an E_NOTICE error informing us of an array to string conversion. 程序本身不会报错,但是当我们执行output2->HTML()会报一个警告级别的错误,即数组到字符串的转换错误。

  • To fix this, instead of returning an array from the VolumeCalculator class sum method, you should simply:
    为避免错误,VolumeCalculator的sum必须返回和父类一样的数据格式,而不是数组:

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

4. 接口隔离原则 Interface segregation principle

  • A client should never be forced to implement an interface that it doesn't use or clients shouldn't be forced to depend on methods they do not use. 一个类不应该被强制实现它不需要用到的接口,或者说一个类不应该依赖它用不到的方法。

  • Still using our shapes example, we know that we also have solid shapes, so since we would also want to calculate the volume of the shape, we can add another contract to the ShapeInterface: 还是用上边的例子,当我们还有立方体的形状,因此我们应该还有计算体积的方法,故我们在ShapeInterface接口增加该属性。

interface ShapeInterface {
    public function area();
    public function volume();
}   
  • Any shape we create must implement the volume method, but we know that squares are flat shapes and that they do not have volumes, so this interface would force the Square class to implement a method that it has no use of. 所有图形类必须实现计算体积的方法,但我们知道平面没有体积, 该接口强制实现它的某些类必须实现他们用不到的方法。

  • ISP says no to this, instead you could create another interface called SolidShapeInterface that has the volume contract and solid shapes like cubes e.t.c can implement this interface: 接口隔离告诉我们,你可以创建另一个接口专门计算体积的,然后像立方体之类的可以实现该接口。

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
    }
}
  • This is a much better approach, but a pitfall to watch out for is when type-hinting these interfaces, instead of using a ShapeInterface or a SolidShapeInterface. 这看起来是个更好的方法,但必须意识到的一个陷阱是这样一来就没有了将实现ShapeInterface和SolidShapeInterface的类统一起来的接口。

  • You can create another interface, maybe ManageShapeInterface, and implement it on both the flat and solid shapes, this way you can easily see that it has a single API for managing the shapes. For example:
    为解决上面的问题,你可以创建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();
    }
}    
  • Now in AreaCalculator class, we can easily replace the call to the area method with calculate and also check if the object is an instance of the ManageShapeInterface and not the ShapeInterface.
    现在AreaCalculator类里,我们能发现area方法里面的object实例已经是ManageShapeInterface而不是ShapeInterface。

5. 依赖反转原则 Dependency Inversion principle

  • Entities must depend on abstractions not on concretions. It states that the high level module must not depend on the low level module, but they should depend on abstractions. 依赖反转原则(Dependency inversion principle,DIP)是指一种特定的解耦(传统的依赖关系创建在高层次上,而具体的策略设置则应用在低层次的模块上)形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。

  • This might sound bloated, but it is really easy to understand. This principle allows for decoupling, an example that seems like the best way to explain this principle:
    这听起来有点绕,但其实很好理解,这个原则有助于解耦,一个最好的例子如下:


class PasswordReminder {
    private $dbConnection;

    public function __construct(MySQLConnection $dbConnection) {
        $this->dbConnection = $dbConnection;
    }
}
  • First the MySQLConnection is the low level module while the PasswordReminder is high level, but according to the definition of D in S.O.L.I.D. which states that Depend on Abstraction not on concretions, this snippet above violates this principle as the PasswordReminder class is being forced to depend on the MySQLConnection class. 首先MySQLConnection是一个低层次的模型而PasswordReminder是高层次的模型,根据依赖反转原则,应该依赖抽象而不是具体,因此上面的代码违反了依赖反转原则。

  • Later if you were to change the database engine, you would also have to edit the PasswordReminder class and thus violates Open-close principle. 其次,假如你要修改数据库引擎,你又要改动类PasswordReminder的代码,又违反了开闭原则。

  • The PasswordReminder class should not care what database your application uses, to fix this again we "code to an interface", since high level and low level modules should depend on abstraction, we can create an interface: 类PasswordReminder应该不关注你的应用用到的具体的数据库类型,为了修正它我们还是面向接口编程,新建一个接口:

interface DBConnectionInterface {
    public function connect();
}    
  • The interface has a connect method and the MySQLConnection class implements this interface, also instead of directly type-hinting MySQLConnection class in the constructor of the PasswordReminder, we instead type-hint the interface and no matter the type of database your application uses, the PasswordReminder class can easily connect to the database without any problems and OCP is not violated. 接口有一个connect方法类MySQLConnection必须实现它,并且在PasswordReminder的构造方法参数应该改为接口类型,这样就没问题了。
class MySQLConnection implements DBConnectionInterface {
    public function connect() {
        return "Database connection";
    }
}

class PasswordReminder {
    private $dbConnection;

    public function __construct(DBConnectionInterface $dbConnection) {
        $this->dbConnection = $dbConnection;
    }
}  
  • According to the little snippet above, you can now see that both the high level and low level modules depend on abstraction.
    根据上面的描述,你现在应该能理解为什么说高层次模型和低层次模型都要依赖抽象而不是具体了。

Conclusion

Honestly, S.O.L.I.D might seem to be a handful at first, but with continuous usage and adherence to its guidelines, it becomes a part of you and your code which can easily be extended, modified, tested, and refactored without any problems.

实话说,solid原则看起来挺简单的,但坚持不懈地贯穿它的指导方针,它将会成为你代码的一部分,它将让你的代码更容易扩展,修改,测试,重构。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容