这一章主要讲三个方面。
设计模式、依赖管理和PHP的代码整洁。
一、依赖管理Composer
composer是什么?
是 PHP 用来管理依赖(dependency)关系的工具。你可以在自己的项目中声明所依赖的外部工具库(libraries),Composer 会帮你安装这些依赖的库文件。
其实一门语言的未来,是要靠生态来决定的。而生态呢,又需要制定标准。
在现代编程语言中,几乎都是共享库/包的方式来组合项目的。那么就需要一个权威的、被认可的标准包管理工具来管理这些包。
世界上最大的包管理工具是npm,上面托管了几十万上百万个包。
每一门主流语言都需要包管理工具,JavaScript和node.js有npm,Java有Maven,Dart有pub,python有pip,go就比较混乱了,有好几个,目前最主流的应该是govendor。php的呢,就是composer。
甚至可以这么理解,在某种程度上,composer是PHP的未来。
包管理器解决了什么问题?
解决了这个场景:你的项目依赖于若干个库,其中一些库依赖于其他库。你声明你的项目所依赖的东西,包管理器会找出哪个版本的包需要安装,并将它们下载到你的项目中。
安装
windows的安装相当简单。下载下来安装包后,一直点击下一步即可。
下载地址https://getcomposer.org/download/
安装完成后,使用composer --version
命令来查看是否是否安装成功。出现版本号即代表安装成功。
mac或linux的安装方式参见官方文档:https://docs.phpcomposer.com/00-intro.html
使用镜像
什么是镜像?为什么使用镜像?
源服务器离我们很远,或者我们国家对相应的域名/地址进行了限制,导致无法连接或者网速很慢。
为了解决这种现象带来的问题。很多大公司都在自己的服务器上面搭建了一个和源服务器一摸一样的环境,再通过CDN和云存储来提供服务给我们下载,并且每隔一段时间同步一下,比如10分钟,这就是镜像。
Packagist 中国全量镜像豆瓣搭建的pip镜像;阿里搭建的npm镜像cnpm;github的golang镜像;google在中国搭建的dart镜像,它们的目的都是解决以上问题。
Composer的镜像是由Packagist 中国全量镜像搭建的。
用法有两种。
-
系统全局配置
-
单项目配置
修改全局配置
在命令行终端输入:
composer config -g repo.packagist composer https://packagist.phpcomposer.com
修改当前项目配置
在项目根目录下输入:
composer config repo.packagist composer https://packagist.phpcomposer.com
这条命令会在当前项目的composer.json
文件的末尾自动添加镜像配置信息。因为我们还没有项目的配置,所以这里暂时先不使用这种方式。
解除镜象
通过以下命令解除镜像,配置会重置为官方源。
$composer config -g --unset repos.packagist
使用composer
composer的使用方式和npm极为相似,因为composer是受到了npm的启发创造出来的。
通过一个composer.json
文件来声明依赖关系,格式如下:
{ "require": { "monolog/monolog": "1.2.*" } }
composer.json文件所在的路径,就被认为是项目的根路径,如果你用过npm,这很好理解。
安装依赖
通过install命令安装项目依赖。
$ composer install
除了使用 install 命令外,我们也可以使用 require 命令快速的安装一个依赖而不需要手动在 composer.json 里添加依赖信息:
$ composer require monolog/monolog
更新依赖
# 更新所有依赖
$ composer update
# 更新指定的包
$ composer update monolog/monolog
# 更新指定的多个包
$ composer update monolog/monolog symfony/dependency-injection
# 还可以通过通配符匹配包
$ composer update monolog/monolog symfony/*
删除依赖
$ composer remove monolog/monolog
搜索依赖
$ composer search monolog
查看依赖
# 列出所有已经安装的包
$ composer show
# 可以通过通配符进行筛选
$ composer show monolog/*
# 显示具体某个包的信息
$ composer show monolog/monolog
包版本
composer包的版本与npm是一致的。采用语义化版本控制,格式遵循semver 2.0规范。
版本号的格式由X.Y.Z
组成,X为主版本号,只有更新了不向下兼容的API时进行修改主版本号;Y为次版本号,当模块增加了向下兼容的功能时进行修改;Z为修订版本号,当模块进行了向下兼容的bug修改后进行修改。
版本的安装约束一般有5种。
1.精确锁定版本
1.1.1
2.范围版本
使用连字符 - 来指定版本范围。
有一定的操作符>,>=,<,<=,!=和逻辑操作符||
> = 1.1、>=1.2 <2.0、>=1.2 <2.0 || 3.1.1、1.1.1-2.2.2
3.通配符
1.*
等于 1.0.0 - 2.0.0
4.波浪号
定义最小版本,~1.2
等于>=1.2 <2.0.0
5.脱字符
允许升级版本到安全的版本。
^1.2.3
相当于>=1.2.3 <2.0.0
包存储库
可以在这个网站搜索你想要的包:https://packagist.org/
二、框架基础设计模式-依赖注入dependency injection
什么是依赖注入?
wikipedia上给出的答案是:依赖注入是一种允许我们从硬编码的依赖中解耦出来,从而在运行时或者编译时能够修改的软件设计模式。
看上去不怎么好理解。这里我给出我所理解的什么是依赖注入?的答案:
依赖注入的本质是按需加载,高度解耦。就这么简单。
依赖注入是一种设计模式,遵守依赖倒置原则。
依赖倒置原则主要有三个核心:
-
高层模块不应该依赖低层模块。两个都应该依赖抽象
-
抽象不应该依赖细节,细节应该依赖抽象
-
针对接口编程,不要针对实现编程
为什么要讲依赖注入?
因为明天就要进入学习框架的阶段了,框架应该学什么?这个问题不好回答。我个人认为,学框架,得明白什么是框架。其实大多数面向对象语言的服务端框架都是MVC的模式。里面会充斥着各种设计模式。而此类框架设计的重中之重,应该是依赖注入。
其实这部分未必能够在实际中用到,如果你对依赖注入比较了解,或者不感兴趣,你可以选择不看这一部分。
实际应用
明白了上面的概念,我们来实际体验一下依赖注入。
假设有如下场景:
小明需要玩王者荣耀,但需要一部手机。
根据这一句话的业务,我们可以抽象出,人类,手机,游戏。
这个业务的本质是什么?玩王者荣耀。
谁来玩?小明。
前置条件是?需要一部手机。
分析完毕,写出以下代码。
<?php class Phone { public $phone; public function __construct($phone) { $this->phone = $phone; } public function getInfo() { return $this->phone; } } class Game { public $game_name; public function __construct($game_name) { $this->game_name = $game_name; } public function getInfo() { return $this->game_name; } } class Human { public $name; public function __construct($name) { $this->name = $name; } public function play() { $samsung_s10 = new Phone('三星s10'); $wang_zhe_rong_yao = new Game('王者荣耀'); echo $this->name . '用' . $samsung_s10->getInfo() . '玩' . $wang_zhe_rong_yao->getInfo(); } } $ming = new Human('小明'); $ming->play();
这样就可以实现了业务。但是如果换了一个手机来玩别的游戏该怎么办呢?比如小明这次要使用iPhone xs来玩消消乐。
如果继续使用上面这种方式的话,就需要来动态改变Human类中的内容。此时如果我们将手机和游戏在Human类的构造中注入进去,就可以比较好的解决这个问题。
思考完毕,修改代码。由于这里Phone和Game类都属于Human类一个行为的依赖,我们可以将它们放置到另一个php文件中,并创建一个名命空间 paly。
<?php class Phone { public $phone; public function __construct($phone) { $this->phone = $phone; } public function getInfo() { return $this->phone; } } class Game { public $game_name; public function __construct($game_name) { $this->game_name = $game_name; } public function getInfo() { return $this->game_name; } } class Human { public $name; public $phone; public $game; public function __construct($name, Phone $phone, Game $game) { $this->name = $name; $this->phone = $phone; $this->game = $game; } public function play() { echo $this->name . '用' . $this->phone->getInfo() . '玩' . $this->game->getInfo(); } } $iphone_xs = new Phone('iPhone xs'); $xiao_xiao_le = new Game('消消乐'); $ming = new Human('小明', $iphone_xs, $xiao_xiao_le); $ming->play();
这样解决了这个问题。
但同时又出现了新的问题,将上面的两个场景合起来, 业务场景就变成了这样:
小明用三星s10玩王者荣耀,接着用苹果xs玩消消乐。
如果继续使用上面这种方式的话,就需要再创建一个Human,传递不同的参数进去,这样做并不合适。我们需要有一个方法来注册不同的手机和游戏,并且同时只需要一个小明的实例。
<?php class Phone { public $phone; public function __construct($phone) { $this->phone = $phone; } public function getInfo() { return $this->phone; } } class Game { public $game_name; public function __construct($game_name) { $this->game_name = $game_name; } public function getInfo() { return $this->game_name; } } class Human { public $name; public $phone; public $game; public function __construct($name) { $this->name = $name; } public function setPhone($phone) { $this->phone = $phone; } public function setGame($game) { $this->game = $game; } public function play() { echo $this->name . '用' . $this->phone->getInfo() . '玩' . $this->game->getInfo() . '<br/>'; } } $ming = new Human('小明'); $samsung_s10 = new Phone('三星s10'); $wang_zhe_rong_yao = new Game('王者荣耀'); $ming->setPhone($samsung_s10); $ming->setGame($wang_zhe_rong_yao); $ming->play(); $iphone_xs = new Phone('iPhone xs'); $xiao_xiao_le = new Game('消消乐'); $ming->setPhone($iphone_xs); $ming->setGame($xiao_xiao_le); $ming->play();
接下来,我们的业务需求再继续细化,添加了时间和地点。比如:
小明今天中午在教室先用三星s10玩王者荣耀,下午用苹果xs玩消消乐。
我们可能会写出这种代码:
// 伪代码。 $ming = new Human('小明'); $samsung_s10 = new Phone('三星s10'); $wang_zhe_rong_yao = new Game('王者荣耀'); $xx_time = new Time('xx'); $xx_location = new Location('xx'); $ming->setPhone($samsung_s10); $ming->setGame($wang_zhe_rong_yao); $ming->setTime($xx_time); $ming->setLoaction($xx_location); $ming->play(); $iphone_xs = new Phone('iPhone xs'); $xiao_xiao_le = new Game('消消乐'); $yy_time = new Time('yy'); $yy_location = new Location('yy'); $ming->setPhone($iphone_xs); $ming->setGame($xiao_xiao_le); $ming->setTime($yy_time); $ming->setLoaction($yy_location); $ming->play();
我们发现,一旦业务逻辑所依赖的组件过多时,仍然会让程序难以维护。我们需要消灭掉那些set。
我们可以用工厂函数来对其进行一层封装。
<?php class Phone { public $phone; public function __construct($phone) { $this->phone = $phone; } public function getInfo() { return $this->phone; } } class Game { public $game_name; public function __construct($game_name) { $this->game_name = $game_name; } public function getInfo() { return $this->game_name; } } class Time { public $time; public function __construct($time) { $this->time = $time; } public function getInfo() { return $this->time; } } class Location { public $location; public function __construct($location) { $this->location = $location; } public function getInfo() { return $this->location; } } class Human { public $name; public $phone; public $game; public $time; public $location; public function __construct($name, $phone, $game, $time, $location) { $this->name = $name; $this->phone = $phone; $this->game = $game; $this->time = $time; $this->location = $location; } public static function factory() { $name = '小明'; $phone = new Phone('iphone xs'); $game = new Game('王者荣耀'); $time = new Time('上午'); $location = new Location('教室'); return new self($name, $phone, $game, $time, $location); } public function play() { echo $this->name . '' . '' . $this->time->getInfo() . '在' . $this->location->getInfo() . '用' . $this->phone->getInfo() . '玩' . $this->game->getInfo() . '<br/>'; } } $ming = Human::factory(); $ming->play();
但这又回到了最初的问题,在类的内部创建依赖。
我们需要再次认真思考,到底怎么样才能更好的解决这个问题。
我们可以尝试创建一个高级的注册抽象类,这个抽象类本身也是一个容器。
class DI { private $dep; public function set($key, $val) { array_push($this->dep, $key); $dep[$key] = $val; } public function get($key) { return array_search($this->dep, $key); } }
由于代码量太多,影响阅读,这里就不再贴出来了。
我们将Human类的构造函数参数改成di,然后Human类中每一个依赖都拆分成一个方法,从di中获取。
这样DI容器类就成了一个桥梁,一个全局注册表,将复杂性隔离出去。代码耦合更低。
如果不需要某一个依赖,甚至可以不初始化。
一个完善的依赖注入框架,还会有其他特性,比如自动绑定、自动解析、注释解析器、延迟注入等。这些就比较复杂了,需要花时间去理解和探索,我们到此为止,了解依赖注入是个什么东西,解决了什么事就可以了。
我写的示例代码有些重度设计,但是实际业务中的代码不会这么简单,手机内部肯定会包含很多属性和方法,如电量、温度、信号等。游戏也会包含一些属性和方法,如加载、更新、版本、高性能模式、FPS、延迟等。但这些都不是重点,我这么做的最终目的是让我们便于理解。
现在我们明白了,使用依赖注入解决我们的问题。不是在代码内部创建依赖关系,而是让其作为一个参数传递,这使得我们的程序更容易维护,降低程序代码的耦合度,实现一种松耦合。
三、代码整洁之道
作为PHP语言本身内容的最后一部分,我认为应该就是学一下代码整洁之道了,代码的整洁程度,直接影响了项目的可读性和可维护性。要知道,除了极少数的极端项目以外,代码的可读性和可维护性是比性能还要重要的部分。
好的代码不止是计器能读懂,还得让人也读懂。
这里简单说一下几个注意点:
变量
名命有意义。
同种类变量使用同一个名字。
使用易于搜索的变量名。
明确的名字比隐晦的名字更好。
不要添加没必要的上下文。
函数
参数保持简洁(2个以内),参数超过2个使用对象或者数组。
函数保持简洁,一个函数只做一件事。
函数只进行一层抽象,当超过一层抽象时,继续拆分功能。达到最高的可重用性和易用性。
删除重复代码。
通过对象赋值,设置默认值。
避免函数副作用。这个很重要,能够大大降低程序出现BUG的几率。
避免写全局函数。这样做的好处是防止污染命名空间。
封装条件语句。把判断写成函数。
避免消极条件。就是不要使用!来做判断。
避免条件声明。不要使用if。使用switch和case,一个函数只做一件事。
避免类型检查。不要给PHP代码写类型检测,做不到就是用类型声明。
移除僵尸代码。比如声明后从未调用的函数。
这一部分的内容,很多语言都是互通的,我也不给每一条写例子了,如果看了以上内容后仍有疑惑,你可以打开以下链接进行自学。里面包含了大量的对比示例。
https://github.com/yangweijie/clean-code-php
四、总结
这一章通过学习依赖管理器Composer,了解了PHP项目是如何构建和组成的。之后学些了依赖注入,并将概念落实到代码上,对理解框架的设计原理会有所帮助。最后通过学习代码整洁之道,避免我们写出质量很差的代码。
这一章为后续学习框架的使用及理解框架背后的运行原理提供了帮助。