游乐游手机版
首页/业界动态/文章详情

访问者模式:可以在不改变类的前提下给它加功能?访问者模式太神奇了!

时间:2026-04-14 22:10
访问者模式:优雅地分离数据结构与操作 咱们今天聊聊软件设计里一个经典难题:当你的业务对象(或者叫“数据结构”)已经稳定成型,但后续需要不断为它添加五花八门的新操作时,该怎么办?一种笨方法就是,每来一个新需求,就吭哧吭哧地去修改那些“祖宗级”的类。但这么干,麻烦可就大了。 访问者模式,就是为了优雅地解

访问者模式:优雅地分离数据结构与操作

咱们今天聊聊软件设计里一个经典难题:当你的业务对象(或者叫“数据结构”)已经稳定成型,但后续需要不断为它添加五花八门的新操作时,该怎么办?一种笨方法就是,每来一个新需求,就吭哧吭哧地去修改那些“祖宗级”的类。但这么干,麻烦可就大了。

访问者模式,就是为了优雅地解决这个问题而生的。它的核心思想,就是把“数据结构”(比如你的商品)和“操作”(比如生成各种格式的报表)彻底分开。这样一来,你想增加新操作时,完全不用碰原来的数据结构,只需要专心写一个新的“访问者”就行,然后让数据结构去“接受”这个访问者来拜访。

从一个电商系统的例子说起

假设你正在开发一个电商系统,里面有几种不同类型的商品:

  • 书籍 (Book):有书名、页数。
  • 水果 (Fruit):有名称、重量。
  • 电子产品 (Electronic):有型号、保修期。

第一个需求来了:给所有商品生成一个展示用的HTML详情页。这简单,你在每个商品类里都加一个toHtml()方法就搞定了。

下个月,产品经理说还需要能导出XML格式的报表。好吧,你搓搓手,又得去每个类里加一个toXml()方法。

再下个月,前端说要JSON格式的数据接口……得,你又要去改那三个类,周而复始。

问题一下子就暴露了:

  • 违反开闭原则:每次加一种新格式,都不得不打开并修改所有现有的商品类,这无异于在稳定的核心代码上反复动手术。
  • 职责混乱:商品类的本职应该是管理自己的核心数据(比如书名、重量),现在却被迫掺和进了数据展示和格式转换的活儿,变得臃肿不堪。

这时候,访问者模式就该出场了。它告诉你:别折腾那些商品类了。你需要什么新操作(比如导出XML),就专门写一个“XML访问者”,然后让商品们“接待”一下这位访问者,任务就完成了。

一、PHP 8.1+ 实战演示

1. 元素接口 (Element)

首先,我们得定个规矩。所有商品都必须实现这个统一的“元素”接口。它的核心就是一个accept方法,用来迎接访问者。

2. 具体元素 (Concrete Element)

接下来,看看具体的商品类怎么实现。这里用到了一个叫双重分派的精妙技巧。注意看$visitor->visitBook($this)这行:

  1. 第一重分派:客户端调用$product->accept($visitor),把访问者“请进门”。
  2. 第二重分派:商品对象(比如Book)自己很清楚“我是谁”,所以它立刻调用了访问者身上专门为自己准备的方法——visitBook($this),并把自身引用传递过去。
class Book implements Product
{
    public function __construct(
        public string $title,
        public int $pages
    ) {}

    public function accept(Visitor $visitor): void
    {
        // 我知道我是 Book,所以我调用 visitBook
        $visitor->visitBook($this);
    }
}

class Fruit implements Product
{
    public function __construct(
        public string $name,
        public float $weight
    ) {}

    public function accept(Visitor $visitor): void
    {
        // 我知道我是 Fruit,所以我调用 visitFruit
        $visitor->visitFruit($this);
    }
}

3. 访问者接口 (Visitor)

访问者接口定义了它能为哪些类型的“客人”(元素)提供服务。每种元素都有一个对应的访问方法。

interface Visitor
{
    public function visitBook(Book $book): void;
    public function visitFruit(Fruit $fruit): void;
}

4. 具体访问者 (Concrete Visitor)

重头戏在这里。所有具体的操作逻辑,都被封装在各个访问者类中。想加新功能?直接新建一个访问者类就行,原有代码纹丝不动。

