ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

适配器模式:为什么设备软件里不能直接调控制卡 API

适配器模式:为什么设备软件里不能直接调控制卡 API 这篇解决一个问题设备软件里为什么要包一层Axis、Cylinder、MotionObj而不是直接调控制卡的MoveTo、ReadBit。从最直觉的写法开始一步步讲清适配器模式在设备软件里到底解决什么。一、先从一个生活例子说起你买了一个港版 iPhone充电器是英标三脚插头。但你家墙上是国标两脚插座。插不进去怎么办你有三个选择把墙上的插座拆了换成英标——成本太高而且别的电器用不了。买一个新手机——更荒谬。买一个转换头——一头插英标一头插国标便宜好用。这个转换头就是适配器。适配器的本质让两个接口不兼容的东西能配合使用而不改任何一方。在代码里这个「转换头」就是适配器模式。二、回到代码为什么要适配器设备软件里最常见的场景你有一张运动控制卡DMC5812、雷赛、固高卡厂商提供了 SDK。你的业务代码要控制电机移动。最直觉的写法直接调 SDK#include DMC5812.h void Arm::PickStation() { DMC5812_MoveTo(0, 1, 100.5); // 卡号0轴1移动到100.5 DMC5812_WaitArrive(0, 1, 10000); // 等到位超时10秒 DMC5812_WriteOutput(0, 5, 1); // 开真空位5 Sleep(300); if (DMC5812_ReadInput(0, 6) 0) // 读真空检测位6 { AfxMessageBox(真空失败); return; } DMC5812_MoveTo(0, 1, 0); // Z轴回安全位 }能跑但问题一大堆。问题一换卡就全完蛋明天老板说「省钱换雷赛卡」。你要把所有DMC5812_MoveTo改成LeiSai_MoveTo所有DMC5812_ReadInput改成LeiSai_ReadDI。一台设备几百处调用改到天亮还可能漏。问题二业务代码知道太多硬件细节PickStation是业务逻辑取料动作但它知道卡号是 0、轴号是 1、真空阀在位 5、检测在位 6。这些硬件接线细节泄露到了业务层。换个机型接线变了业务代码要改。问题三没法测试想测PickStation的逻辑必须插着真卡。没有卡就跑不了因为DMC5812_MoveTo没卡会崩。单元测试无从下手。问题四SDK 函数风格不统一DMC5812 是DMC5812_MoveTo(card, axis, pos)雷赛是LeiSai_Move(axis, pos)没有卡号固高是GT_AxisMove(axis, pos, vel)多个速度参数。直接调 SDK换一家卡函数签名全变了适配代码改不完。.三、适配器模式怎么解决思路很简单在业务代码和 SDK 之间加一层「转换头」。业务代码只调转换头的接口转换头内部调 SDK。业务代码 ──→ 适配器Axis/Cylinder ──→ SDKDMC5812/雷赛/固高业务代码不知道底下是什么卡适配器知道。换卡只改编译器层业务代码一行不改。第一步定义统一的抽象接口先定义「一根轴应该能做什么」跟具体卡无关class IAxis { public: virtual bool MoveTo(double pos) 0; virtual bool WaitArrive(int timeoutMs 10000) 0; virtual bool Home() 0; virtual double GetPosition() 0; virtual ~IAxis() default; };这是「目标接口」——业务代码期望的接口。不管底下什么卡轴都能MoveTo、WaitArrive、Home。第二步为每种卡写一个适配器// DMC5812 的适配器 class DmcAxis : public IAxis { int m_cardId; int m_axisId; public: DmcAxis(int card, int axis) : m_cardId(card), m_axisId(axis) {} bool MoveTo(double pos) override { return DMC5812_MoveTo(m_cardId, m_axisId, pos) 0; } bool WaitArrive(int timeoutMs) override { int t 0; while (DMC5812_GetStatus(m_cardId, m_axisId) ! 1) // 还没到位 { Sleep(10); t 10; if (t timeoutMs) return false; } return true; } bool Home() override { return DMC5812_Home(m_cardId, m_axisId) 0; } double GetPosition() override { return DMC5812_GetEncoder(m_cardId, m_axisId); } };// 雷赛的适配器 class LeiSaiAxis : public IAxis { int m_axisId; public: LeiSaiAxis(int axis) : m_axisId(axis) {} bool MoveTo(double pos) override { return LeiSai_Move(m_axisId, pos) 0; // 雷赛没有卡号 } bool WaitArrive(int timeoutMs) override { int t 0; while (LeiSai_IsMoving(m_axisId)) // 雷赛用 IsMoving 判断 { Sleep(10); t 10; if (t timeoutMs) return false; } return true; } // ... };第三步业务代码只调抽象接口class Arm { IAxis* m_xAxis; IAxis* m_zAxis; public: void SetAxis(IAxis* x, IAxis* z) { m_xAxis x; m_zAxis z; } void PickStation() { m_xAxis-MoveTo(100.5); m_xAxis-WaitArrive(); m_zAxis-MoveTo(50.0); m_zAxis-WaitArrive(); // 开真空、检测... } };Arm不知道底下是 DMC5812 还是雷赛。它只知道「我有个轴能MoveTo、能WaitArrive」。第四步初始化时注入具体适配器// 用 DMC5812 auto* xAxis new DmcAxis(0, 1); // 卡0轴1 auto* zAxis new DmcAxis(0, 2); // 卡0轴2 arm-SetAxis(xAxis, zAxis); // 换雷赛了只改这里 auto* xAxis new LeiSaiAxis(1); // 轴1 auto* zAxis new LeiSaiAxis(2); // 轴2 arm-SetAxis(xAxis, zAxis);业务代码一行不改只改初始化时new的具体适配器。这就是适配器模式的价值。四、适配器模式解决了什么把前面的问题逐个对照问题不用适配器用适配器换卡改几百处 SDK 调用只改初始化new的适配器硬件细节泄露业务代码知道卡号、轴号、位号业务代码只知道MoveTo(pos)没法测试必须插真卡可以 mockIAxis做单元测试SDK 风格不统一每家卡签名不同适配器内部翻译对外统一接口一句话适配器把「硬件厂商的接口」翻译成「业务代码期望的接口」让两边各自独立变化。五、适配器模式的标准结构角色例子Target目标接口IAxis业务代码期望的统一接口Adaptee被适配者DMC5812/LeiSaiSDK已有的、不能改的接口Adapter适配器DmcAxis/LeiSaiAxis实现 Target内部调 AdapteeClient调用方Arm只依赖 Target不知道 AdapteeClientArm│ 调 IAxis::MoveTo▼TargetIAxis▲ 实现AdapterDmcAxis│ 内部调▼AdapteeDMC5812 SDK关键Client 只认 Target不认 Adaptee。Adapter 负责翻译。六、设备软件里的真实场景场景一运动控制卡适配前面讲的IAxis/DmcAxis/LeiSaiAxis就是真实场景。一台设备可能用 DMC5812 做运动、用雷赛做 IO、用固高做伺服。每家 SDK 函数名、参数、返回值都不一样。适配器层把它们统一成IAxis、ISensor、ISwitch业务代码只调统一接口。场景二传感器和开关的适配传感器和电磁阀也是一样。不同卡的 IO 读写函数不同// 抽象接口 class ISensor { public: virtual bool Read() 0; // 读传感器状态 }; class ISwitch { public: virtual void Write(bool on) 0; // 写开关 }; // DMC5812 适配器 class DmcSensor : public ISensor { int m_card, m_bit; public: DmcSensor(int card, int bit) : m_card(card), m_bit(bit) {} bool Read() override { return DMC5812_ReadInput(m_card, m_bit) ! 0; } }; class DmcSwitch : public ISwitch { int m_card, m_bit; public: DmcSwitch(int card, int bit) : m_card(card), m_bit(bit) {} void Write(bool on) override { DMC5812_WriteOutput(m_card, m_bit, on ? 1 : 0); } };业务代码class Picker { ISensor* m_vacuumSen; ISwitch* m_vacuumValve; public: void OpenVacuum() { m_vacuumValve-Write(true); } bool CheckVacuum() { return m_vacuumSen-Read(); } };Picker不知道真空阀在哪个卡哪个位只管Write(true)和Read()。换卡只换适配器。场景三测试机通信协议适配测试机有多种通信协议串口、网口、GPIB。每家测试机厂商的指令格式不同。// 抽象接口 class ITester { public: virtual bool SendCommand(const string cmd) 0; virtual string ReadResult() 0; }; // 串口测试机适配器 class SerialTester : public ITester { SerialPort* m_port; public: bool SendCommand(const string cmd) override { return m_port-Write(cmd \r\n) 0; } string ReadResult() override { return m_port-ReadLine(); } }; // 网口测试机适配器 class TcpTester : public ITester { TcpClient* m_client; public: bool SendCommand(const string cmd) override { return m_client-Send(cmd); } string ReadResult() override { return m_client-Recv(); } };业务代码class TestFlow { ITester* m_tester; public: TestResult RunTest() { m_tester-SendCommand(TEST:START); string result m_tester-ReadResult(); return ParseResult(result); } };换测试机只换适配器测试流程不改。场景四扫码枪适配扫码枪有串口版、USB版、网口版通信方式不同但业务要的一样触发扫码、读结果。class IScanner { public: virtual bool Trigger() 0; virtual string ReadCode(int timeoutMs) 0; }; class SerialScanner : public IScanner { ... }; class UsbScanner : public IScanner { ... }; class TcpScanner : public IScanner { ... };业务代码只调Trigger()ReadCode()换扫码枪只换适配器。七、适配器 vs 直接封装有什么区别有人会说「这不就是封装吗有什么高级的」。区别在于目的。普通封装把一段代码包成函数方便复用。目的只是「整理代码」。适配器模式把一个已有接口翻译成另一个接口让不兼容的两边能配合。目的是「解决接口不兼容」。维度普通封装适配器模式目的整理代码、复用解决接口不兼容场景一段逻辑写太多抽函数两个接口对不上加转换层关键函数签名自己定Target 和 Adaptee 都已存在Adapter 要同时满足两边例子把 50 行取料逻辑包成DoPick()DMC5812 的MoveTo翻译成IAxis::MoveTo适配器的标志是Target 接口和 Adaptee 都已经存在Adapter 在中间翻译。不是从零设计接口而是让已有的两边能配合。八、适配器 vs 策略别搞混这两个模式都用了「接口 多实现」的结构容易混。维度适配器模式策略模式目的让不兼容接口能配合让同一件事按不同规则做谁主动Client 持有 TargetContext 持有 Strategy变化点Adaptee 可能换换卡Strategy 可能换换规则时机初始化时选一个适配器可能运行时换策略例子DMC5812 适配成 IAxis上料策略换成下料策略简单记适配器是「翻译官」策略是「可换的算法」。适配器解决「接口对不上」策略解决「规则会变」。九、适配器的坑坑一适配器层太薄形同虚设class DmcAxis : public IAxis { bool MoveTo(double pos) override { return DMC5812_MoveTo(m_card, m_axis, pos) 0; // 直接转发 } };如果适配器只是把 SDK 函数包一层、参数原样转发没有做任何翻译和统一那它的价值很有限。好的适配器应该做这些事统一参数含义比如 DMC 用毫米、雷赛用脉冲适配器要转换单位。统一返回值DMC 返回 0 表示成功雷赛返回 1 表示成功适配器统一成bool。统一超时处理有的 SDK 自带超时有的没有适配器补上。统一错误码把各家不同的错误码翻译成统一枚举。坑二适配器层太厚变成业务层class DmcAxis : public IAxis { bool MoveTo(double pos) override { // 检查安全 if (pos m_maxLimit) return false; // 检查互斥 if (m_isBusy) return false; // 记日志 Log(MoveTo to_string(pos)); // 通知 UI pDlg-UpdateAxisStatus(); // 调 SDK return DMC5812_MoveTo(m_card, m_axis, pos) 0; } };适配器里塞了安全检查、互斥、日志、UI 刷新——这些是业务逻辑不是适配器的活。适配器应该只做「翻译」不做「决策」。对策安全检查放业务层日志放业务层或独立模块适配器只翻译接口。坑三漏适配某些 SDK 功能DMC5812 有回零、限位、伺服使能等功能适配器只实现了MoveTo和WaitArrive其它功能没适配。业务代码要用回零时发现接口里没有。对策设计 Target 接口时先盘点所有卡需要的功能一次定义全。后面发现缺了再加但要保证所有适配器都实现。坑四多个卡混用适配器没统一设备里可能同时用 DMC 做运动、用雷赛做 IO。如果运动适配器和 IO 适配器的风格不统一一个返回bool一个返回int业务代码还是要写两套判断。对策所有适配器的 Target 接口风格统一——返回值统一、错误处理统一、超时参数统一。十、可复用结论适配器模式的本质在两个不兼容的接口之间加一层翻译让它们能配合而不改任何一方。设备软件里最典型的应用Axis/Cylinder/SensorObj/Picker都是适配器把各家控制卡 SDK 翻译成统一接口。三个角色Target业务期望的接口、Adaptee已有的 SDK、Adapter中间翻译层。核心价值换卡只改适配器层业务代码一行不改可以 mock 接口做单元测试。适配器只翻译不决策安全检查、互斥、日志、UI 刷新是业务层的事别塞进适配器。和策略模式的区别适配器解决「接口对不上」策略解决「规则会变」。适配器是翻译官策略是可换的算法。设备软件里适配器模式不是「教科书练习题」而是「换卡不改业务」的实打实手段。没有适配器层每台设备的代码都和具体控制卡绑死换卡就是重写。有了适配器层核心业务代码跨设备、跨卡复用适配器层按需替换。这就是为什么所有成熟的设备软件框架都有一层硬件抽象——它就是适配器模式的工程化落地。
返回列表