工厂模式是面向对象设计中最常用的创建型模式之一。它的核心思想很简单:把实例化对象的活儿交给一个专门的类去做,调用方只管要结果,不关心对象是怎么new出来的。在PHP项目里,无论是封装数据库驱动、支付渠道,还是处理不同格式的文件解析,工厂模式都能派上用场。这篇文章会用具体的PHP代码,把简单工厂、工厂方法、抽象工厂三种形态讲透,并说说各自的取舍。

为什么需要工厂模式
先看一段典型的坏味道代码。假设系统需要支持多种缓存,调用处直接写死了实现类:
$cache = new RedisCache();
$cache->set('user_1', $data);
这段代码的问题在于,调用方和具体实现类RedisCache紧紧绑在了一起。哪天要把Redis换成Memcached,你就得全局搜索,把每一处new RedisCache()都改一遍。如果这个调用发生在几十个文件里,改动的代价非常高,而且很容易漏改。
工厂模式就是为这个问题而生的。它引入一个中间角色,把创建逻辑集中到一处。调用方只依赖工厂暴露的接口,具体实例化哪个类,只有工厂自己知道。这样即便将来实现类换了,也只需要改工厂内部的一行代码,调用方完全无感知。这种解耦带来的另一个好处是便于测试:单元测试时可以让工厂返回一个假对象,避免测试代码真的去连Redis。
简单工厂:最直接的封装方式
简单工厂严格来说不是GoF定义的标准模式,但它是理解工厂模式的最佳起点。它的结构非常朴素:一个静态方法,根据传入的参数决定返回哪个类的实例。先定义一个缓存接口和两个实现:
interface Cache
{
public function set(string $key, $value): void;
public function get(string $key);
}
class RedisCache implements Cache
{
public function set(string $key, $value): void
{
// 实际项目中这里调用Redis扩展
echo "写入Redis: {$key}\n";
}
public function get(string $key)
{
return null;
}
}
class MemcachedCache implements Cache
{
public function set(string $key, $value): void
{
echo "写入Memcached: {$key}\n";
}
public function get(string $key)
{
return null;
}
}
然后编写简单工厂类:
class CacheFactory
{
public static function create(string $type): Cache
{
switch ($type) {
case 'redis':
return new RedisCache();
case 'memcached':
return new MemcachedCache();
default:
throw new InvalidArgumentException("不支持的缓存类型: {$type}");
}
}
}
// 调用方式
$cache = CacheFactory::create('redis');
$cache->set('user_1', ['name' => '张三']);
这段代码的关键点在于返回值类型声明: Cache。它强制工厂返回的对象都实现了统一接口,调用方拿到实例后可以放心调用set和get,不用做任何类型判断。另外注意default分支抛出异常,比静默返回null要安全得多,能让错误尽早暴露。
简单工厂的缺点也很明显:每新增一种缓存类型,都要修改工厂类的switch语句,违背了开闭原则。当产品类型不多、变化不频繁时,简单工厂完全够用;如果类型会持续增长,就该考虑工厂方法了。
工厂方法模式:让每个工厂只负责一个产品
工厂方法模式的做法是:不再用一个上帝工厂包揽所有创建逻辑,而是把工厂本身也抽象出来,让每种产品对应一个专属工厂。新增产品时,只需要加一个新的产品类和一个新的工厂类,原有代码一行不动。先看工厂侧的定义:
interface CacheFactoryInterface
{
public function createCache(): Cache;
}
class RedisCacheFactory implements CacheFactoryInterface
{
public function createCache(): Cache
{
return new RedisCache();
}
}
class MemcachedCacheFactory implements CacheFactoryInterface
{
public function createCache(): Cache
{
return new MemcachedCache();
}
}
调用方的用法变成这样:
function handleRequest(CacheFactoryInterface $factory)
{
$cache = $factory->createCache();
$cache->set('session_abc', ['uid' => 100]);
}
// 需要Redis时传入Redis工厂,需要切换时换一个工厂即可
handleRequest(new RedisCacheFactory());
注意handleRequest的参数类型是工厂接口而不是具体工厂,这就是依赖倒置的体现:高层逻辑依赖抽象,不依赖具体实现。想切换缓存方案,只需在入口处换掉传入的工厂实例,业务代码内部完全不用动。
工厂方法的代价是类的数量翻倍,每加一种产品要写两个类。对于小型项目来说可能显得繁琐,但在组件化程度高、需要插件式扩展的系统里,这种结构能带来极大的灵活性。Laravel的服务容器内部大量使用的就是这种思路,只是它用一个通用容器统一管理,省去了手写工厂类的过程。
抽象工厂:一次创建一族相关对象
如果系统里的对象是成组出现的,比如一套存储方案包含缓存和队列两个组件,Redis方案配RedisCache加RedisQueue,Memcached方案配MemcachedCache加MemcachedQueue,这时用简单工厂或工厂方法就要创建两次,还可能搭配出错。抽象工厂把这些相关对象的创建打包到一个工厂里:
interface Queue
{
public function push(string $job): void;
}
class RedisQueue implements Queue
{
public function push(string $job): void
{
echo "任务推入Redis队列: {$job}\n";
}
}
class MemcachedQueue implements Queue
{
public function push(string $job): void
{
echo "任务推入Memcached队列: {$job}\n";
}
}
interface StorageFactory
{
public function createCache(): Cache;
public function createQueue(): Queue;
}
class RedisStorageFactory implements StorageFactory
{
public function createCache(): Cache
{
return new RedisCache();
}
public function createQueue(): Queue
{
return new RedisQueue();
}
}
使用时,调用方从同一个工厂取缓存和队列,天然保证它们来自同一套方案,不会出现Redis缓存配Memcached队列的混搭错误。当整个产品族需要整体切换时,只需要替换工厂实例。
抽象工厂的短板在于扩展产品种类很痛苦。假设后来要给每套方案再加一个日志组件,就得修改抽象工厂接口以及所有具体工厂类。所以抽象工厂适合产品族结构稳定的场景,比如多数据库适配层、跨平台UI组件库,而不适合产品种类频繁增加的场景。
实战建议与常见坑
写工厂模式时有两个容易踩的坑。第一点是工厂里不要塞业务逻辑。工厂的职责只有创建对象,如果发现工厂方法里开始写数据校验、调接口之类的代码,说明职责已经越界,应该把这些逻辑挪到产品类或服务类里。第二点是参数校验要严格,简单工厂收到未知类型时果断抛异常,不要返回null或空对象,否则错误会延迟到很远的地方才爆出来,排查成本翻倍。
另外提一个进阶技巧:结合PHP的类名做动态实例化,可以让工厂代码更精简:
class CacheFactory
{
public static function create(string $type): Cache
{
$class = ucfirst($type) . 'Cache';
if (!class_exists($class)) {
throw new InvalidArgumentException("类 {$class} 不存在");
}
$instance = new $class();
if (!$instance instanceof Cache) {
throw new RuntimeException("{$class} 未实现Cache接口");
}
return $instance;
}
}
这种写法省去了switch分支,新增类型只要按命名规则建类即可。但要注意class_exists和instanceof双重校验不能省,否则外部传入的type字符串可能实例化出意料之外的类,留下安全隐患。如果type来自用户输入,务必用白名单过滤,绝不能直接拼类名。
总结一下选择思路:产品类型少且稳定,用简单工厂;产品会持续增加,或想让框架使用者自行扩展,用工厂方法;对象成族出现且需要整体切换,用抽象工厂。在现代PHP开发中,更常见的做法是直接使用Composer生态中的依赖注入容器,它本质上是一个功能更强的超级工厂,理解了本文的三种形态,再看Laravel、Symfony的容器实现就会轻松很多。