深入PHP面向对象模式与实践

2021/6/2 20:25:01

本文主要是介绍深入PHP面向对象模式与实践,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

特别说明
● 前Yahoo!公司PHP高级开发人员20余年企业级Web开发经验总结
● 用通俗易懂的方式讲解复杂的面向对象编程原则和模式

简介
本书是PHP经典图书升级版,它既是一本关于面向对象设计与编程的书,也是一本关于如何使用工具管理PHP代码(从协作到部署)的书。书中讲解了PHP的新特性,例如匿名类以及标量参数提示和返回值类型。第5版重写了Composer和Packagist库的相关内容,并增加了关于Git版本控制的篇幅。示例代码全面更新,符合PSR-1和PSR-2标准。阅读本书能够帮你构建实现既定目标且易于协同开发的系统,并让你的代码优雅、简洁且易于理解。

本书特色
“本书正是为PHP企业级开发人员和软件工程师量身打造的,每一位想提升技能的PHP开发人员都应阅读。”——Ken Guest,PEAR QA团队成员,爱尔兰Linux用户组主席

“本书将原本复杂的面向对象编程原则和模式讲解得通俗易懂。我有Java编程经验,工作中又要用到PHP,在尝试将Java设计模式整合到PHP项目的过程中,从本书获得了不少实实在在的帮助。”——亚马逊英文网站读者评论

想要成为一名出类拔萃的PHP程序员,让项目开发高效、可持续,就得百尺竿头更进一步,不仅要知道PHP能做什么,还要理解软件系统的设计美学。

本书通过提出问题、给出实现和讨论效果的介绍方式,详细讲解面向对象的PHP编程原则与关键设计模式,是一本难得的PHP进阶佳作。第5版针对PHP新特性和程序开发现状全面更新,增加了匿名类、标量参数提示和返回值类型等特性介绍,重写了Composer和Packagist库的相关内容,并用完整章节分别介绍了Git版本管理、Vagrant以及PHP标准,示例代码也全面更新,符合PSR-1和PSR-2标准。

● 第一部分“对象”:PHP和对象的历史;类、对象和继承;PHP的高级面向对象特性。

● 第二部分“模式”:设计模式及原则;一些关键模式,如用于生成对象的模式、用于组织对象和类的模式、面向任务的模式、企业设计模式、数据库模式等。

● 第三部分“实践”:PHP标准;Composer和Packagist库;用Git进行版本控制;自动化测试及构建;Vagrant;持续集成。

文件:590m.com/f/25127180-497753686-685aaf(访问密码:551685)

以下内容无关:

-------------------------------------------分割线---------------------------------------------

前排提示:不要模仿这个例子,有类似的需求应该寻找第三方库或者使用容器/智能指针来实现类似的功能。

现在我们来设计一个二维数组,这个二维数组可以存任意的数据,所以我们需要泛型;我还想要能在初始化时指定数组的长度,所以我们需要一个构造函数来分配动态数组,于是我们的代码第一版是这样的:

template
class Matrix {
public:
Matrix(unsigned int _x, unsigned int _y)
: x{_x}, y{_y}
{
data = new T*[y];
for (auto i = 0; i < y; ++i) {
data[i] = new T[x]{};
}
}

~Matrix() noexcept
{
    for (auto i = 0; i < y; ++i) {
        delete [] data[i];
    }
    delete [] data;
}

private:
unsigned int x = 0;
unsigned int y = 0;
T **data = nullptr;
};
x是横向长高度,y是纵向长度,而在c++里想要表示这样的结构正好得把x和y对调,这样一个x=4, y=3的matrix看上去是下面的效果:

显而易见,我们的二维数组其实是多个单独分配的一维数组组合而成的,这也意味着他们之间的内存可能不是连续的,这也是我不推荐模仿这种实现的原因之一。

在构造函数中我们分配了内存,并且对数组使用了方括号初始化器,所以数组内如果是类类型数据则会默认初始化,如果是标量类型(int, long等)则会进行零初始化,因此不用担心我们的数组里会出现未初始化的垃圾值。

接着我们还定义了析构函数用于释放资源。

看起来一个简易的二维数组类Matrix定义好了。

还缺些什么
对,直觉可能告诉你是不是还有什么遗漏。

直觉通常是不可靠的,然而这次它却十分准,而且我们遗漏的东西不止一个!

不过在查漏补缺之前请允许我对两个早就人尽皆知的c++原则炒个冷饭。

rule of zero
c++的类类型里有几种特殊成员函数:默认构造函数、复制构造函数、移动构造函数、析构函数、复制赋值运算符和移动赋值运算符。

如果用户没有定义(哪怕是空函数体,除非是=default)这些特殊成员函数,且没有其他语法定义的冲突(比如定义了任何构造函数都会导致默认构造函数不进行自动合成),那么编译器会自动合成这些特殊成员函数并用在需要它们的地方。

其中复制构造/赋值、移动构造/赋值是针对每一项类的非静态数据成员进行复制/移动。析构函数则自动调用每一项类的非静态数据成员的析构函数(如果有的话)。

看起来是很便利的功能吧,假如我的类有10个成员变量,那编译器自动合成这些函数可以省去不少烦恼了。

这就是rule of zero:如果你的类没有自己定义任何一个除了默认构造函数外的特殊成员函数,那么就不应该定义任何一个复制/移动构造函数、复制/移动赋值运算符、析构函数。

