面向对象设计之五大原则

时间:2022-02-12 17:25:55

本文目录

  1. 五大原则总览
  2. 单一职责原则
  3. 接口隔离原则
  4. 开放-封闭原则
  5. 里氏替换原则
  6. 依赖倒置原则
  7. 本文小结
  8. 参考文档


在按照我的理解方式审查了软件开发的周期后,我得出一个结论:实际上满足工程设计标准的唯一软件文档,就是源代码清单。
-- Jack Reeves (他发布了「什么是软件设计?」的开创性论文)


1、 五大原则总览

面向对象是一种高度抽象的思维,所有的软件设计几乎都是围绕着类(模块)来展开的,面向对象的设计基本就是在探讨类与类之间的关系。
面向对象设计的五大原则是:单一职责原则、接口隔离原则、开放-封闭原则、替换原则、依赖倒置原则。它们是软件中的常用的设计模式的基础。
为什么需要这些原则呢?因为代码容易腐化(借用参考书2中的说法),不断的需求变化和新增功能,导致软件中的糟糕代码不断累积,导致腐化,变得难以维护和拓展,这些设计原则能有效防止软件腐化。

因为这些原则听起来比较抽象,我借用通篇的例子来讲一下我对这些原则的理解。
本文以 发布博客 这一行为过程,设计符合上述五个原则的例子(借鉴参考文档1的例子)。这个例子按照五个原则拆成5个小例子。因为几个小例子之间有可能有多于一种原则的情况,我就只着重讲要讲的内容,毕竟五个原则其实某种程度上也是有点交叉的。

例子所用的语言是PHP,我们就不谈PHP面向对象中的一些缺陷了,但是设计原则是一种指导,无碍实现。

先发表一下我的设计感受:期待变化


2、 单一职责原则

单一设计原则:Single Responsibility Principle,即SRP。这个原则讲的是减少类之间的耦合、提高类的复用性,正所谓职责单一。

分析一下需求(看,需求来了),发布博客这一行为,涉及到的东西有哪些,罗列一下:
1.要发表的博客(博客实体)
2.博客操作(发布、删除、浏览等,虽然需求只是发布,但是要期待变化呀)
3.博客处理逻辑(发布的时候要进行的逻辑处理,或其他操作处理)
4.面向用户的接口(耳熟能详的控制器)

思考一下上面的区分是否职责够单一,好了,写一下上面的设计的代码流程,如下:

$blog = new Blog(); //博文实体
$blog->post_time = time();
$blog->author = '匿名者';
$blog->title = '面向对象示例';
$blog->content = '如你所见,这是示例,谢谢赏读,不吝指正';

$model = new FileBlogModel; //基本操作模型
$model->setConfig('E:\test');

$proc = new BlogProcModel; //逻辑处理模型
$controller = new BlogController; //创建一个控制器,面向用户,应用开发者

//发布博文
$controller->release($proc, $model, $blog); //依赖注入

//浏览博文
$controller->view($proc, $model, $blog);

//删除博文
$controller->delete($proc, $model, $blog);

接着,我们来逐一实现上面的具体代码。


3、 接口隔离原则

接口隔离原则:Interface Segregation Principle,即ISP。接口做到专一,不依赖不需要的接口方法,做到“隔离”。

博客操作模型中,设计它的接口时,一个接口就只有一个操作,做到专一。比如说,你不应该在 write 接口的里边有 close 操作,虽然大多数时候是写完即关,但是你怎么去实现那些要write多次的情况呢?重新写个接口?所以要做到隔离,不加入多余的操作。这就是你在期待变化的时候做出了衡量。

/**
* 博文操作模型抽象
*/
abstract class BlogModel
{
public function open()
{
return true;
}
public function close()
{
return true;
}
abstract function read($author, $title);
abstract function write($content, $post_time, $author, $title);
abstract function delete($author, $title);
}


4、 开放-封闭原则

开放-封闭原则:Open-Close Principle,即OCP。一个模块,扩展性上是开放的,更改性上是封闭的。

刚刚写了个博文操作的抽象类,为什么要写成抽象的呢?因为要支持扩展性和封闭性呀,利用对象的继承性和多态性进行扩展,继承的类在固定的抽象上的修改是封闭的,扩展性就是在期待变化呀。

比如我们可以写个用文件方式操作博文的类 FileBlogModel,它继承BlogModel。如下:
可是适当增加一些固有的操作,修改也时封闭的。

/**
* 文件博文存储实现类
*/
class FileBlogModel extends BlogModel
{
private $rootpath;

public function setConfig($rootpath)
{
$this->rootpath = $rootpath;
}
public function open()
{
if (!file_exists($this->rootpath) || !is_dir($this->rootpath)) {
exit("rootpath: {$this->rootpath} is not exist!");//应该抛出一个异常
}
return true;
}
public function read($author, $title)
{
$data = file_get_contents($this->getFilePath($author, $title));
return json_decode($data, true);
}
public function write($content, $post_time, $author, $title)
{
$data = json_encode([
'content' => $content,
'post_time' => $post_time,
'title' => $title,
'author' => $author
], JSON_UNESCAPED_UNICODE);
return file_put_contents($this->getFilePath($author, $title), $data);
}
public function delete($author, $title)
{
return unlink($this->getFilePath($author, $title));
}
private function getFilePath($author, $title)
{
$is_win = strpos(PHP_OS, 'WIN') === false;
$dirpath = $this->rootpath . DIRECTORY_SEPARATOR . $author;
$path = $is_win ? $dirpath : iconv("UTF-8","gb2312", $dirpath);
!file_exists($path) && mkdir($path);
$filepath = $dirpath . DIRECTORY_SEPARATOR . $title;
return $is_win ? $filepath : iconv("UTF-8","gb2312", $filepath);
}
}



