人人都能看懂的全栈开发教程——面向对象

人人都能看懂的全栈开发教程——面向对象

Chris Yue No Comment
Posts

一个人开发一个淘宝那样的网站,我估计给他五年时间都是不太现实的,还好我们可以分工,将一个网站分成若干部分,给若干个程序员完成,甚至是可以分给若干小组,每个小组再分给若干程序员完成。

如果我们按照一开始那样写代码,所有功能都在一个文件里,我们只能从业务上分工(A 写任务列表,B 写新增任务)。后来我们引入了 MVC 的概念,这样可以让熟悉业务的人去完成 M 层,而让熟悉前端的工程师去做 V,而 C 因为相对简单随便安排个人来完成就行。MVC 其实已经是一种层的分工,从而可以更大发挥『术业有专攻』的好处。

虽然我们目前已经掌握了『纵向』和『横向』两种分工方式,但实际的开发过程中,产品可能给出十个需求,但一般来说都会先要求实现,或者先上线前三个必要的需求。现在都讲究『敏捷』,如果感觉反响不错,再继续投入开发资源陆续实现剩下的需求(当然也有可能没啥效果,就直接放弃作别的)。

所以能在层次上分的越细,越有可能让更多人来参与项目,加快开发速度。

在 MVC 的基础上,我们还能继续分吗?

C 看起来是不可能再分了,因为已经够简单。

V 主要面对的是『交互界面』,其实要处理的事情不少,但在我们还没找到『设计师』来给我们美化页面之前,先把这个问题放放。

而我们的 M 做了什么?以创建任务为例:1) 检查用户的输入,2)并存到数据库。嗯,是不是听起来就可以拆分?

在之前,我们都是先使用 PDO 创建了一个数据库链接对象,然后操作 SQL 语句完成某一个需求。但实际上,产品的需求刚传达到技术的时候,跟你表有什么字段,字段什么类型,用什么数据库,甚至用不用数据库,都还没什么直接关系,但需求已经有了。我们之前的(或者说经典的)开发方式,是以数据(库)为中心的,但是这不够直接,为什么我们要把跟数据库强相关的技术知识,跟那些和技术其实没太大关系的业务知识放在一起思考呢?我们能不能就先跟着产品需求走,先把需求对应的业务代码写好,再考虑数据存储的问题呢?

正好我们有一个新的需求,用户注册,就属于并不是简单存储数据的需求,我们在存储之前需要先加密密码。注意,虽然密码加密看起来好像是一个技术问题,但实际上这个问题跟某种具体的技术知识一点关系都没有。我通过一句话来阐述我的意思:密码要不要加密,是一个业务需求,即使一个产品不知道怎么去加密,但他可以要求加密密码,另外还有一点是,加密密码是会影响产品的需求的,即产品得知道,使用非明文的方式存密码,以后就不能提『查看用户密码』这类的需求了(当然如果要求是双向加密可以还原那另当别论);而具体用 password_hash 函数并且是用默认加密方式来加密,是一个技术问题,产品关心不了这个,也不需要关心。

这种先将一部分问题抛在一边,而只关注某一个领域的思想,其实就可以称之为『抽象』了,而面向对象编程,几乎是为解决『抽象』问题量身定制的。

我们可以实例对比一下,使用面向对象的思维,是怎么去考虑,或者说是怎么去描述『用户注册』这个问题的。

我们先在项目根目录下创建一个目录叫做 Domain,意为『(业务)领域』,我们可以这么想这个问题:产品提出的需求,都应当翻译成代码,并且这些代码都应该在 Domain 这个目录里。

然后我们在 Domain 里创建一个文件叫 UserManager.php,并写上如下代码:

这里我还是先简单介绍一下什么叫做类(class),以及它和对象什么关系。

以人为例子,我们可以把人看作一个『类』,而某个具体的人,比如你,就是人这个类的一个『对象』。而人都有个名字,所以『返回名字』是人的一个『方法』。上面例子中 public function xxxx 那部分就是『方法』。实际上,我们已经接触过类和对象,PDO 就是一个用来连接数据库的『类』,我们只知道它规定了可以执行 SQL 语句;而我们获取任务列表时,使用 new 操作符,描述了要连接的数据库名称,用户名,密码,这么创建出来的一个全世界独一无二的,具体的东西,就是我们的 $pdo 『对象』。类是对象的抽象。虽然每个人名字不同,但都可以『返回名字』;虽然每个 PDO 对象要连接的数据库不同,但都可以执行 SQL 语句。

再回到我们的用户管理器类(UserManager)上,我们定义了用户管理器类的一个方法,叫做『注册(register)』,并且这个方法要求传两个参数,一个字符串类型的 $username,一个字符串类型的 $password。类『方法』跟数学里的『函数』一样,当参数 x 输入的时候,f(x) 总是要返回一个值;而在 PHP 里,我们除了可以规定方法的参数的类型,也可以规定方法返回结果的类型。在我们上面的例子里 register 方法返回的类型叫 void 也就是『不返回任何值』。

