maksim 发布的文章

网上有很多数据模式的文章,这里不会按照已经成型的书和文章来进行编写,这样没有任何意义,在应用设计模式的时候一定要根据自己业务和使用的语言来进行编写。

在工厂模式中,我们基于商城的案例编写了一个书、狗和酒的工厂案例,在线我们对需求进行一次迭代,老板决定每个商品的 getList 方法都要额外加上狗的信息(狗本身除外)。

当我们接到这个需求的时候,首先第一个想法肯定是这样实现:

include 'autoload.php';
//$object = new Books();
$book = ProductFactory::getProduct('book');
$bookList = $book->getList();
$dog = ProductFactory::getProduct('dog');
$dogList = $dog->getList();

接下来,我们利用设计模式来设计一下,不过首先我们需要解决一个问题,目前的代码虽然都有 getList 但是却并不是一个硬性的规范,如果现在商城增加品类了,例如增加了一个 Phone 交给了一个新的开发人员去开发,获取列表的方法它给写成了 getPhones 那么我们对应的 ProductFactory 在实现上可能就会存在很大的麻烦。

对于约束,我们可以先定义个接口:

<?php

interface IProduct
{
    public function getList();
} 

这样无论以后我们新增什么品类都使用这个方法去进行创建,我们目前已有的三个类都去 implements ,接下来我们对工厂模式进行一些改进。

<?php

class ProductFactory
{
    public static function getProduct($type)
    {
        switch ($type) {
            case "book" :
                $obj = new Books();
                break;
            case "dog" :
                $obj = new Dogs();
                break;
            case "wine" :
                $obj = new Wines();
                break;
            default:
                $obj = null;
        }
        if (is_subclass_of($obj, "IProduct")) {
            return $obj;
        }
        return null;
    }
}

我们在最后增加了 is_subclass_of 判断,该函数可以判断当前的变量是否是某一个类、抽象类、接口的子类,这样可以让我们的工厂更加健全,避免返回的对象不满足 IProduct 的约束,可以减少运行时的报错,因为你无法保证其他人修改工厂方法的时候放进来的类到底满不满足我们的业务要求。

如果要实现我们现在的需求,需要用到注册树模式,我们在这里叫它数据中你想你模式,我们把数据全部都放到数据中心中进行统一的管理,取数据的时候也从数据中心取,坚决不合类本身私自交往。

<?php

class ProductDataCenter
{
    public static $objectList = [];

    public static function set($k, $v)
    {
        self::$objectList[$k] = $v;
    }

    public static function get($key)
    {
        return self::$objectList[$key] ?? [];
    }
}

ProductDataCenter 的职责非常简单,就是存储我们的产品列表,对外提供两个方法:

  • set 存值

接下来我们继续改造工厂方法,工厂不再继续返回值,而是向 ProductDataCenter 中增加数据。

<?php

class ProductFactory
{
    public static function getProduct($type)
    {
        switch ($type) {
            case "book" :
                $obj = new Books();
                break;
            case "dog" :
                $obj = new Dogs();
                break;
            case "wine" :
                $obj = new Wines();
                break;
            default:
                $obj = null;
        }
        if (is_subclass_of($obj, "IProduct")) {
            // 把创建的对象放到数据中心
            ProductDataCenter::set($type, $obj);
        }
    }
}

这样一来,我们在外面调用的时候就变成了这个样子:

<?php

include 'autoload.php';
//$object = new Books();
ProductFactory::getProduct('book');

$data = ProductDataCenter::get;
var_dump($data);

但是目前还是没有解决我们的需求,我们还需要继续修改数据中心。

<?php

class ProductDataCenter
{
    public static $objectList = [];

    public static function set($k, $v)
    {
        self::$objectList[$k] = $v;
    }

    public static function __callStatic($name, $arguments)
    {
        $result = [];
        foreach (self::$objectList as $k => $v) {
            if (method_exists($v, $name)) {
                $ret = $v->$name($arguments);

                if ($ret) {
                    foreach ($ret as $item) {
                        $result[] = $item;
                    }

                }
            }
        }
        if (count($result) > 0) {
            return $result;
        }
    }
}

在这里,我们增加了一个 __callStatic 魔术方法,这个魔术方法的作用是,当调用静态方法不存在的时候就会走到这个函数。