又比如我们可以扩展写个 Mysql 数据库操作博文的类MysqlBlogModel,也是继承BlogModel,如下:

/**
* mysql数据库博文存储实现类
*/
class MysqlBlogModel extends BlogModel
{
private $link; //mysql资源句柄
private $config = [];

public function setConfig($host, $user, $password, $database)
{
$this->config = [
'host' => $host,
'user' => $user,
'pass' => $password,
'db' => $database,
];
}
public function open()
{
$c = &$this->config;
$this->link = mysqli_connect($c['host'], $c['user'], $c['pass'], $c['db']);
return empty($this->link);
}
public function close()
{
return mysqli_close($this->link);
}
public function read($author, $title)
{
$sql = "SELECT title,content,post_time,author FROM blog"
. " WHERE author=$author AND title=$title";
$result = mysqli_query($this->link, $sql);
$row = mysqli_fetch_assoc($result);
mysqli_free_result($result);
return $row;
}
public function write($content, $post_time, $author, $title)
{
$sql = "REPLACE INTO blog(title,content,post_time,author)"
. " VALUES($title,$content,$post_time,$author)"
. " FROM WHERE author=$author AND title=$title";
return mysqli_query($this->link, $sql);
}
public function delete($author, $title)
{
$sql = "DELETT FROM blog WHERE author=$author AND title=$title";
return mysqli_query($this->link, $sql);
}
}


5、 里氏替换原则

里氏替换原则:Liskov Substitution Principle,即LSP。里氏指Liskov女士。该原则指出:子类必须能替换它们的父类,并出现在父类能够出现的任何地方。

下面写的 BlogProcModel 逻辑处理类,就能体现这一点,如 save(BlogModel $model, Blog $blog) 方法,入参 BlogModel 是个抽象的父类,不管你后面传入的是 FileBlogModel ,还是 MysqlBlogModel ,它们的 open(), write(), close() 一样使用,并且能根据你传入什么子类,就执行子类相应的方法,这就是类多态的体现了。

/**
* 博文处理模型-逻辑处理
*/
class BlogProcModel
{
public function save(BlogModel $model, Blog $blog)
{
$content = $blog->content;
$content = $this->escape($content); /* 转义非法字符 */
//$content = $this->filter();/* 过滤非法字符 */
//$content = $this->checkSensitiveWord($content);/* 检查敏感词 */
//$content = $this->checkCopy($content);/* 博文抄袭检测 */
$model->open();
$return = $model->write($content, $blog->post_time, $blog->author, $blog->title);
$model->close();
return $return;
}
private function escape($content)
{
return addslashes($content);
}
public function read(BlogModel $model, Blog $blog)
{
$model->open();
$data = $model->read($blog->author, $blog->title);
$model->close();
return $data;
}
public function delete(BlogModel $model, Blog $blog)
{
$model->open();
$return = $model->delete($blog->author, $blog->title);
$model->close();
return $return;
}
}


6、 依赖倒置原则

依赖倒置原则:Dependence Inversion Principle,即DIP。就是依赖关系倒置为依赖接口,这里的接口是对问题的抽象。即抽象不能依赖于具体,具体应该依赖于抽象。依赖抽象是实现代码扩展和运行期内多态的基础。

比如下面的控制器 BlogController的方法 release(BlogProcModel $proc, BlogModel $model, Blog $blog),它依赖于入口参数 BlogProcModel 、BlogModel。很多人想,为什么要写成参数形式?不在方法内写死?其实用上 写死 这个字眼你也就知道弊端了。这个方法依赖于不写死的入参,而且是抽象!!(BlogProcModel你可以更进一步抽象。)

我们只需在外边具体化抽象接口或类,也可运行时传入具体化参数。 这里到处都期待着变化

/**
* 博客控制器
*/
class BlogController
{
public function release(BlogProcModel $proc, BlogModel $model, Blog $blog)
{
$return = $proc->save($model, $blog) ? 'ok' : 'false';
echo "发表 $return!<br/>"; //view
}
public function view(BlogProcModel $proc, BlogModel $model, Blog $blog)
{
$data = $proc->read($model, $blog);
echo "<br/>标题: {$data['title']}<br/>"
. "作者: {$data['author']}<br/>"
. "发表时间: ".date('Y年m月d日', $data['post_time'])."<br/>"
. "正文内容: {$data['content']}<br/><br/>"; //view
}
public function delete(BlogProcModel $proc, BlogModel $model, Blog $blog)
{
$return = $proc->delete($model, $blog) ? 'ok' : 'false';
echo "删除 $return !<br/>"; //view
}
}


小结

把整个例子串起来看,会发现许多原则分布各处,如接口隔离原则的小例子用单子职责原则也能说得通,依赖倒置的小例子用里氏替换原则也说得通,等等。
因为客户的需求是不稳定的,也是软件实现中最不稳定的因素,所以我们要设计与之相容的代码,设计中要本着期待变化的心情去设计。
实际编码的时候,其实框架如 Thinkphp5.0 就已经按设计原则进行比较好的设计,我们只是在一个规范的框架内开发而言,可以感受到需求变更时,框架带来的便利和可维护性。那为什么还要懂得设计原则呢?这是一种内功修养吧。万一后面你不得不造*呢?



主要参考文档

1、《PHP核心技术与最佳实践》 第2章 面向对象的设计原则
2、《敏捷软件开发:原则、模式与实践》 第二部分 敏捷设计



-end-

版权声明:*转载-非商用-非衍生-保持署名(创意共享3.0许可证)
发表日期:2017年5月30日