可能有人会问了,不是说 public function xxx 就是方法吗?为什么只说 register 呢?__construct 不是方法吗?__construct 也是方法,但比较特殊,它只能在某个类被 new 的时候运行。

UserManager::__construct 方法上,要求输入两个参数,一个参数是 UserRepositoryInterface 类型,另一个是 PasswordEncoderInterface 类型。实际上,这俩是我定义的『接口』,它们也分别对应了两个新的 PHP 文件,我们先来看 UserRepositoryInterface.php,它也被放在了 Domain 目录下

不同于类,它是一个接口(interface)。类是对象的抽象,而接口是类的抽象。

人可以移动,猫也可以移动,但是移动的方式是一个是两脚站着走,一个是四脚爬着走,如果『人』是个类,『猫』是个类,那么『动物』就是一个接口。虽然对于动物来说,不知道用几条腿走,甚至都不一定是走而是飞或者游,但它一定能『移动』。

回到我们自己的例子,UserRepositoryInterface 代表了用来存放用户信息的『仓库』,这个时候我们也不用去关心这个『仓库』到底是把数据存数据库了还是存文件里了,库用的是 MySQL 还是 SQLite,但我们知道它有一个方法叫做『添加』,并且接受用户名和密码两个参数就行了。

同理,我们再来创建 PasswordEncoderInterface.php 也放在 Domain 目录下:

无论密码加密器是用什么方法加密,总之它能加密(encode),并且接受一个字符串做参数,以及返回一个字符串(即加密后的字符串)。

让我们再回到 UserManager.php 并注意看 register 方法内部,即体现了我们的需求:

  1. 把用户的密码加密
  2. 然后存起来

有了这俩接口文件,我们就可以找两个程序员同时一起开发了,一个只用关心如何把用户名和密码存到 MySQL 里:

一个只用关心怎么给密码加密。

注:Infrastructure 直译为『基础设施』,其实就是指『具体实现』。

最后我们创建 console/register.php 来完成在命令行下创建用户这个需求:

先解释一下上面例子第 7 行引入的 autoload.php,如果按照传统的 PHP 写法,要使用某一个类或者接口,得先 require 类的文件,如果我们一个 PHP 文件要使用 10 个类或者接口,就得 require 10 次,好在 PHP 后来提供了『自动加载』功能,让我们在某个文件使用某个类的时候,就尝试自动去加载类的文件。但 PHP 它怎么知道加载哪一个文件呢?规则其实是我们自己定的,让我们在项目根目录下先创建 autoload.php 并写入以下代码

spl_autoload_register 方法传入了一个我们自己定义的匿名函数,此匿名函数的作用就是告诉 PHP 应该如何去找类文件。比如如果提到一个类叫 Domain/UserManager ,我会先尝试将类名里的 \ 替换成路径分隔符 /,并在末尾添加 .php,再在头部拼上 __DIR__,变成类的完整文件路径,并 require 之。按理说我应当先检查拼好的路径是否存在,但实际上此方法只是一个临时做法,后面我会告诉大家更常规并且更好用的方法,这里就先简单处理了。

回到 console/register.php,我们看到此文件的作用就是,定好要用什么仓库,定好要用什么密码加密器,传给用户管理器,然后接受从命令行传来的数据,通过用户管理器的 register 方法注册完就完事儿了。让我们通过命令行运行一下试试看:

再通过 MySQL 的客户端确认是否添加成功:

如果有一天,我们不用 MySQL 而用 SQLite 了,我们无需动 Domain 里的任何代码,实际上我们也不需要动 Infrastructure 里的代码,只用在 Infrastructure 里再新增一个 SqliteUserRepository 类,然后再在 console/register.php 里重新指定用 SqliteUserRepository 来做我们的『仓库』就好了,想要替换加密方式也是同理。修改存储数据库类型,或者修改加密方式,都是技术关心的问题,跟业务,或者说跟产品一点关系都没有,所以我们不应该修改 Domain 里任何东西;如果非得要改,说明代码设计是有问题的。反过来,如果有一天产品说,我们密码不需要加密了,那么就应该要改 Domain 里的代码。

最后,扯到这,我们已经接触一个概念叫做『业务驱动开发了』,你偶尔会看见的 DDD(Domain Driven Development)就是它。如果我们总是尝试快速响应业务需要,业务代码先行,就可以说是 DDD。

注:实例代码里出现的用于获取命令行参数的 getopt 函数,替换字符串使用的 strtr 函数,以及还有一个看着用法很『奇怪』的 compact 函数,这里就不去单独解释他们怎么用了,以后教程也不会介绍每一个函数的用法。知道某个函数怎么用不重要,知道怎么去查某个函数的用法,并且知道各种细节更重要,所以大家一定要养成去官网查文档的好习惯。

本章完整代码见这里

人人都能看懂的全栈开发教程——面向对象 by Chris Yue is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

微信赞赏码

如果觉得文章还不错,就请扫码鼓励一下作者吧
天使打赏人

发表评论

80 + = 85