文章目录
- 一、代理模式(Proxy Pattern)概述
- 二、代理模式和观察者设计模式
- 三、模式结构
- 四、协作角色
- 五、实现策略
- 六、相关模式
- 七、示例
- 八、应用
一、代理模式(Proxy Pattern)概述
- 代理模式是一种设计模式,它通过引入一个代理类来代表真实的服务器类,从而在客户端和服务器之间提供了一个抽象层。
- 这种模式的核心目的是隐藏服务器的实现细节,使得客户端不需要关心服务器的具体位置和通信细节,从而简化了客户端的设计,并提高了系统的可扩展性和灵活性。
- 在代理模式中,客户端不直接与服务器通信,而是通过一个客户端代理来订阅服务器代理,服务器代理负责封装与抽象服务器的通信细节。当服务器有数据更新时,服务器代理会通知所有订阅的客户端代理。
- 代理模式的一个关键优势是它能很好地隔离主题,使得服务器的远程性对客户端透明。这样,客户端代码更加简洁,不必区分远程和本地服务器。同时,代理模式还封装了联系服务器的知识,如果通信媒介发生变化,需要更新的类更少。
总之,代理模式通过在客户端和服务器之间设置代理,隐藏了服务器的位置和通信细节,简化了客户端的设计,并提高了系统的可扩展性和灵活性。
二、代理模式和观察者设计模式
观察者设计模式(Observer Pattern)和代理设计模式(Proxy Pattern)是两种常用的设计模式,它们在某些方面有着相似之处,但也有各自独特的特点和应用场景。观察者模式和代理模式的关系和演变相关概述:
-
观察者模式(Observer Pattern)是一种设计模式,它允许多个观察者对象监听一个主题对象的状态变化。当主题对象状态发生变化时,会通知所有的观察者对象。这种模式通常用于实现分布式事件处理系统,多个客户端需要知道某个值或状态的变化。
-
代理模式(Proxy Pattern)在观察者模式的基础上增加了一个代理层,代理对象充当客户端和主题之间的中介。代理模式通常用于控制对主题的访问,可以用于远程对象通信、延迟初始化、访问控制等场景。
-
代理模式可以被视为观察者模式的分布式版本,它提供了类似于数据总线模式(Data Bus Pattern)的功能。在代理模式中,服务器和客户端最终直接连接,客户端需要预先知道如何联系服务器。
-
在代理模式的结构中,客户端代理(Client-side Proxies)订阅服务器端代理(Server-side Proxies),这些服务器端代理由具体的服务器(Concrete Servers)控制并发布数据。当具体的服务器调用发送(send)操作时,所有远程客户端代理都会被通知新数据。
-
代理模式的一个关键优势是它隔离了主题,使得服务器可能是远程的这一事实对客户端透明。客户端不需要区分远程和本地客户端,代理模式还封装了如何联系服务器的知识,如果通信媒介发生变化,需要更新的类更少。
-
代理模式的一个扩展是中介模式(Broker Pattern),它通过包含一个代理(Broker)——一个对客户端和服务器都可见的“对象引用仓库”——来扩展代理模式。代理模式通过代理解决了潜在的服务器远程性的透明性问题,以及隐藏和封装如何联系这些远程服务器的手段。
-
代理模式和观察者模式的实现策略在客户端方面是相似的。例如,抽象客户端对象订阅客户端代理,就像在观察者模式中,具体客户端订阅具体服务器一样。
-
代理模式和观察者模式的区别主要在于代理模式在抽象客户端和抽象主题之间添加了代理。代理模式的结构清晰地显示了它从观察者模式演变而来。
总结来说,观察者模式主要用于单个服务器与多个客户端之间的信息共享,而代理模式则在观察者模式的基础上增加了代理层,使得客户端可以透明地处理远程服务器。代理模式的出现是为了解决分布式系统中的一些特定问题,如远程服务器的透明性和通信细节的封装。在设计分布式系统时,根据系统的具体需求和约束,观察者模式可能会演变为代理模式。
三、模式结构
- 代理模式(Proxy Pattern)在图8-12中展示,它源自观察者模式(Observer Pattern)。代理模式涉及客户端代理(Client-side Proxies)订阅服务器端代理(Server-side Proxies),服务器端代理由具体服务器(Concrete Servers)控制发布数据。当有新数据时,客户端代理会通知它们的本地订阅者。
- 代理模式特别适用于在设计时已知对象位置的非对称分布体系结构中。
- 代理模式的目的是简化客户端的交互,通过代理类隔离了服务器可能是远程的知识,封装了如何联系服务器的知识。
- 代理模式具有可扩展性,通过使用订阅策略和仅在必要时发送消息来最小化通信带宽。
- 代理模式的结构包括抽象客户端(Abstract Client)、具体客户端(Concrete Client)**、客户端代理(Client-side Proxy)、抽象服务器(Abstract Server)、具体服务器(Concrete Server)、**服务器端代理(Server-side Proxy)、数据(Data)类等。
- 客户端代理负责从抽象服务器获取数据,并将其格式化和封装以便发送给客户端。
- 服务器端代理则负责封装抽象服务器,并管理来自客户端代理的远程订阅,当数据被推送到服务器端代理时,它会通知客户端代理。
- 代理模式的一个关键优势是它将如何联系服务器的知识封装到代理类中,这样如果通信媒介发生变化,需要更新的类就会减少。
四、协作角色
- 抽象客户端(Abstract Client):与客户端代理关联,可以调用订阅(subscribe)和取消订阅(unsubscribe)操作。包含一个接受(accept)操作,用于接收通过订阅所需的Data信息。
- 具体客户端(Concrete Client):抽象客户端的应用特定子类。通过子类化抽象客户端并添加应用特定的语义到新子类中,应用代理模式。
- 抽象代理(Abstract Proxy):提供处理客户端订阅和数据传递的一般机制。它通过组合聚合了零到多个Notification Handle对象(用于通知其客户端实例)和Data对象。它有两个子类:一个服务于应用客户端,一个服务于应用服务器。在客户端,代理的行为类似于观察者模式中的Abstract Subject类:客户端订阅以接收随后“推送”给它们的数据。在服务器端,客户端代理子类充当Server-side Proxy子类的(远程)客户端。
- 客户端代理(Client-side Proxy):作为远程服务器的本地“替身”,使用本地化的通知句柄,以便在从其关联的服务器端代理接收更新信息时,能够通知其本地客户端。必须解组消息,提取数据对象,并将其格式化为客户端本地格式。
- 服务器端代理(Server-side Proxy):封装了抽象服务器与通信媒介和协议的隔离。管理来自客户端代理对象的远程订阅,并在抽象服务器将数据“推送”给它时通知它们。服务器端代理必须将从具体服务器传递给它的数据对象重新格式化为网络格式,并组织消息发送给客户端代理。
- 抽象服务器(Abstract Server):作为信息的服务器,提供抽象客户端所需的信息。适当时,它将数据对象推送到服务器端代理,通过调用后者的发送(send)操作。
- 具体服务器(Concrete Server):抽象服务器的应用特定子类。通过子类化抽象服务器并添加应用特定的语义到新子类中,应用代理模式。
- 数据(Data):Data类包含了Abstract Server所知道的信息以及Abstract Client想要了解的信息。Data对象包含适当的值,可能会以值的形式与客户端共享,因为它至少有可能被传递到不同的地址空间。
- 通知句柄(Notification Handle):Notification Handle类存储每个客户端的信息,以便Abstract Proxy可以通知其客户端Data类中存储的值。
- 本地通知句柄(Local Notification Handle):Notification Handle的子类,用于客户端代理类。
- 远程通知句柄(Remote Notification Handle):Notification Handle的子类,用于服务器端代理类。
这些角色共同定义了代理模式中的协作机制,使得客户端和服务器端能够有效地进行通信和数据交换。
五、实现策略
本地侧实现策略:
- 与观察者模式(Observer Pattern)的实现策略类似,客户端对象通过抽象接口订阅代理服务。
- 代理作为客户端与服务器之间的中介,封装了客户端的请求并转发给服务器。
- 客户端代理对象需要知晓如何与服务器端代理进行通信以获取信息。
远程侧实现策略:
- 实现通常依赖于特定的通信协议。
- 服务器端代理负责封装接收到的消息,并通过通信系统将消息传递给远程客户端代理对象。
适用场景:
- 代理模式特别适用于在设计时就知道对象位置的非对称分布体系结构。
- 对于在设计时不知道服务器位置的对称分布体系结构,需要使用其他模式,如中介模式。
代理模式的核心优势:
- 隔离远程服务器:代理模式有效地隔离了客户端不需要知道服务器是否为远程服务器,简化了客户端代码。代理模式允许服务器部署在任何可访问的位置,而客户端无需关心服务器的具体位置。
- 封装通信细节:代理类封装了与服务器通信的细节,若通信介质发生变化,需要更新的类数量减少。
- 订阅策略:使用订阅策略只在必要时传输数据,而不是不断轮询数据,这可以减少总线带宽并提高系统性能。
缺点:
- 复杂性:代理模式可能会增加系统的复杂性,因为它引入了额外的抽象层次,并且需要创建代理类。
- 性能开销:代理的使用可能会引入性能开销,因为通信过程中的额外步骤,如消息的封装和解封装。
- 设计时知识:代理模式需要预先知道服务器的位置,尽管客户端不需要这样做。这意味着客户端代理需要知道如何订阅服务器代理,这在某些情况下可能限制了灵活性。
- 数据可能过时:如果代理不够频繁地更新其信息,客户端可能会收到过时的数据。
六、相关模式
- 观察者模式(Observer Pattern):代理模式可以被看作是观察者模式的分布式版本,提供了类似于数据总线模式(Data Bus Pattern)的功能。在这些模式中,服务器和客户端最终直接连接,要求客户端事先知道如何联系服务器。因此,这些模式更适合于对象位置在设计时已知的非对称分布体系结构。
- 数据总线模式(Data Bus Pattern):数据总线模式提供了一个共享数据的公共虚拟介质,代理模式可以看作是在处理器之间使用观察者模式。
- 远程方法调用模式(Remote Method Call Pattern):远程方法调用模式与代理模式类似,尽管它使用不同的底层基础设施。远程方法调用模式也是非对称的,因为客户端和服务器必须就服务的名称和它们在系统中的位置达成一致。
- 中介模式(Broker Pattern):中介模式可以被看作是代理模式的对称版本,它在客户端和服务器位置在设计时未知的情况下共享服务。中介模式通过包含一个对客户端和服务器都全局可见的“对象引用仓库”来扩展代理模式。
- 共享内存模式(Shared Memory Pattern):共享内存模式使用多端口内存来共享全局数据和发送消息,它是一种在处理器之间共享数据的简单方法,但不要求及时响应消息和事件。
这些模式通常在分布式系统设计中使用,以便在多个处理器或不同的地址空间之间共享数据和服务。代理模式特别关注隐藏服务器可能的远程性和隐藏联系远程服务器的方式,而中介模式则允许客户端动态地发现和调用服务。
七、示例
图8-13:代理模式示例结构
- 结构展示了四个节点:气体混合处理器(Gas Mixer processor)、安全监控处理器(Safety processor)、医疗输送处理器(Medical Delivery processor)和用户控制处理器(User Control processor)。
- 气体混合处理器包含了服务器,一个名为O2流量传感器的对象,
- 该服务器(O2 Flow Server Sensor)与服务器端代理类O2流量服务器代理(O2 Flow Server Proxy)相连。
- 服务器端代理聚合了对象ID,用作总线上节点的地址。
- 总线提供了物理手段,用于在不同处理器上运行的对象之间传递消息。
- 每个包含至少一个客户端的处理器也有一个O2流量客户端代理实例(O2 Flow Client Proxy),用于从O2流量服务器代理获取值。
- 气体混合器处理器包含O2流量传感器,它作为数据的服务器。O2流量传感器调用O2服务器代理的send()操作,将数据发送给所有注册的客户端。这是通过遍历通知句柄列表(持有用于消息传递的低级通信协议的对象ID)并向每个注册的客户端(O2流量客户端代理)发送消息来完成的。
图8-14:代理模式示例场景
- 场景展示了客户端如何订阅它们的客户端代理,以及客户端代理如何订阅服务器代理。
- 当O2流量传感器接收到更新时,它调用O2流量服务器代理的send()操作,该操作遍历通知句柄列表(为了节省空间未显示)并将数据发送给每个注册的客户端代理。
- 接收到的客户端代理随后遍历其客户端列表,最终将数据交付给客户端。
- 尽管send()操作按顺序遍历订阅者(远程通知句柄)列表,但由于不同地址空间中的对象通常是异步操作的,因此消息的到达顺序不能从发送顺序确定。
这个例子展示了如何在分布式系统中使用代理模式来管理不同处理器之间的通信。该模式通过使用代理来抽象服务器和客户端之间的细节,代理处理通信和数据交付的细节,允许客户端像与本地服务器交互一样与远程服务器交互。
八、应用
代理模式(Proxy Pattern)主要解决的问题是如何在客户端和服务端之间提供一个中介层,以控制这种访问,常见的应用场景包括远程代理、虚拟代理、保护代理和智能引用等。代理模式的核心是为其他对象提供一种代理以控制对这个对象的访问。
具体到嵌入式系统开发中,代理模式可以解决的问题有:
-
远程通信的封装:当嵌入式设备需要通过网络与远程服务器或其他设备进行通信时,代理可以作为客户端和服务端之间的中介,将远程调用封装起来,使得客户端可以像访问本地对象一样透明地使用远程服务。
当设计嵌入式设备进行远程通信的封装时,需要考虑的关键因素包括通信效率、数据安全性、错误处理机制以及系统资源的有效管理。下面是一些步骤和策略来实现远程通信的封装:
- 选择合适的通信协议:根据嵌入式设备的性能和网络环境,选择适当的通信协议,例如UART、TCP/IP、MQTT等。对于资源受限的嵌入式设备,可以考虑使用轻量级的通信协议。
- 定义通信接口:为远程服务定义清晰的接口,这样客户端可以通过这些接口与服务端进行交互。接口应包含必要的方法和数据结构,确保调用的一致性和可预测性。
- 实现代理客户端:编写代理客户端代码,它将封装网络通信的细节,如连接管理、数据序列化/反序列化、请求的发送和响应的接收等。
- 安全机制:实现加密和认证机制,如使用TLS/SSL进行数据传输的加密,以及OAuth或JWT进行身份验证和授权,保证数据在网络中的安全。
- 错误处理:为通信过程中可能出现的错误和异常情况设计合理的错误处理策略,包括超时、断线重连、数据校验失败等。
- 异步通信:考虑使用异步通信机制,如回调函数、事件机制或消息队列,以提高通信的效率,减少对设备主程序的阻塞。
- 资源管理:合理安排网络资源的使用,如连接池管理、内存优化和低功耗策略,以适应嵌入式设备的资源限制。
- 接口封装测试:编写测试用例验证接口封装的正确性和稳定性,确保在不同的网络条件和负载情况下都能可靠工作。
- 文档和示例:提供详细的API文档和使用示例,让其他开发者能够快速理解和集成远程通信的封装接口。
- 固件升级策略:为保证远程通信模块的长期可维护性,设计固件升级机制,以便在未来可以轻松部署新功能或修复安全漏洞。
通过采取这些步骤和策略,你可以为嵌入式设备设计一套稳健的远程通信封装体系,确保通信的可靠性、安全性和高效性。
-
资源访问控制:在处理器资源有限的嵌入式环境中,代理模式可以用来控制对重要资源的访问,如通过保护代理确保只有具有适当权限的代码才能访问某些关键资源。
在资源受限的嵌入式环境中,使用设计模式来控制对关键资源的访问是提高系统安全性和稳定性的重要手段。代理模式作为一种结构型设计模式,非常适合用来实现资源的访问控制。以下是使用代理模式进行资源访问控制的一般步骤和实现方法:
步骤1:定义资源接口
首先,定义一个资源接口,它规定了对资源进行操作的方法。
class IResource { public:virtual void Access() = 0;};
步骤2:实现真实资源类
然后,实现具体的资源类,这个类将包含实际的资源操作逻辑。
class RealResource : public IResource { public:void Access() override {// 实际的资源操作}};
步骤3:创建保护代理类
接下来,创建一个代理类,它实现了同样的资源接口,并在内部持有一个真实资源类的引用。代理类将负责检查调用者是否有权访问资源,并在有权的情况下委托给真实资源类。
class ProxyResource : public IResource { private:RealResource\* realResource;bool CheckAccess() {// 检查访问权限return true; // 假定权限检查通过}public:ProxyResource(RealResource\* resource) : realResource(resource) {}void Access() override {if (CheckAccess()) {realResource->Access();}}};
步骤4:使用代理控制资源访问
在系统中,使用代理类的实例来控制对真实资源的访问。客户端代码不再直接与真实资源类进行交互,而是通过代理类来实现访问控制。
RealResource realResource;
ProxyResource proxy(&realResource);
proxy.Access(); // 客户端通过代理访问资源
通过这种方式,代理模式能够在不改变客户端代码的情况下,为资源访问添加一层安全检查,保护关键资源不被未授权的访问。同时,它也方便了对权限逻辑的维护,因为所有的权限检查都集中在代理类中,而不是散布在各个客户端代码中。在嵌入式系统中这种严格的资源访问控制尤为重要,因为资源通常非常有限,且系统的稳定性和安全性要求高。
3. 延迟加载和初始化:虚拟代理允许延迟对象的创建和初始化,这在嵌入式系统中尤其有用,因为它可以节省宝贵的启动时间和内存资源,仅在实际需要时才加载重要的资源。
延迟加载(Lazy Loading)和初始化是一种设计模式,它可以在嵌入式系统开发中发挥重要作用,特别是在资源受限的情况下。使用虚拟代理(Virtual Proxy)设计模式可以实现这种延迟初始化的机制。
在嵌入式系统中实现延迟加载和初始化通常需要以下步骤:
- 定义资源或对象的接口:这个接口描述了客户端代码将会使用的方法和属性。
- 实现一个虚拟代理类:这个类实现了与实际对象相同的接口,但是它并不立即创建或初始化实际对象。而是在实际需要使用对象时,虚拟代理才会去创建和初始化实际对象。
- 实现实际对象类:这个类包含了真实的逻辑和数据,通常会消耗更多的内存和计算资源。它应该只在真正需要时被加载和初始化。
- 在虚拟代理中延迟实际对象的创建:代理类可以根据需要,比如某个方法被调用时,来延迟实际对象的创建和初始化。
- 使用虚拟代理代替实际对象:在客户端代码中,使用虚拟代理的实例来代替实际对象的引用。客户端代码不需要了解背后的延迟加载机制。
class Subject {
public:virtual void doSomething() = 0;
};
// 真实类
class RealSubject : public Subject {
public:void doSomething() override {std::cout << "Do something in real subject." << std::endl;}
};// 代理类
class ProxySubject : public Subject {
private:RealSubject* realSubject;public:ProxySubject() {realSubject = nullptr;}virtual void doSomething() override {if (realSubject == nullptr) {realSubject = new RealSubject();}realSubject->doSomething();}
};// 客户端
class Client {
public:static void main() {Subject* subject = new ProxySubject();subject->doSomething();}
};// 真实类
struct RealSubject {void doSomething() {printf("Do something in real subject.\n");}
};// 代理类
struct ProxySubject {struct RealSubject* realSubject;ProxySubject() {realSubject = NULL;}void doSomething() {if (realSubject == NULL) {realSubject = malloc(sizeof(struct RealSubject));new(realSubject) RealSubject();}realSubject->doSomething();}
};// 客户端
int main() {struct Subject* subject = malloc(sizeof(struct ProxySubject));new(subject) ProxySubject();subject->doSomething();return 0;
}
在嵌入式系统中应用延迟加载和初始化的好处包括:
- 减少启动时间:通过延迟不必要的对象初始化,可以缩短系统的启动时间。
- 节省内存资源:仅在实际需要时才创建对象,这可以减少静态和动态内存的占用。
- 提高系统响应性:对关键资源进行延迟加载,可以使得系统更加迅速地响应用户或事件。
在嵌入式系统中成功实现延迟加载和初始化,需要仔细分析系统的使用模式,确保在不影响用户体验的情况下,合理地进行资源管理和调度。同时,也要确保虚拟代理的设计和实现不会引入额外的复杂性和潜在的错误。
4. 接口和实现分离:代理模式提供了一种分离接口和实现的机制,这有助于在不更改客户端代码的情况下替换或更新系统组件,这在嵌入式设备的固件升级和维护中尤为重要。
接口和实现分离是设计高质量软件系统的核心原则之一,代理模式则是实现这一原则的有效方式。在嵌入式系统开发中,应用代理模式可以带来一系列的好处:
- 模块化和封装:通过定义清晰的接口,代理模式使得系统的各个部分可以独立地开发和测试,从而提高模块化水平。这种封装隐藏了实现的复杂性,使得修改和维护更加容易。
- 易于维护:在嵌入式系统中,硬件平台可能会变化,或者需求可能会发展。使用代理模式,只需要更换实现部分,而客户端代码不需要任何更改,这大大降低了维护成本。
- 固件升级:嵌入式设备往往需要远程升级固件。通过代理模式,可以在不停机的情况下动态替换系统的某些组件,从而实现平滑升级。
- 安全性增强:代理模式允许在真实对象和客户端之间加入一层安全代理,用于控制对特定组件的访问,从而提高系统的安全性。
- 性能优化:一个虚拟代理可以在需要时才创建真正的服务对象,从而延迟初始化,减少系统启动时的开销。
在实施代理模式时,主要步骤包括:
- 定义接口:创建一个接口以定义代理和真实对象共有的方法。
- 创建代理类:实现接口并在代理类中添加一个引用指向真实对象。
- 实现真实对象:真实对象也实现了同一接口,代理通常会在适当的时候调用真实对象的方法。
- 使用代理:客户端代码持有接口的引用,运行时可以透明地使用代理或真实对象。
当设计嵌入式软件时,需要考虑系统的资源限制、性能要求和安全性等因素。代理模式为您提供了额外的灵活性,以应对这些挑战,并确保软件能够有效适应不断变化的需求。在使用代理模式时,还应当注意减少因引入额外的抽象层而可能增加的系统开销,特别是在资源非常有限的嵌入式环境中。
5. 性能优化:代理可以缓存请求和响应,减少实际函数/方法调用的次数,这有助于优化系统性能,尤其在网络延迟敏感或带宽受限的嵌入式应用中。
代理模式可以通过缓存请求和响应来实现性能优化。当客户端调用代理类的方法时,代理类会先检查缓存中是否存在相应的请求和响应。如果存在,代理类会直接返回缓存中的响应。如果不存在,代理类会调用真实类的方法,并将请求和响应缓存在本地,从而降低对远程服务器的频繁访问需求,减少网络通信所需的时间和带宽消耗。。
例如,在一个网络应用中,客户端需要频繁访问一个远程服务器。如果直接调用远程服务器,每次调用都会产生网络延迟和带宽消耗。通过代理模式,代理类可以缓存远程服务器的响应。当客户端再次调用相同的请求时,代理类可以直接返回缓存中的响应,避免了再次访问远程服务器。
以下是使用代理缓存进行性能优化的一些关键步骤和考虑事项:
- 缓存策略选择:根据应用场景选择适当的缓存策略,如最少使用(LRU),先进先出(FIFO),或时间戳排序等。
- 数据一致性:设计合理的缓存更新和失效机制,确保缓存数据与实际数据的一致性。
- 存储介质:选择合适的存储介质来保存缓存数据,可以是内存、闪存或其他类型的持久化存储,同时考虑嵌入式设备的存储资源。
- 缓存大小:合理设计缓存大小,平衡存储资源和缓存效率。过大的缓存可能会占用过多的存储资源,而过小的缓存可能不会带来期望的性能提升。
- 线程安全:如果在多线程环境下使用缓存,需要确保缓存操作的线程安全性。
- 性能测试:对缓存机制进行性能测试,验证缓存是否真正提高了性能并达到了预期效果。
- 监控与维护:实施监控机制来跟踪缓存的命中率和效率,根据实际情况调整缓存策略。
代理缓存的实施需要结合具体的嵌入式系统及其应用场景来具体设计。例如,一个嵌入式设备可能具有有限的计算能力和存储资源,实施代理缓存时需要特别考虑如何最小化资源消耗同时最大化性能提升。此外,缓存数据的安全性也是一个重要考虑点,必须确保敏感数据不会被未经授权的用户访问。
综上所述,代理缓存是一种强大的性能优化手段,但必须谨慎设计和实施以确保系统的稳定性和数据的安全性。
总的来说,代理模式提供了一种有效的设计方法来解决嵌入式系统中的对象访问控制、资源管理和远程通信等问题,它有助于提高系统的灵活性、安全性和可维护性。