其主要作用就是从注册树种取出 Proudct 类,然后执行 getList 方法,并且将在注册树中存在的 Product 都执行一遍,组成成我们想要的数据返回回去。

现在如果我们想要展示 Book 和 Dog 的数据只需要这样获取即可:

<?php

include 'autoload.php';
//$object = new Books();
ProductFactory::getProduct('book');
ProductFactory::getProduct('dog');
$data = ProductDataCenter::getList();
var_export($data);

## 执行结果如下:
array (
  0 => 
  array (
    'product_id' => 101,
    'product_name' => 'java 从入门到精通',
  ),
  1 => 
  array (
    'product_id' => 102,
    'product_name' => 'PHP 从入门到精通',
  ),
  2 => 
  array (
    'product_id' => 201,
    'product_name' => '泰迪',
  ),
  3 => 
  array (
    'product_id' => 202,
    'product_name' => '金毛',
  ),
)%

既然是注册树模式,就还需要有从树上摘除的操作:

public static function remove($key)
{
        unset(self::$objectList[$key]);
}

这一节就结束了。

在正式开始之前,我们先来编写一个自动加载功能,这样我们就不用老是 include 文件了。

<?php

define('ROOT_PATH', str_replace('\\', '/', realpath(dirname(__FILE__) . '/')) . "/");
function autoload($className)
{
    $classPath = ROOT_PATH . 'src' . DIRECTORY_SEPARATOR . $className . '.class.php';
    include $classPath;
}

spl_autoload_register('autoload');
  • 首先我们声明一个 ROOT_PATH,用来帮助我们定位当前目录
  • autoload 函数,用来自动文件的函数,其中定义了加载规则,我们统一将代码放到 src 目录下,每个类文件的后缀为 .class.php
  • 通过 spl_autoload_register 来做自动加载注册,当 php 运行后如果发现当前没有要用得类就会触发 autoload 函数进行文件加载。

我们的项目目录最终结构如下:

├── autoload.php
├── index.php
└── src
    └── xxx.class.php

好了我们回到正题。很多人觉着设计模式在实际代码中离自己很远,希望看完这个系列的文章后,能够利用设计模式改善自己的代码。

最开始我们来学最简单的工厂模式,网上有很多关于工厂模式的文章,在这里,我们结合 PHP7 来进一步的演化和演进,我们写设计模式不能按照网上的通用代码,一定要结合语言和项目才能写好,很多同学在网上看到了 Java 的工厂模式,就和 Java 写的一模一样,那为什么不直接使用 Java 而使用 PHP 呢?

我们来看一个非典型的电商系统,刚起步的时候往往只有 1-2 个商品,例如只有图书,此时对的协作模式很简单,开发人员可能只有一个,编写的代码可能就是下面这个样子。

$object = new Books();

在 Book 中包含:

  1. 图书信息的维护
  2. 图书点击量
  3. 图书架构
  4. 图书的订单

如果这种规模确实这么做就可以了,随着项目越来越大,我们的库中会增加的一些其他商品除了图书还增加了酒喝狗,开发人员也越来越多。

# index.php
# 程序员 A 负责 Book 的维护迭代
$obj = new Books();
# 程序员 B 负责 Dog 的维护迭代
$obj = new Dogs();
# 程序员 C 负责 Wine 的维护迭代
$obj = new Wines();

我们大哥比方,假设每个类都有一个获取商品列表的方法:getList();

Books 的代码实现:

<?php

class Books
{
    public function getList()
    {
        return [
            ['product_id' => 101, "product_name" => "java 从入门到精通"],
            ['product_id' => 102, "product_name" => "PHP 从入门到精通"],
        ];
    }
}

Dogs 的代码实现

<?php

class Dogs
{
    public function getList()
    {
        return [
            ['product_id' => 201, "product_name" => "泰迪"],
            ['product_id' => 202, "product_name" => "金毛"],
        ];
    }
}

Wines的代码实现

<?php

class Wines
{
    public function getList()
    {
        return [
            ['product_id' => 301, "product_name" => "红酒"],
            ['product_id' => 302, "product_name" => "白酒"],
        ];
    }
}

正常业务代码中,getList 中的应该从数据库中取,这里为了演示方便直接返回数组。

如果我们做这个开发,这三个商品都应该在库中标记为商品,不过类型不同,很可能呈现的展现形式和业务逻辑是不一样的,所以需要区分成不同的商品。