// 导出 XML 的逻辑
class XmlExportVisitor implements Visitor
{
    public function visitBook(Book $book): void
    {
        echo "{$book->title}

{$book->pages}\n"; } public function visitFruit(Fruit $fruit): void { echo "{$fruit->name}{$fruit->weight}\n"; } } // 导出 JSON 的逻辑(想加新功能?加个新类就行!) class JsonExportVisitor implements Visitor { public function visitBook(Book $book): void { echo json_encode(['type' => 'book', 'title' => $book->title]) . "\n"; } public function visitFruit(Fruit $fruit): void { echo json_encode(['type' => 'fruit', 'name' => $fruit->name]) . "\n"; } }

5. 客户端调用

最后,看看客户端如何使用这套机制,优雅地完成不同格式的导出任务。

$products = [
    new Book("PHP 核心技术", 500),
    new Fruit("苹果", 1.5),
    new Book("设计模式", 300)
];

// 1. 导出 XML
echo "--- Exporting XML ---\n";
$xmlVisitor = new XmlExportVisitor();
foreach ($products as $product) {
    $product->accept($xmlVisitor);
}

// 2. 导出 JSON
echo "\n--- Exporting JSON ---\n";
$jsonVisitor = new JsonExportVisitor();
foreach ($products as $product) {
    $product->accept($jsonVisitor);
}

// 输出示例:
// --- Exporting XML ---
// PHP 核心技术...
// 苹果...
// ...
// --- Exporting JSON ---
// {"type":"book","title":"PHP 核心技术"}
// {"type":"fruit","name":"苹果"}
// ...

二、一个至关重要的细节与取舍

访问者模式并非万能灵药,它有一个非常显著的特点(或者说缺点):增加新的元素类型会非常困难

想象一下,如果你的系统突然要加入一个新的Electronic(电子产品)类。那么,你不仅要修改Visitor接口(增加一个visitElectronic方法),还必须回过头去修改所有已经写好的具体访问者类(比如XmlExportVisitorJsonExportVisitor),让它们实现对这个新产品的处理逻辑。

所以,这就引出了访问者模式的经典适用场景:数据结构非常稳定,但操作(算法)需要频繁扩展和变化。编译器就是个绝佳的例子——编程语言的语法规则(数据结构)几乎不变,但针对语法树的分析、优化、转换等操作(访问者)却层出不穷。报表系统也是类似,数据模型固定,但导出格式、统计规则常变。

三、什么时候该考虑用它?

总结一下,当你在项目中遇到以下几种情况时,访问者模式值得放入备选方案:

  1. 你的对象结构由许多不同接口的类对象组成,而你想要执行一些依赖于这些对象具体类型的操作。
  2. 你需要在对象结构上执行许多彼此无关的不同操作,并且不希望这些操作把对象本身的类弄得乱七八糟。
  3. 对象结构本身(类的种类)很少改变,但你却需要经常在这个结构上定义新的操作。

四、总结与延伸思考

访问者模式,用一句话概括就是:封装一些作用于某种数据结构中各元素的操作,它可以在不改变该数据结构的前提下,定义作用于这些元素的新操作。

它的核心价值在于扩展操作。通过将数据结构与数据操作解耦,增加新操作变得像搭积木一样简单——新建一个访问者类即可。

最后,留一个有趣的思考题:PHP本身并不支持基于参数类型的真正方法重载。如果PHP支持重载,我们或许就不需要用visitBookvisitFruit这样区分方法名,而可以统一用visit(Book $b)visit(Fruit $f)。如果真是这样,你觉得accept方法里的双重分派逻辑会发生什么变化?这背后又涉及到静态分派与动态分派哪些微妙的区别呢?

来源:https://www.51cto.com/article/838679.html
上一篇苹果占据半壁江山!全球畅销智能手机TOP10榜单出炉 下一篇国产“汽车大脑”来了:首款国产车规级MCU芯片DF30量产上车
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
长安汽车明年一季度发布首款车载人形机器人小安
业界动态 · 2026-06-29

长安汽车明年一季度发布首款车载人形机器人小安

长安汽车公布机器人战略,采用“1+N+X”布局,联合头部伙伴攻克大脑、能源、驱动技术。人形机器人“小安”身高169cm,体重69kg,移动速度0 8m s,具备40个自由度,续航超2小时。预计明年一季度发布首款车载组件机器人,已在广州车展展示。

中国信科刷新光通信世界纪录 每秒可下载1.4万部4K电影
业界动态 · 2026-06-29

中国信科刷新光通信世界纪录 每秒可下载1.4万部4K电影

3月25日,光通信领域迎来又一个里程碑:中国信科集团光通信技术和网络全国重点实验室联合鹏城实验室、烽火藤仓光纤科技有限公司,成功实现了2 5Pb s 24芯光纤超大容量实时光传输,再次刷新了世界纪录。 这一研究成果不仅入选国际顶级光通信会议OFC(2026)并荣获“高分论文”称号,还受国际权威SCI

美国调查18万辆特斯拉Model3车门应急释放装置易找性
业界动态 · 2026-06-29

美国调查18万辆特斯拉Model3车门应急释放装置易找性

美国国家公路交通安全管理局对约17 9万辆2024款特斯拉Model3启动缺陷调查,焦点在于车门应急释放装置是否不易找到且标识不清。该调查源于一份缺陷请愿,不意味着立即召回,但可能引发后续监管措施。

doc个人图书馆停服 创始人称无偿转让失败
业界动态 · 2026-06-29

doc个人图书馆停服 创始人称无偿转让失败

运营长达20年,累计服务8000万用户的360doc个人图书馆,最终还是迎来了谢幕时刻。2026年5月1日,这个承载着无数用户收藏记忆的知名平台将正式停止服务——关停原因并非用户流失,而是始终未能寻得一位能够安全接管的合适人选。 创始人蔡智在告别信中坦言,近两个月来,他一直在尝试将360doc无偿转

年Q1随身WiFi实测安全靠谱高性价比机型推荐
业界动态 · 2026-06-29

年Q1随身WiFi实测安全靠谱高性价比机型推荐

2025年10月,艾瑞咨询正式授予飞猫“AI WiFi品类开创者”认证,紧接着CIC也将其认定为“多网融合自由切换技术服务首创者”。这些权威认证背后,折射出一个清晰的市场趋势:移动办公、户外出行、宿舍上网等场景的需求正在快速增长,随身WiFi几乎已成为不少用户的刚需装备。但问题也随之而来——网络卡顿