工厂设计模式及场景

一、概述

由策略模式进一步封装,演变成工厂模式。简单粗暴的理解就是把原来if else的判断逻辑封装成一层类。

二、应用场景

浏览论坛时,如果你是游客(未登录)的身份,通常只有帖子的阅读权限。当看到某篇感兴趣的帖子,想评论回帖时,系统会提示需要登录或注册会员。
当你注册成为会员后,就有了回帖的权限。随着时间的推移,你非常喜欢这个论坛,并想参与管理、维护该论坛,此时你需要管理员权限。管理员权限通常有删除垃圾贴的权限,这是普通会员无法使用的功能。
那么这个场景如何应用设计模式来解决呢?话不多说,Show you a code。

三、工厂模式

1.概念

一个工厂类拥有一个静态的方法,用来接收一些输入,并根据输入决定创建哪个类的实例。

2.代码示例

abstract class User {
    function __construct($name) {
        $this->name = $name;
    }

    function getName() {
        return $this->name;
    }

    function hasReadPermission() {
        return true;
    }

    function hasModifyPermission() {
        return false;
    }

    function hasDeletePermission() {
        return false;
    }

    function wantsFlashInterface() {
        return true;
    }
}

class GuestUser extends User {

}

class CustomerUser extends User {
    function hasModifyPermission() {
        return true;
    }
}

class AdminUser extends User {
    function hasModifyPermission() {
        return true;
    }

    function hasDeletePermission() {
        return true;
    }

    function wantsFlashInterface() {
        return false;
    }
}

class UserFactory {
    private static $users = array(
        'Andi' => 'admin',
        'Stig' => 'guest',
        'Derick' => 'customer', 
    );

    static function Create($name) {
        if (!isset(self::$users[$name])) {
            die("user not exist");
        }
        switch (self::$users[$name]) {
            case 'guest':
                return new GuestUser($name);
            case 'customer':
                return new CustomerUser($name);
            case 'admin':
                return new AdminUser($name);
            default:
                die('user not exist');
        }
    }
}

function boolToStr($b) {
    if ($b === true) {
        return 'Yes' . "\n";
    } else {
        return 'No' . "\n";
    }
}

function displayPermissions(User $obj) {
    print $obj->getName() . "'s permissions:\n";
    print 'Read: ' . boolToStr($obj->hasReadPermission());
    print 'Modify: ' . boolToStr($obj->hasModifyPermission());
    print 'Delete: ' . boolToStr($obj->hasDeletePermission());
}

function displayRequirements(User $obj) {
    if ($obj->wantsFlashInterface()) {
        print $obj->getName() . ' requires Flash' . "\n";
    }
}

$logins = array(
    'Andi',
    'Stig',
    'Derick',
);

foreach ($logins as $login) {
    displayPermissions(UserFactory::Create($login));
    displayRequirements(UserFactory::Create($login));
}

3.解析

  • 定义基类User,子类GuestUser、CustomerUser、AdminUser分别继承基类,并根据业务重写相应权限方法
  • 定义工厂类UserFactory,静态方法Create根据传入参数name的不同,创建不同子类的实例

策略设计模式及场景

一、概述

软件设计模式中的策略模式,让我联想到生活中一个很常见的场景,挺有趣,于是记录之。

二、场景描述

三大主流操作系统mac os、windows、linux,不同平台文件的后缀名可能有所差异。例如windows上的压缩文件后缀为.zip,linux上压缩文件后缀为.tar.gz。
如果你曾经通过浏览器在网站上下载过软件,一定有过这样的体验。网站会自动选择适配你系统的软件格式,供你点击下载。那么如此便捷操作的背后是怎样做的呢?网站又如何知道你用的是什么操作系统呢?

三、策略模式

1.概念

策略模式的实现方法通常是通过声明一个抽象的拥有一个算法方法的基类来实现的,而且通过继承这个基类的具体新的的类来实现。(有点绕,话不多说,Show you a code.)

2.代码示例