标准库的容器都定义了自己的资源管理手段,如果我们的类只使用这些标准库里的内容,或者没有自己申请分配资源(文件句柄,内存)等,则应该遵守“rule of zero”,编译器会自动为我们合成合适的函数。

默认只进行浅复制
如果我要在类里分配点资源呢?比如某些系统的文件句柄,共享内存什么的。

那就要当心了,比如对于我们的Matrix,编译器合成的复制赋值运算符是类似这样的:

template
class Matrix {
public:
/* … */
// 合成的复制赋值运算符类似下面这样
Matrix& operator=(const Matrix& rhs)
{
x = rhs.x;
y = rhs.y;
data = rhs.data;
}

private:
unsigned int x = 0;
unsigned int y = 0;
T **data = nullptr;
};
问题很明显,data被浅复制了。对于指针的复制操作,默认只会复制指针本身,而不会复制指针所指向的内存。

然而即使能复制指针指向的内存,在我们这个Matrix里还是有问题的,因为data指向的内存里存的几个也是指针,它们分别指向别的内存区域!

这样会有什么危害呢?

两个指针指向同一个区域,而且两个指针最后都会被析构函数delete,当delete第二个指针的时候就会导致双重释放的bug;如果只删除其中一个指针,两个指针指向的内存会失效,对另一个指针指向的失效内存进行访问将会导致更著名的“释放后重用”漏洞。

这两类缺陷犹如c++er永远无法苏醒的梦魇。这也是我不推荐你模仿这个例子的又一个原因。

rule of five
如果“rule of zero”不适用,那么就要遵循“rule of five”的建议了:如果复制类特殊成员函数、移动类特殊成员函数、析构函数这5个函数中定义了任意一个(显式定义,不包括编译器合成和=default),那么其他的函数用户也应该显式定义。

有了自定义析构函数所以需要其他特殊成员函数很好理解,因为自定义析构函数通常意味着释放了一些类自己申请到的资源,因此我们需要其他函数来管理类实例被复制/移动时的行为。

而通常移动类特殊成员函数和复制类的是相互排斥的。

移动意味着所有权的转移,复制意味着所有权共享或是从当前类复制出一个一样的但是完全独立的新实例,这些对于所有权移动模型来说都是禁止的行为,因此一些类只能移动不能复制,比如mutex和unique_ptr。

而一些东西是支持复制的,但移动的意义不大,比如数组或者一块被申请的内存。

最后一种则同时支持移动和复制,通常复制产生副本是有意义的,而移动则在某些情况下帮助从临时对象那里提高性能。比如vector。

我们的Matrix恰好属于后者,移动可以提高性能,而复制出副本可以让同一个二维数组被多种算法处理。

Matrix本身定义了析构函数,因此根据“rule of five”应该至少实现移动类或复制类特殊成员函数中的一种,而我们的类要同时支持两种语义,自然是一个也不能落下。

copy and swap惯用法
说了这么多也该进入正题了,篇幅有限,所以我们重点看复制类函数的实现。

实现自定义复制
因为浅拷贝的一系列问题,我们重新实现了正确的复制构造函数和复制赋值运算符:

// 普通构造函数
Matrix::Matrix(unsigned int _x, unsigned int _y)
: x{_x}, y{_y}
{
data = new T*[y];
for (auto i = 0; i < y; ++i) {
data[i] = new T[x]{};
}
}

Matrix::Matrix(const Matrix &obj)
x{obj.x}, y{obj.y}
{
data = new T*[y];
for (auto i = 0; i < y; ++i) {
data[i] = new T[x];
for (auto j = 0; j < x; ++j) {
data[i][j] = obj.data[i][j];
}
}
}

Matrix& Matrix::operator=(const Matrix &rhs)
{
// 检测自赋值
if (&rhs == this) {
return *this;
}

// 清理旧资源,重新分配后复制新数据
for (auto i = 0; i < y; ++i) {
    delete [] data[i];
}
delete [] data;
x = rhs.x;
y = rhs.y;
data = new T*[y];
for (auto i = 0; i < y; ++i) {
    data[i] = new T[x];
    for (auto j = 0; j < x; ++j) {
        data[i][j] = rhs.data[i][j];
    }
}
return *this;

}
这样做正确,但非常啰嗦。比如复制构造函数里初始化xy和分配内存的工作实际上和构造函数中的没有区别,一句老话叫“Don’t repeat yourself”,所以我们可以借助c++11的新语法构造函数转发把这部分工作委托给构造函数,我们的复制构造函数只进行数组元素的复制:

Matrix::Matrix(const Matrix &obj)
Matrix(obj.x, obj.y)
{
for (auto i = 0; i < y; ++i) {
for (auto j = 0; j < x; ++j) {
data[i][j] = obj.data[i][j];
}
}
}
复制赋值运算符里也有和构造函数+析构函数重复的部分,我们能简化吗?遗憾的是我们不能在赋值运算符里转发操作给构造函数,而delete this后再使用构造函数也是未定义行为,因为this代指的类实例如果不是new分配的则不合法,如果是new分配的也会因为delete后对应内存空间已失效再次进行访问是“释放后重用”。那我们先调用析构函数再在同一个内存空间上构造Matrix呢?对于能平凡析构的类型来说,这是完全合法的,可惜的是自定义析构函数会让类无法“平凡析构”,所以我们也不能这么做。

虽说不能简化代码,但我们的类不是也能正确工作了吗,先上线再说吧。



这篇关于深入PHP面向对象模式与实践的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程