这么写有没有什么问题?

如果我们把书和狗看成两个独立的子频道,开发起来是没有任何问题的。

但是假设现在有一天图书的货源出问题了,老板临时决定范式访问书的数,统一返回狗的数据。

这个时候程序员可能这么做:

<?php

include 'autoload.php';
//$object = new Books();
$obj = new Dogs();
$list = $obj->getList();

注释掉原有的代码,用新的实体类替换老的实体类,如果项目做大了,这个时候要进行这样的改动会很大。这个时候就需要涉及到工厂模式了。

工厂模式就是将我们类的实例化和取出全部有一个通过工厂方法的参数不同来取出对象,实例化对象的时候就变成了这样。

<?php

include 'autoload.php';

$object = ProductFactory::getProduct('book');
$object->getList();

实现代码如下:

<?php

class ProductFactory
{
    public static function getProduct($type)
    {

        switch ($type) {
            case "book" :
                $obj = new Books();
                break;
            case "dog" :
                $obj = new Dogs();
                break;
            case "wine" :
                $obj = new Wines();
                break;
            default:
                $obj = false;
        }

        return $obj;
    }
}

这样工厂模式就完成了,回到刚才老板说的需求,要替代将 book 的数据提到成 dog,我们只需要修改工厂模式就可以了。

<?php

class ProductFactory
{
    public static function getProduct($type)
    {
        if ($type == "book") {
            $type = "dog";
        }

        switch ($type) {
            case "book" :
                $obj = new Books();
                break;
            case "dog" :
                $obj = new Dogs();
                break;
            case "wine" :
                $obj = new Wines();
                break;
            default:
                $obj = false;
        }

        return $obj;
    }
}

这样一来,我们在获取图书列表的的时候就会被替代成狗的数据。

PHP 提供了3个用于测试变量值的函数,分别是isset()、empty()、is_null(从这里就可以看出PHP系统函数变量名命名的混乱,这也是一直被人诟病的地方)。这几个函数均返回布尔值,有时使用不当会造成意想不到的结果。

比如,用isset()和empty()返回的结果是相反的,但却并非一直如此。

isset()用来检测一个变量是否已声明且值不为null。只能在变量不是null时返回真。

empty()用来检测一个变量是否为空,也就是说有如下情况时返回真值:变量是一个空字符串,false,空数组,null,'',以及被unset删除后的变量。

在PHP5.5之后,empty()函数可以接受任意类型的表达式

is_null()函数用来判断变量内容是否是null,即返回真值的条件仅为变量值是null,值得一提的是,is_null() 是 isset() 的反函数,区别是isset()函数可以应用到未知变量,但is_null()只能针对以声明的变量。

对比项
变量值($var)isset($var)empty($var)is_null($var)
""(空字符串)bool(true)bool(true)bool(false)
" "(空格)bool(true)bool(false)bool(false)
FALSEbool(true)bool(true)bool(false)
TRUEbool(true)bool(false)bool(false)
array()bool(true)bool(true)bool(true)
NULL bool(true)bool(true)

对于PHP编译器来说,脚本的结束标签?>是可选的,在写程序时你可以忽略它。你或许碰见过:在使用include()、require()或输入输出缓冲函数时,页面顶部有时会多空行或者出现“header had send”之类的错误信息,这类问题与结束标签有关。

省略结束标签适合纯PHP文件。如果是PHP与HTML混合开发,则不可省略。

忽略结束标签不仅能少些两个字符,而且可以使得我们开发的过程更加顺利。

今天给大家介绍一本 PHP 扩展开发相关的书籍《PHP internals Book》,这本书是几个PHP开发人员之间的协作努力,可以更好地记录和描述PHP内部的工作原理。

《PHP internals Book》 有三个主要目标:

  • 记录和描述PHP内部工作原理。
  • 记录并描述如何使用扩展扩展语言。
  • 记录并描述如何与社区进行交互以开发PHP本身。

《PHP internals Book》 主要面向具有C编程语言经验的开发人员。然而,尽管如此,我们将尝试提炼信息并对其进行总结,以便不了解C的开发人员仍然能够理解内容。

但是,让我们坚持。如果您不知道C语言,您将无法实现高效,稳定(任何平台下的崩溃),性能和实用性。以下是有关C语言本身,生态系统和构建工具以及操作系统API的一些非常好的在线资源: