设计模式:对象生成(单例、工厂、抽象工厂) | BNDong
0%

设计模式:对象生成(单例、工厂、抽象工厂)

对象的创建有时会成为面向对象设计的一个薄弱环节。我们可以使用多种面向对象设计方案来增加对象的创建的灵活性。

  • 单例模式:生成一个且只生成一个对象实例的特殊类。
  • 工厂方法模式:构建创建者类的继承层级。
  • 抽象工厂模式:功能相关产品的创建。

单例模式

全局变量是面向对象程序员遇到的引发bug的主要原因之一。全局变量将类捆绑于特定的环境,破坏了封装。如果新的应用程序无法保证一开始就定义了相同的全局变量,那么一个依赖于全局变量的类就无法从一个应用程序中提取出来并应用到新的应用程序中。这些问题的产生也引发了单例模式的出现,保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式是最简单的设计模式之一,也是被使用较多的设计模式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

1
2
3
单例类对象可以被系统中的任何对象使用。
单例类对象不应该被储存在会被覆写的全局变量中。
单例类对象在系统中不应该超过一个。

首先,我们构建一个类 Preferences,先要求这个类无法从其自身外部来创建实例的类。实现这个只需定义一个私有的构造方法即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Preferences 
{
private $_props = array();

private function __construct() {} // 禁止从外部创建类实例

public function setProperty($key, $val) {
$this->_props[$key] = $val;
}

public function getProperty($key) {
return $this->_props[$key];
}
}

现在这个类不能从外部类创建实例,但是内部也没有对外提供获取实例的方法,所以这个类是不能用的。这里我们添加静态方法和静态属性来间接实例化对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Preferences
{
private $_props = array();
private static $_instance = null;

private function __construct() {}

public static function getInstance() {
if (empty(self::$_instance)) {
self::$_instance = new Preferences();
}
return self::$_instance;
}

public function setProperty($key, $val) {
$this->_props[$key] = $val;
}

public function getProperty($key) {
return $this->_props[$key];
}
}

这是一个最基本的单例模式,我们可以通过静态方法 getInstance 来获取 Preferences 类的实例对象,同时能够保证该对象在系统中是唯一的。测试一下:

1
2
3
4
5
6
7
8
9
$pref1 = Preferences::getInstance();
$pref1->setProperty('name', 'bndong');
var_dump($pref1);

unset($pref1);

$pref2 = Preferences::getInstance();
var_dump($pref2);
var_dump($pref2->getProperty('name'));

打印结果:

1
2
3
4
5
6
7
8
9
10
11
object(Preferences)[1]
private '_props' =>
array (size=1)
'name' => string 'bndong' (length=6)

object(Preferences)[1]
private '_props' =>
array (size=1)
'name' => string 'bndong' (length=6)

string 'bndong' (length=6)

可以看到 $pref1$pref2 的对象标识符相同,并且 $pref2 可以获取到 $pref1 写入的值,也就说明二者所指向的对象为一个。实现了单例模式的需求:

注意:

  1. 上面实现单例模式使用的是PHP,所以不需要考虑线程安全的问题, Java 实现单例模式就需要考虑线程安全问题了,可以使用 synchronized 状态修饰来进行控制,这里我不过多赘述,想了解的可以去网上查阅。
  2. 在PHP中,所有的变量无论是全局变量还是类的静态成员,都是页面级的,每次页面被执行时,都会重新建立新的对象,都会在页面执行完毕后被清空。所以PHP单例模式我觉得只是针对单次页面级请求时出现多个应用场景并需要共享同一对象资源时是非常有意义的。
  3. 单例和全局变量都可能被误用。因为单例在系统任何地方都可以被访问,所以它们可能会导致很难调试的依赖关系。如果改变一个单例,那么所有使用该单例的类都会受到影响。在这里,依赖本身不是问题。毕竟,我们在每次声明一个有特定类型参数的方法时,也就创建了依赖关系。问题是,单例对象的全局化的性质会使程序员绕过类接口定义的通信线路。当单例被使用时,依赖便会被隐藏在方法内部,而并不会出现在方法声明中。这使得系统中的依赖关系更加难以追踪,因此需要谨慎小心地部署单例类。

另外(无关紧要),由于项目毕竟是一个团队开发,我在实现核心类的单例模式时候一般对类外加一些限制:

  1. 使用 final 对类进行限制,防止类被继承或覆盖。
  2. 私有化 __clone() 防止克隆。

工厂方法模式

工厂方法模式也是一种常用的对象创建型设计模式,这种模式是用特定的类来处理实例化,通过依赖注入达到解耦,解决了当代码关注于抽象类型时如何创建对象实例的问题。

工厂方法模式是对简单工厂模式的改进。

简单工厂模式:专门定义一个类用来负责创建其他类的实例,被创建的实例通常都具有共同的父类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
abstract class ApptEncoder
{
abstract function encode();
}

class BloggsApptEncoder extends ApptEncoder
{
public function encode()
{
// TODO: Implement encode() method.
}
}

class MegaApptEncoder extends ApptEncoder
{
public function encode()
{
// TODO: Implement encode() method.
}
}

class CommsManager
{
const BLOGGS = 1;
const MEGA = 2;

private $_mode = 1;

public function __construct($mode)
{
$this->_mode = $mode;
}


public function getApptEncoder()
{
switch ($this->_mode) {
case self::BLOGGS :
return new MegaApptEncoder();
default:
return new BloggsApptEncoder();
}
}
}

$comms = new CommsManager(CommsManager::BLOGGS);
$apptEncoder = $comms->getApptEncoder();
$apptEncoder->encode();

我们通过简单工厂模式的结构可以发现:(适用于工厂类负责创建对象较少的情形)

  1. 工厂类中包含了所有实例的创建逻辑,一旦这个工厂不能工作,整个系统都会受到影响。
  2. 违背开放关闭原则,一旦添加新的工厂子类就不得不修改工厂类的逻辑。
  3. 如果添加新的方法会迫使我们要重复使用条件判断语句,维护麻烦,而且当条件语句蔓延到代码中,我们不应该感到乐观。

为了解决这些问题,衍生出了设计模式:工厂方法模式。

工厂方法模式的工厂类,不再负责所有类的实例创建,不会因为添加新的工厂子类而修改工厂类的逻辑,符合开放闭合原则。

CommsManager 重新制定为抽象类。这样可以得到一个灵活的父类,并把所有特定协议相关代码方法具体的子类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
abstract class ApptEncoder
{
abstract function encode();
}

class BloggsApptEncoder extends ApptEncoder
{
public function encode()
{
// TODO: Implement encode() method.
}
}

class MegaApptEncoder extends ApptEncoder
{
public function encode()
{
// TODO: Implement encode() method.
}
}

abstract class CommsManager
{
abstract function getA();
abstract function getB();
abstract function getApptEncoder();
}

class BloggsCommsManger extends CommsManager
{
public function getA()
{
// TODO: Implement getA() method.
}

public function getB()
{
// TODO: Implement getB() method.
}

public function getApptEncoder()
{
return new BloggsApptEncoder();
}
}

class MegaCommsManger extends CommsManager
{
public function getA()
{
// TODO: Implement getA() method.
}

public function getB()
{
// TODO: Implement getB() method.
}

public function getApptEncoder()
{
return new MegaApptEncoder();
}
}

这里简单介绍下上面代码的结构,理解了的请略过

  • 工厂类CommsManager 用于定义工厂子类
  • 工厂子类MegaCommsManagerBloggsCommsManager 用于创建每个产品对象
  • 产品类ApptEncoder 用于定义产品子类
  • 产品子类MegaApptencoderBloggsApptEncoder

可以看到在工厂类中增加了两个方法:getAgetB。这也是工厂方法模式所适用的一种情形, 如果只为创建子类就实现工厂方法模式就需要再考虑下了。套用我上级老大的一句话:永远不要为了模式而模式。

工厂方法模式,解决了许多简单工厂模式的问题,遵循开放闭合原则,可扩展。但是这种模式也形成了一种特殊的代码重复,因而不被一些人喜欢。并且添加新产品时,处理增加新“产品”类外,还要提供与之对应的具体工厂类,系统类的个数成对增加。工厂方法模式可以解决类的横向扩展,对于类的纵向扩展并不能有效解决,这也就引出了抽象工厂模式。

抽象工厂模式

抽象工厂解决了工厂方法纵向扩展问题,例如我们纵向增加了一些类:

如果使用工厂方法模式来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
abstract class CommsManager
{
abstract function getA();
abstract function getB();
abstract function getApptEncoder();
abstract function getTtdEncoder();
abstract function getContactEncoder();
}

class BloggsCommsManger extends CommsManager
{
public function getA()
{
// TODO: Implement getA() method.
}

public function getB()
{
// TODO: Implement getB() method.
}

public function getApptEncoder()
{
return new BloggsApptEncoder();
}

public function getTtdEncoder()
{
return new BloggsTtdEncoder();
}

public function getContactEncoder()
{
return new BloggsContactEncoder();
}
}

上面是使用工厂方法模式来实现的类图,有没有感觉这个模式一旦添加了新产品维护起来特别的麻烦,因为不仅要创建新产品的具体实现,而且为了支持它,还必须修改抽象创建者和它的每一个具体实现。

这种时候应该使用抽象工厂模式,抽象工厂模式是对工厂进行一次抽象。统一对象的获取接口,使用参数来决定返回的对象。修改下:(ps:感觉就是使用简单工厂模式对工厂方法模式再进行一次抽象)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
abstract class CommsManager
{
const APPT = 1;
const TTD = 2;
const CONTACT = 3;

abstract function getA();
abstract function getB();
abstract function make($flg);
}

class BloggsCommsManger extends CommsManager
{
public function getA()
{
// TODO: Implement getA() method.
}

public function getB()
{
// TODO: Implement getB() method.
}

public function make($flg)
{
switch ($flg) {
case self::APPT:
return new BloggsApptEncoder();
case self::TTD:
return new BloggsTtdEncoder();
case self::CONTACT:
return new BloggsContactEncoder();
default:
return new BloggsApptEncoder();
}
}
}

可以看到所有创建对象接口统一到 make() 方法,接口变得更加紧凑。但是这样也暴露出了一些简单工厂模式的弊端,如开放闭合原则。使用抽象工厂模式会使工厂类更容易维护,基类 CommsManager 可以提供默认的 make() 实现,子类可以实现自己的调用基类的。

关于工厂方法和抽象工厂的区别

  1. 工厂方法产出的是产品(实例),抽象工厂产出是接口(创建每个实例的接口)
  2. 工厂方法是产出单一对象,抽象工厂产出是一系列相关产品的对象

关于工厂方法和抽象工厂的适用:无论是什么样的设计模式,最切合当前业务场景的就是最适用的。个人理解,如果我们的产品是,奔驰、宝马、五菱宏光之类单一类别产品就比较适用工厂方法模式。但是有一天我们的产品除了汽车还需要添加华硕、联想、惠普等另一种类别产品的时候就要考虑使用抽象工厂模式了。抽象工厂方法更适用于对产品族的处理。

结语

终于抽出时间学习设计模式了o(╥﹏╥)o,边学习边写随笔,写东西有助于更好理解。好的设计模式运用能使整体代码结构更清晰,维护扩展性更高。感谢阅读!再会!

↓赏一个鸡腿... 要不,半个也行↓