abstract class FileNamingStrategy {
    abstract function createLinkName($fileName);
}

class ZipFileNamingStrategy extends FileNamingStrategy {
    function createLinkName($fileName) {
        return "https://example.com/$fileName.zip";
    }
}

class TarGzFileNamingStrategy extends FileNamingStrategy {
    function createLinkName($fileName) {
        return "http://example.com/$fileName.tar.gz";
    }
}

if (strstr($_SERVER['HTTP_USER_AGENT'], 'Win')) {
    $fileNamingObj = new ZipFileNamingStrategy();
} else {
    $fileNamingObj = new TarGzFileNamingStrategy();
}

$calc_filename = $fileNamingObj->createLinkName('Calc101');
$stat_filename = $fileNamingObj->createLinkName('Stat2000');

print <<<EOF
<h1>The following is a list of great downloads</h1>
<br>
<a href="$calc_filename">A great calculator</a>
<br>
<a href="$stat_filename">The best statistics application</a>
<br>
EOF;

3.解析

  • 用户在浏览器访问页面时,HTTP请求头中带有user agent信息,UA里面包含了用户的操作系统、浏览器、浏览器版本等信息,据此服务端能够判断出用户的操作系统类型。根据不同的操作系统类型,选择new不同的对象。
  • 基类FileNamingStrategy是一个抽象类,定义了一个createLinkName的抽象方法。分别在子类ZipFileNamingStrategy、TarGzFileNamingStrategy中实现。

php中的自动加载

一、概述

看到__autoload函数,感到很有意思,于是记录之。

二、Show you a code

MyClass.php:

<?
class MyClass {
    function printHelloWorld() {
        print "Hello, World\n";
    }
}

general.inc:

<?php
function __autoload($strClassName) {
    // 本地测试时,此处可以替换成本地目录
    require_once($_SERVER["DOCUMENT_ROOT"] . '/$strClassName');
}

main.php:

<?php
require_once 'general.inc';

$obj = new MyClass();
$obj->printHelloWorld();

三、总结

1.使用方法

将MyClass.php、general.inc、main.php三个文件放到同一目录下,执行php main.php。输出结果为Hello, World.

2.原理浅析

general.inc中通过传入$strClassName的不同,进而加载不同的类。
而main.php中通过require_once引入general.inc文件,进而相当于也加载了类。

php对象的浅拷贝与深拷贝

一、概述

php4时php的对象复制采用深拷贝,php5后php的对象复制采用深拷贝。话不多说,看下面一段代码。

二、Show you a code

class MyClass {
    public $var = 1;
}
$obj1 = new MyClass();
$obj2 = $obj1;
$obj2->var = 2;
print $obj1->var;

1.这段代码做了什么

首先new一个MyClass类的对象obj1,然后将obj1复制给$obj2,将obj2对象的var属性变为2,最后输出obj1的var属性值。

2.输出是什么

  • 在php4中,输出结果为1。
  • 在php5中,输出结果为2。

三、简单粗暴地理解深拷贝和浅拷贝

  • 浅拷贝:复制时指向同一块内存,在其中一方有变更时,其他指向该内存的实体也会受影响。
  • 深拷贝:复制时开辟一块新内存,并将原内存中的数据复制过去。其中一方变更时,不影响其他实体。

四、php5中复制时如何使用浅拷贝

将复制那行代码作如下更改:

$obj2 = clone $obj1;

Redis数据结构-字典

一、概述

redis字典的数据结构主要涉及三个结构体,分别为dict、dictht和dictEntry。

二、数据结构

typedef struct dictht {
    // 哈希表数组
    dictEntry **table;
    // 哈希表大小
    unsigned long size;
    // 哈希表大小掩码,用于计算索引值
    // 总是等于size - 1
    unsigned long sizemask;
    // 该哈希表已有节点的数量
    unsigned long used;
} dictht;
typedef struct dictEntry {
    // 键
    void *key;
    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;
    // 指向下个哈希表节点
    struct dictEntry *next;
} dictEntry;
typedef struct dict {
    // 类型特定函数
    dictType *type;
    // 私有数据
    void *privdata;
    // 哈希表
    dictht ht[2];
    // rehash索引
    // 当rehash不再进行时,值为-1
    int rehashidx;
} dict;

