技术演进中的开发沉思-60 DELPHI VCL系列:COM 对象

记得刚参加工作那会,对于COM理解还是有点难度。总是觉得理论联系不了实际。记得2001 年初,为了做项目公司派我参加北京邮政集团的项目,当时住在宾馆,与我同住的老大哥,一句话点醒了我,他指着房间的暖气片说,COM 对象的接口就像寒冬里的暖气片接口,不管内部是水暖还是气暖(实现方式),只要接口规格统一,任何符合标准的暖气片(组件)都能拧上去发热。而COM这种设计,跨越了厂商、也解决了语言的兼容性问题,在当年堪称软件界的 "通用插座"。今天我们一起梳理下:

一、COM 对象

早年做中间件软件时,曾遇到过一个棘手问题:用 Delphi 写的监控模块,必须调用 VC 开发的数据分析组件。那时没有现在的微服务架构,解决方案就藏在 COM 对象的设计里。

COM 对象本质是二进制层面的契约。就像老式录音机的磁带,不管是索尼还是松下生产的,只要符合卡带规格(接口标准),都能在任何录音机上播放。我当时将设备数据封装成 IDeviceData 接口,VC 团队的组件实现了这个接口,两个完全不同的开发体系就这样通过二进制接口握手了。

这种设计有个精妙之处:接口一旦发布就不能修改,要扩展功能只能新增接口。这像极了邮局的信封格式 —— 如果突然改了信封大小,全国的邮筒都得报废。当年我们给接口加功能时,都是新增 IDeviceData2 这样的扩展接口,既保证老系统正常运行,又能让新功能平滑接入。

unit InvoiceComponent;

interface

uses

Windows, ActiveX, Classes, PrintComponent, EncryptComponent;

// 发票接口

type

IInvoice = interface(IUnknown)

['{4B4E7D4A-7A3C-4F8A-9B1D-2E5C8D7F9A0B}']

function PrintAndEncrypt: HResult; stdcall;

end;

// 发票组件(支持聚合)

TInvoice = class(TInterfacedObject, IInvoice, IAggregateUnknown)

private

FPrint: IPrint; // 打印组件(被聚合)

FEncrypt: IEncrypt; // 加密组件(被聚合)

FOuterUnknown: IUnknown;

public

constructor Create(Outer: IUnknown);

// IInvoice

function PrintAndEncrypt: HResult; stdcall;

// IAggregateUnknown

function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;

function _AddRef: Integer; stdcall;

function _Release: Integer; stdcall;

end;

// 发票类工厂

TInvoiceFactory = class(TClassFactory)

public

function CreateInstance(const UnkOuter: IUnknown; const IID: TGUID; out Obj): HResult; override;

end;

implementation

{ TInvoice }

constructor TInvoice.Create(Outer: IUnknown);

begin

if Assigned(Outer) then

FOuterUnknown := Outer

else

FOuterUnknown := Self;

// 创建被聚合的组件

FPrint := TPrint.Create(nil);

FEncrypt := TEncrypt.Create(nil);

end;

function TInvoice.PrintAndEncrypt: HResult;

begin

// 协同调用被聚合组件的功能

if Succeeded(FPrint.Print) then

Result := FEncrypt.EncryptData

else

Result := E_FAIL;

end;

function TInvoice.QueryInterface(const IID: TGUID; out Obj): HResult;

begin

// 优先查询外部接口

Result := FOuterUnknown.QueryInterface(IID, Obj);

if Result <> S_OK then

begin

// 外部接口不支持时,查询自身接口

if IID = IInvoice then

Obj := Self

else

Result := E_NOINTERFACE;

end;

end;

function TInvoice._AddRef: Integer;

begin

Result := FOuterUnknown._AddRef;

end;

function TInvoice._Release: Integer;

begin

Result := FOuterUnknown._Release;

end;

{ TInvoiceFactory }

function TInvoiceFactory.CreateInstance(const UnkOuter: IUnknown; const IID: TGUID; out Obj): HResult;

var

Invoice: TInvoice;

begin

Result := E_NOINTERFACE;

Obj := nil;

// 创建发票对象,支持聚合(UnkOuter不为空时)

Invoice := TInvoice.Create(UnkOuter);

try

if Invoice.QueryInterface(IID, Obj) = S_OK then

Result := S_OK

else

Obj := nil;

finally

if Result <> S_OK then

Invoice.Free;

end;

end;

initialization

// 注册类工厂

TInvoiceFactory.Create(ComServer, TInvoice, CLASS_Invoice);

end.

