S.O.L.I.D: PHP 面向对象设计的五个基准原则
S.O.L.I.D 是首个 5 个面向对象设计 (OOD) 准则的首字母缩写,这些准则是由 Robert C. Martin 提出的,他更为人所熟知的名字是 Uncle Bob。
这些准则使得开发出易扩展、可维护的软件变得更容易。也使得代码更精简、易于重构。同样也是敏捷开发和自适应软件开发的一部分。
S.O.L.I.D 意思是:
扩展出来的首字母缩略词看起来可能很复杂,实际上它们很容易理解。
- S - 单一功能原则
- O - 开闭原则
- L - 里氏替换原则
- I - 接口隔离原则
- D - 依赖反转原则
接下来让我们看看每个原则,来了解为什么 S.O.L.I.D 可以帮助我们成为更好的开发人员。
单一职责原则
缩写是 S.R.P ,该原则内容是:
一个类有且只能有一个因素使其改变,意思是一个类只应该有单一职责。
例如,假设我们有一些图形,并且想要计算这些图形的总面积。是的,这很简单对不对?
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 使用方法,我们只需简单的实例化这个类,并且传递一个图形数组,在页面底部展示输出内容。
$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,然而这违背了开闭原则。
让这个 sum 方法变得更好的方式是将计算每个形状面积的代码逻辑移出 sum 方法,将其放进各个形状类中:
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);
}
}
在我们的 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);
}
里氏替换原则
如果对每一个类型为 T1 的对象 o1,都有类型为 T2 的对象 o2,使得以 T1 定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。
这句定义的意思是说:每个子类或者衍生类可以毫无问题地替代基类 / 父类。
依然使用 AreaCalculator 类,假设我们有一个 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 VolumeCalculator($solidShapes);
$output = new SumCalculatorOutputter($areas);
$output2 = new SumCalculatorOutputter($volumes);
程序不会出问题, 但当我们使用 $output2 对象调用 HTML 方法时 ,我们接收到一个 E_NOTICE 错误,提示我们数组被当做字符串使用的错误。
为了修复这个问题,只需:
public function sum() {
// logic to calculate the volumes and then return and array of output
return $summedData;
}
而不是让 VolumeCalculator 类的 sum 方法返回数组。
$summedData 是一个浮点数、双精度浮点数或者整型。
接口隔离原则
使用方(client)不应该依赖强制实现不使用的接口,或不应该依赖不使用的方法。
继续使用上面的 shapes 例子,已知拥有一个实心块,如果我们需要计算形状的体积,我们可以在 ShapeInterface 中添加一个方法:
interface ShapeInterface {
public function area();
public function volume();
}
任何形状创建的时候必须实现 volume 方法,但是「平面」是没有体积的,实现这个接口会强制的让「平面」类去实现一个自己用不到的方法。
ISP 原则不允许这么去做,所以我们应该创建另外一个拥有 volume 方法的 SolidShapeInterface 接口去代替这种方式,这样类似立方体的实心体就可以实现这个接口了:
interface ShapeInterface {
public function area();
}
interface SolidShapeInterface {
public function volume();
}
class Cuboid implements ShapeInterface, SolidShapeInterface {
public function area() {
//计算长方体的表面积
}
public function volume() {
// 计算长方体的体积
}
}
这是一个更好的方式,但是要注意提示类型时不要仅仅提示一个 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 principle)。
此 PasswordReminder 类不应该关注你的应用使用了什么数据库,为了进一步解决这个问题,我们「面向接口写代码」,由于高等级和低等级模块都应该依赖于抽象,我们可以创建一个接口:
interface DBConnectionInterface {
public function connect();
}
这个接口有一个连接数据库的方法,MySQLConnection 类实现该接口,在 PasswordReminder 的构造方法中不要直接将类型约束设置为 MySQLConnection 类,而是设置为接口类,这样无论你的应用使用什么类型的数据库,PasswordReminder 类都能毫无问题地连接数据库,且不违背 开闭原则。
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 一开始似乎很难掌握,但只要不断地使用和遵守其原则,它将成为你的一部分,使你的代码易被扩展、修改,测试,即使重构也不容易出现问题。
S.O.L.I.D: PHP 面向对象设计的五个基准原则的更多相关文章
- S.O.L.I.D 是面向对象设计(OOD)和面向对象编程(OOP)中的几个重要编码原则
注:以下图片均来自<如何向妻子解释OOD>译文链接:http://www.cnblogs.com/niyw/archive/2011/01/25/1940603.html < ...
- SOLID:面向对象设计的五个基本原则
在程序设计领域,SOLID 是由罗伯特·C·马丁在 21 世纪早期引入的记忆术首字母缩略字,指代了面向对象编程和面向对象设计的五个基本原则.当这些原则被一起应用时,它们使得一个程序员开发一个容易进行软 ...
- 面向对象设计之SRP(单一职责)原则
SRP设计原则面向对象类设计的第一个原则,最优先考虑的因素 一个类应该有且仅有一个职责.所谓一个类的职责是指引起该类变化的原因,如果一个类具有一个以上的职责,那么就会有多个不同的原因 引起该类变化,其 ...
- 面向对象设计(OOD)七大原则
这篇文章我会不停的维护它,它将会越来越长,但它是关于我在面向对象中的一些学习的思考心得.希望对自己对各位都能实用处. 开篇前,说明一下写这篇文章的原因.原因是由于设计模式.由于设计模式里的各种 ...
- 【转】面向对象设计的SOLID原则
S.O.L.I.D是面向对象设计和编程(OOD&OOP)中几个重要编码原则(Programming Priciple)的首字母缩写. SRP The Single Responsibility ...
- 面向对象设计的SOLID原则
S.O.L.I.D是面向对象设计和编程(OOD&OOP)中几个重要编码原则(Programming Priciple)的首字母缩写. SRP The Single Responsibility ...
- SOLID (面向对象设计) 基本原则
SOLID (面向对象设计) 基本原则 在 程序设计领域, SOLID (单一功能.开闭原则.里氏替换.接口隔离以及依赖反转)是由罗伯特•C•马丁在21世纪早期[1] 引入的记忆术首字母缩略 ...
- zz SOLID (面向对象设计)
SOLID (面向对象设计) 维基百科,自由的百科全书 跳到导航 跳到搜索 在程序设计领域, SOLID(单一功能.开闭原则.里氏替换.接口隔离以及依赖反转)是由罗伯特·C·马丁在21世纪早期[1] ...
- Java成长第五集--面向对象设计的五大原则
S.O.L.I.D 是面向对象设计(OOD)和面向对象编程(OOP)中的几个重要编码原则(Programming Priciple)的首字母缩写.以下图说明: 下面就个人的理解来说说这五大原则的含义到 ...
随机推荐
- js 日期 (10 + '').length == 10 ? '0' + 10 : 10;
js 日期 (10 + '').length == 10 ? '0' + 10 : 10;
- Access 执行查询时,抛出“标准表达式中数据类型不匹配”的错误
Access根据时间查询时应在查询条件前后加# SELECT COUNT(*) FROM [User] WHERE [CreateTime] > #{0}#
- 怎样查询锁表的SQL
通过以下的语句查询出锁表的SQL: select l.session_id sid, s.serial#, l.locked_mode, l.oracle_username ...
- mysql使用“.frm”文件恢复表结构
mysql创建每张表后都会在“mysql安装目录/data/数据库名/”目录下创建一个“表名.frm”文件. 该.frm文件并不能直接打开,但是它可以帮助你恢复你的表结构~~ 具体操作如下: 我现在准 ...
- 项目Beta冲刺(团队2/7)
项目Beta冲刺(团队2/7) 团队名称: 云打印 作业要求: 项目Beta冲刺(团队) 作业目标: 完成项目Beta版本 团队队员 队员学号 队员姓名 个人博客地址 备注 221600412 陈宇 ...
- 项目Beta冲刺(团队5/7)
项目Beta冲刺(团队5/7) 团队名称: 云打印 作业要求: 项目Beta冲刺(团队) 作业目标: 完成项目Beta版本 团队队员 队员学号 队员姓名 个人博客地址 备注 221600412 陈宇 ...
- subclassdlgitem
subclassdlgitem 该函数用来子类化一个控件. Subclass(子类化)是MFC中最常用的窗体技术之一.子类化完成两个工作:一是把窗体类对象attach到一个windows窗体实体中(即 ...
- gradle配置远程仓库(以及使用本地maven仓库)
allprojects{ repositories { mavenLocal() def REPOSITORY_URL = 'http://maven.aliyun.com/nexus/content ...
- zTree async 动态参数处理
async:{ enable: true,//开启异步机制 url: opts.url,//异步地址 otherParam: { 'plateNo': function(){ return $('# ...
- Oracle Exception
Oracle存储过程的异常处理 1.为了提高存储过程的健壮性,避免运行错误,当建立存储过程时应包含异常处理部分.2.异常(EXCEPTION)是一种PL/SQL标识符,包括预定义异常.非预定义异常和自 ...