三、哈希算法

Redis计算哈希值和索引值的方法如下:

hash = dict->type->hashFunction(key);

index = hash & dict->ht[x].sizemask;

其中dict的type属性是一个指向dictType结构的指针,每个dictType结构保存了一簇用于操作特定数据类型的函数

四、解决键冲突

dictEntry结构的next指针可以看出,redis的字典采用链地址法解决哈希冲突。

注意当发生哈希冲突时,总是将新节点添加到表头

五、rehash

1.目的

随着操作的进行,哈希表保存的键值对会逐渐增多会减少,为了让哈希的负载因子维持在一个合理的范围内,程序需要对哈希表的大小进行相应的扩展或收缩。

负载因子计算公式:

load_factor = ht[0].used / ht[0].size

2.步骤

  1. 为字典的ht[1]分配空间,分配空间的大小取决与做什么操作及ht[0]当前包含的键值对数量。
    • 扩展操作,ht[1]的大小为第一个大于等于 (ht[0].used * 2)的2^n;
    • 收缩操作,ht[1]的大小为第一个大于等于(ht[0].used)的2^n;
  2. 将保存在ht[0]上的所有键值对rehash到ht[1]上。rehash是指重新计算键的哈希值和索引值,然后将键值对放到ht[1]哈希表指定的位置。
  3. 当ht[0]包含的所有键值对都迁移到ht[1]后,释放ht[0],将ht[1]设置成ht[0],并在ht[1]新创建一个空白的哈希表,为下一次rehash做准备。

渐进式rehash

1.目的

rehash动作不是一次性、集中式地完成的,当哈希表里保存的键值对是四百万、四千万甚至四亿个时,一次性将这些键值对rehash到ht[1],庞大的计算量可能会导致服务器一段时间内停止服务。

2.步骤

在rehash基础上,将步骤2细化

  • (1) 在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作开始。
  • (2) 每次rehash工作完成之后,即将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1]。rehashidx属性的值+1。
  • (3) 整个rehash操作完成后,即ht[0]上的所有键值对都被rehash至ht[1]。rehashidx的值设为-1

注意rehash过程中的删除、查找、更新将在ht[0]和ht[1]两个哈希表上进行,新增加的键值对一律保存到ht[1]里,保证了ht[0]里的键值对数量只减不增。

php写时复制

背景:

我们定义两个变量a和b,并且他们的值都是1。站在内存角度考虑,希望他们公用一份内存。未来某个变量被修改时,再为其申请一份内存空间,这个操作名为写时复制。

<?php
    $foo = 1;
    xdebug_debug_zval('foo');
    $bar = $foo;
    xdebug_debug_zval('foo');
    $bar = 2;
    xdebug_debug_zval('foo');
?>
//-----执行结果-----
foo: (refcount=1, is_ref=0)=1
foo: (refcount=2, is_ref=0)=1
foo: (refcount=1, is_ref=0)=1

分解来看:

1.第一个debug输出,定义foo变量,在内存开辟一块空间,foo变量指向那块空间;

2.第二个debug输出,将foo赋给bar变量。注意此处并不为bar申请新空间,而是将bar指向最开始开辟的那块空间。即此时foo和bar同时指向同一块空间;

3.第三个debug输出,由于bar的值为修改,所以为bar申请一块新的内存空间。

注意引用

显示引用会破坏写时复制,举个例子:

<?php
$foo['love'] = 1;
$bar  = &$foo['love'];
$tipi = $foo;
$tipi['love'] = '2';
echo $foo['love'];

最后输出结果为2。因为bar的引用,使foo[‘love’]变成了引用,从而使Zend没有对tipi的修改产生内存的复制分离。

参考资料

写时复制(Copy On Write)