【简介】我们已经了解了 PHP 中面向对象的一些具体知识,本章将跳过技术细节,回过头来考量如何更好地使用 PHP 面向对象的问题。
6. 对象与设计
6.1 面向对象设计和过程式编程
我们以创建一个用于读写配置文件的工具为例子,分别用面向对象和面向过程式代码的方式来进行分析。为了重点关注代码的结构,示例中将忽略具体的功能实现。
- 配置文件为一个文本文件,文本文件中存储键值对:
key1:value1
key2:value2
key3:value3
key4:value4
按面向过程的方式来解决这个问题,需要两个函数:
function readParams($sourceFile) {
$params = array();
// 从$sourceFile中读取txt参数
return $params;
}
function writeParams($params, $sourceFile) {
// 写入txt参数到$sourceFile中
}
readParams() 函数打开文件,读取每一行内容并查找键值对,然后用键值对构建一个关联数组。writeParams() 函数两个参数分别为关联数组和文件路径,函数循环遍历关联数组,将每对键值对写入文件。
调用程序如下:
$file = './param.txt';
$arr['key1'] = 'val1';
$arr['key2'] = 'val2';
$arr['key3'] = 'val3';
writeParams($arr, $file);
$output = readParams($file);
print_r($output);
现在需要使这个工具扩展 XML 格式的文件:
<params>
<param>
<key></key>
<val></val>
</param>
</params>
在控制代码中,需要检查文件扩展名,如果参数文件以 .xml 结尾,就应该读取 XML 文件:
function readParams($sourceFile) {
$params = array();
if (preg_match('/\.xml$/i', $source)) {
// 从$sourceFile中读取XML参数
} else {
// 从$sourceFile中读取txt参数
}
return $params;
}
function writeParams($params, $sourceFile) {
if (preg_match('/\.xml$/i', $source)) {
// 写入XML参数到$sourceFile中
} else {
// 写入txt参数到$sourceFile中
}
}
这两个函数都使用 if-else 进行判断,导致了代码的重复,如果以后有更多格式的文件需要支持扩展,代码的重复就会更加严重。
对于同样的问题,我们使用类来处理:
abstract class ParamHandler {
protected $source;
protected $params = array();
function __construct($source) {
$this->source = $source;
}
function addParam($key, $val) {
$this->params[$keys] = $val;
}
function getAllParams() {
return $this->params;
}
static function getInstance($filename) {
if (preg_match('/\.xml$/i', $filename)) {
return new XmlParamHandler($filename);
} else {
return new Text ParamHandler($filename);
}
}
abstract function write();
abstract function read();
}
定义 addParam() 方法来允许用户整机参数到 $params
,getAllParams() 方法则用于访问该属性,获取 $params
的值。
此外,还创建了 getInstance() 方法来检测文件扩展名,并根据文件扩展名返回特定的子类。以及两个抽象方法 read() 和 write(),确保 PararmHandler 类的任何子类都支持这个接口。
接下来我们定义子类:
class XMLParamHandler extends ParamHandler {
function write() {
// 写入 XML 文件
// 使用 $this->params
}
function read() {
// 读取 XML 文件内容
// 并赋值给 $this->params
}
}
class TextParamHandler extends ParamHandler {
function write() {
// 写入 txt 文件
// 使用 $this->params
}
function read() {
// 读取 txt 文件内容
// 并赋值给 $this->params
}
}
这两个子类根据适当的文件格式进行读写。
在程序调用时,将会根据文件扩展名来写入数据到文本和 XML 格式的文件:
$test = ParamHandler::getInstance('./params.xml');
$test -> addParam('key1', 'val1');
$test -> addParam('key2', 'val2');
$test -> addParam('key3', 'val3');
$test -> write(); // 写入 XML 文件
$test -> read(); // 读取 XML 文件
通过类的方式,如果再有其他格式的扩展修改 getInstance() 方法和新增一个相应的子类就可以,不影响原来的两个子类,扩展上耦合性降低,重复减少。
6.2 UML
UML(Unified Modeling Language)是一种强大的图形化的语法,它的引入是用来描述面向对象系统。简单的逻辑可以直接用代码描述,但随着代码示例的增长以及复杂性的增加,仅使用代码来说明设计就显得力不从心,而通过类图可以清楚地描述结构和模式。
6.2.1 类图
1) 描述类(类用带有类名的方框来描述)
抽象类使用斜体的类名或者增加 {abstract} 到类名下面。
如果是接口,则增加<<interface>>
标识。
2)描述属性(直接写在类名下面)
属性前面的符号表示该属性的可见性(private、protected、public),具体见下图:
例如本例中的 # 表示 protected。属性名后面的冒号用于分隔属性名和它的类型及默认值。
3)描述方法(方法写在属性的下面,语法与属性相似)
4)描述继承和实现
使用一条空心闭合箭头的实线来表示子类和父类的继承关系。
使用一条空心闭合箭头的虚线来表示子类和父类的继承关系。
5)关联
继承只是面向对象中诸多关系中的一种。当一个类的属性保存了对另一个类的一个实例(或多个实例)的引用时,就产生了关联。
使用一条直线建立类和类之间的关联。
使用箭头来描述关联的方向。如果 Teacher 类拥有 Pupil 类的一个实例,但是 Pupil 类没有 Teacher 类的实例。就可以使用一条从 Teacher 类指向 Pupil 类。
两个类间互相拥有对方的引用,可以使用一个双向箭头来描述这种双向关联。
可以通过把次数或范围放在每个类旁边来说明。用(*)表示任意次数。
有一个 Teacher 对象和零个到多个 Pupil 对象。
一个 Teacher 对象拥有 5 到 10 个对 Pupil 对象的引用。
6)聚合和组合
聚合和组合都描述了一个类长期持有其他类的一个或多个实例的情况。通过聚合和组合,被引用的对象实例成为引用对象的一部分。
聚合关系使用一条以空心菱形开头的线来表示。我们定义两个类:SchoolClass 和 Pupil。SchoolClass 类聚合了 Pupil。
学生组成一个班级,但是相同的Pupil 对象可以同时被不同的 SchoolClass 实例引用。如果我们要删除一个学校类,不需要同时删除它引用的这些学生类,因为学生还可以加入其它班级。
而组合则是一种更强的关系。在组合中,被包含对象只能被它的容器所引用。当容器被删除时,它也应该被删除。
组合使用实心菱形描述。
Person 类持有对 SocialSecurityData 对象的引用,而该 SocialSecurityData 对象实例只属于包含它的 Person 对象。
7)描述使用
一个对象使用了另一个对象的关系通过一条虚线和开放箭头表示。
Report 类使用了 ShopProductWriter 对象,但 Report 类并没有把 ShopProductWriter 对象保存到类的属性中。
8)使用注解
类图可以描述描述系统的结构,但是不能解释处理任务的过程。通过使用注解(注解由一个折角的方框组成,通常包含伪代码片段)来描述具体细节。
图中,Report 对象使用了 ShopProductWriter,并使用注解进行了补充说明两者间的具体使用关系。
6.2.2 时序图
时序图(sequence diagram)是基于对象而不是基于类的,它用于为系统中过程化的行为建模。
时序图中只用类名来标记对象。
该图为 Report 对象输出产品数据的过程建模,时序同从左到右地展现了系统中的参与者。
使用垂直的虚线来表示生命线,展示系统中对象的生命周期。生命线上的矩形是对象的激活期。
该图从上到下展示了该过程中每个对象的生命周期。
使用箭头表示从一个对象传递到另一个对象。每个消息都用相关的方法调用来标记。