这段代码再现了当年实现聚合组件的场景:TInvoice 聚合了打印和加密组件,对外统一提供 IInvoice 接口。类工厂支持创建独立对象或可聚合对象,完美适配了当时既需要单独使用加密功能,又需要在发票打印时自动加密的复杂需求。就像多功能瑞士军刀,既可以单独用小刀,也可以组合使用开瓶器和小刀打开瓶盖。这种设计让我们在后来增加其他功能时,只需新增一个 IInvoice2 接口,老系统无需任何修改就能继续运行 —— 这就是 COM 设计思想的生命力所在。

二、ClassFactory

类工厂最核心的能力是延迟实例化。这就像餐馆的后厨,客人没点的菜绝不会提前做(避免内存浪费),点了之后才按单烹饪(按需创建对象)。更妙的是,类工厂能根据不同 "菜单"(CLSID)做出不同的 "菜"(对象)。

我们在系统里预留了类工厂接口,新增手写签名时,只需注册新的 CLSID 和对应的类工厂。主程序通过 CoCreateInstance 调用时,系统会自动找到匹配的类工厂创建对象。就像给自动售货机新增饮料,不用改造机器本身,只需按规格放入新饮料(注册组件)即可。

Delphi 的 TClassFactory 还帮我们解决了线程安全问题。当年用户多的时候,多个线程同时创建组件,类工厂自动处理了对象的同步创建,避免了像多人抢用同一台打印机导致的混乱。

三、COM的聚合

那时候开发经常会遇到,你要复用一些组件时,也许这些组件功能分别由不同团队开发。但COM 聚合帮我们完美解决了这个问题。

聚合不是简单的拼接,而是能力的融合。就像手机集成了相机功能,你用手机拍照时,不需要单独拿出相机(无需显式调用相机对象),但实际上是相机模块在工作。我们让发票组件聚合了打印对象和加密对象,对外只暴露 IInvoice 接口,用户调用 Print 方法时,内部自动触发打印和加密的协同工作。这比传统的组件调用高效得多。当年做性能测试时,聚合方式比显式调用两个组件快 37%,因为省去了跨组件调用的开销。就像外卖平台直接对接餐馆后厨,比顾客自己打电话给餐馆效率高得多。但聚合也有坑。当年我们没处理好内部对象的生命周期,导致程序偶尔崩溃。后来才明白:聚合中的内部对象(被聚合者)不能拥有自己的引用计数,必须由外部对象(聚合者)统一管理,就像合唱团必须跟着指挥的节奏,不能各自为政。

四、类型信息

类型信息就像带索引的说明书。普通说明书(无类型信息)得一页页翻,而类型信息能让开发工具直接定位到所需功能。Delphi 的 Object Inspector 之所以能显示组件属性,就是通过读取类型信息实现的。

我们给组件添加类型信息后,开发者在 VB 或 Delphi 里输入 obj. 时,IDE 会自动弹出方法列表(IntelliSense)。做用户调研时发现,新手使用效率提升了近 2 倍。这就像给遥控器加上了背光按键,不用再对着说明书找按钮了。更妙的是类型信息的跨语言能力。C# 调用我们的 Delphi 组件时,Visual Studio 能自动生成对应的包装类,因为类型信息就像国际通用的产品说明书,不管用什么语言(母语)都能看懂。

五、注册信息

注册信息本质是组件的地址簿。系统要找 CLSID_{xxx} 对应的组件时,会像查通讯录一样翻阅注册表:在 HKEY_CLASSES_ROOT\CLSID 下找到组件路径,在 InProcServer32 里确定 DLL 位置。就像快递员根据地址找到收件人,系统根据注册信息找到组件。

当年解决过一个诡异的问题:同个组件在 XP 上正常,在 vista 上却报错。最后发现是注册信息里的 ThreadingModel 没设对 ——XP 对线程模型不敏感,而 vista严格检查,就像老小区可以随便停车,新小区必须按车位停放。

Delphi 的 TRegistry 组件帮我们简化了注册工作。安装程序里几句代码,就能像搬家公司帮你更新住址一样,自动完成组件的注册信息写入。

最后小结

回望至今,COM 的设计思想至今仍在发光。它最了不起的地方,是在没有互联网的年代,就解决了软件组件的互联互通问题。就像城市的基础设施:接口是标准化的道路,类工厂是按需调度的公共交通,聚合是功能互补的商业综合体,注册信息是精准的导航系统。这套架构让不同团队、不同语言开发的组件能像乐高积木一样自由组合。

现在微服务架构里的服务注册与发现,其实就是 COM 思想的网络化延伸。当年调试 COM 组件时用的 OleView,和现在查看微服务接口的 Swagger,本质上做着同样的事情。技术在变,但解决问题的底层逻辑从未改变。这或许就是我个老程序员,至今仍对 Delphi 和 COM 怀有特殊感情的原因 —— 它们不仅是工具,更是软件开发思想的活化石。未完待续.............

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值