原文: 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->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原则看起来挺简单的,但坚持不懈地贯穿它的指导方针,它将会成为你代码的一部分,它将让你的代码更容易扩展,修改,测试,重构。