需要按特定顺序调用功能的接口设计


24

任务是根据一些输入规范在设备内配置硬件。这应该通过以下方式实现:

1)收集配置信息。这可能在不同的时间和地点发生。例如,模块A和模块B都可以(在不同时间)向我的模块请求一些资源。这些“资源”实际上就是配置。

2)在明确不再需要更多请求之后,需要将给出所请求资源摘要的启动命令发送到硬件。

3)只有在此之后,才能(并且必须)对所述资源进行详细配置。

4)另外,只有在2)之后,才能(并且必须)将所选资源路由到已声明的调用方。


导致错误的一个常见原因,即使是写这个东西的我来说,也是错误地遵循了这个顺序。我可以采用什么命名约定,设计或机制来使界面对第一次看到该代码的人可用?


第1阶段是所谓的好discovery还是handshake
rwong

1
时间耦合是一种反模式,应避免。

1
问题的标题使我认为您可能对step builder模式感兴趣。
2014年

Answers:


45

这是一种重新设计,但是您可以防止滥用许多API,但没有可用的不应调用的方法。

例如,代替 first you init, then you start, then you stop

您的构造函数init是一个可以启动的对象,并start创建可以停止的会话。

当然,如果您一次只限制一个会话,则需要处理某人尝试创建一个已经激活的会话的情况。

现在,将该技术应用于您自己的情况。


zlib并且jpeglib都是遵循这个模式初始化两个例子。尽管如此,仍然需要大量文档来向开发人员传授该概念。
rwong 2014年

5
这是正确的答案:如果顺序很重要,则每个函数都会返回一个结果,然后可以调用该结果执行下一步。编译器本身能够强制执行设计约束。

2
这类似于步骤构建器模式;仅呈现在给定阶段有意义的界面。
2014年

@JoshuaTaylor我的答案是一个步骤构建器模式的实现:)
Silviu Burcea 2014年

@SilviuBurcea 您的答案不是步骤构建器的实现,但我会在此而不是在此处进行评论。
2014年

19

您可以让启动方法返回一个对象,该对象是配置的必需参数:

资源* MyModule :: GetResource();
MySession * MyModule :: Startup();
Resource :: Configure(MySession * session);

即使您MySession只是一个空结构,这也将通过类型安全性强制执行,即Configure()在启动之前无法调用任何方法。


什么会阻止某人做某事module->GetResource()->Configure(nullptr)
2014年

@svick:没什么,但是您必须明确地执行此操作。这种方法告诉您期望的结果,绕过期望是有意识的决定。与大多数编程语言一样,没有人会阻止您用脚射击。但是用API清楚地表明您正在这样做总是很不错的;)
Michael Klement 2014年

+1看起来很棒又简单。但是,我可以看到一个问题。如果我有对象a, b, c, d,则可以启动a,并MySession尝试将其b用作已启动的对象,而实际上没有。
Vorac 2014年

8

以Cashcow的答案为基础-当您仅能提供一个新的Interface时,为什么必须向调用者提供一个新的Object?品牌重塑模式:

class IStartable     { public: virtual IRunnable      start()     = 0; };
class IRunnable      { public: virtual ITerminateable run()       = 0; };
class ITerminateable { public: virtual void           terminate() = 0; };

如果会话可以多次运行,您还可以让ITerminateable实现IRunnable。

您的对象:

class Service : IStartable, IRunnable, ITerminateable
{
  public:
    IRunnable      start()     { ...; return this; }
    ITerminateable run()       { ...; return this; }
    void           terminate() { ...; }
}

// And use it like this:
IStartable myService = Service();

// Now you can only call start() via the interface
IRunnable configuredService = myService.start();

// Now you can also call run(), because it is wrapped in the new interface...

这样,您只能调用正确的方法,因为开始时只有IStartable-Interface,并且只有在调用start()时才能访问run()方法。从外部看,它看起来像是具有多个类和对象的模式,但是基础类保留为一个类,该类始终被引用。


1
只拥有一个基础类而不是多个基础类有什么好处?因为这是与我提出的解决方案的唯一区别,所以我对这一点很感兴趣。
2014年

1
@MichaelGrünewald不必用一个类来实现所有接口,但是对于配置类型的对象,这可能是在接口实例之间共享数据的最简单的实现技术(即,由于相同而被共享)宾语)。
2014年

1
这本质上是步骤构建器模式
2014年

@JoshuaTaylor在接口实例之间共享数据是双重的:虽然可能更易于实现,但我们必须注意不要访问“未定义状态”(例如访问未连接服务器的客户端地址)。由于OP将重点放在界面可用性上,我们可以判断这两种方法是相同的。谢谢您引用“步骤构建器模式” BTW。
Michael Le BarbierGrünewald2014年

1
@MichaelGrünewald如果仅通过给定点上指定的特定接口与对象进行交互,则不应有任何方式(不进行强制转换等)来访问该状态。
2014年

2

有很多有效的方法可以解决您的问题。Basile Starynkevitch提出了一种“零官僚主义”的方法,该方法为您提供了一个简单的界面,并依靠程序员适当地使用该界面。尽管我喜欢这种方法,但我将介绍另一种方法,它具有更多的创新性,但允许编译器捕获一些错误。

  1. 识别各种状态的装置可在,如UninitialisedStartedConfigured等等。该列表必须是有限的。¹

  2. 对于每个状态,确定一个struct保持有关该状态下的必要的附加信息,例如DeviceUninitialisedDeviceStarted等等。

  3. 将所有处理打包在一个对象中DeviceStrategy,其中方法使用2.中定义的结构作为输入和输出。因此,您可能有一个DeviceStarted DeviceStrategy::start (DeviceUninitalised dev)方法(或根据您的项目约定的任何等效方法)。

使用这种方法,有效的程序必须按方法原型强制执行的顺序调用某些方法。

各种状态是不相关的对象,这是由于替换原理。如果让这些结构共享一个共同祖先对您有用,请记住,访问者模式可以用于恢复抽象类实例的具体类型。

虽然我在3.中描述了一个独特的DeviceStrategy类,但是在某些情况下,您可能希望将其提供的功能拆分为几个类。

概括起来,我描述的设计要点是:

  1. 由于替换原理,表示设备状态的对象应该是不同的,并且没有特殊的继承关系。

  2. 将设备处理打包在启动对象中,而不是在表示设备本身的对象中进行打包,以便每个设备或设备状态仅看到自己,并且该策略看到所有设备或设备状态并表达它们之间可能的转换。

我发誓我曾经看到遵循这些内容的telnet客户端实现的描述,但是我再也找不到它。这将是一个非常有用的参考!

¹:为此,请按照您的直觉或在您的实际实现中找到方法“等价方法”的等效类。将它们用于同一对象是有效的。” —假设您有一个大对象封装了设备上的所有处理方法。列出状态的两种方法都给出了出色的结果。


1
与其定义单独的结构,不如定义每个阶段的对象应该呈现的必要接口就足够了。这就是步骤构建器模式
2014年

2

使用构建器模式。

有一个对象,该对象具有用于上述所有操作的方法。但是,它不会立即执行这些操作。它只记得以后的每个操作。由于操作不会立即执行,因此将它们传递给构建器的顺序并不重要。

在构建器上定义所有操作之后,调用execute-方法。调用此方法时,它将按照上面存储的操作以正确的顺序执行上面列出的所有步骤。在将操作写入到硬件之前,此方法也是执行一些跨操作的完整性检查(例如尝试配置尚未设置的资源)的好地方。如果您的硬件容易受到此影响,这可能使您免于以不合理的配置损坏硬件。


1

您只需要正确记录如何使用该接口,并给出一个教程示例即可。

您可能还具有调试库变体,它会进行一些运行时检查。

或许,确定和记录正确的一些命名约定(如preconfigure*startup*postconfigure*run*....)

顺便说一句,许多现有接口都遵循类似的模式(例如X11工具箱)。


可能需要类似于Android应用程序活动生命周期的状态转换图来传达信息。
rwong

1

这确实是一种常见且隐蔽的错误,因为编译器只能执行语法条件,而您需要客户端程序在“语法上”正确。

不幸的是,命名约定对这种错误几乎完全无效。如果您确实想鼓励人们不要做不合语法的事情,则应该传递某种命令对象,该命令对象必须使用前提条件的值进行初始化,以使他们不会混乱地执行这些步骤。


你的意思是像这样
Vorac 2014年

1
public class Executor {

private Executor() {} // helper class

  public void execute(MyStepsRunnable r) {
    r.step1();
    r.step2();
    r.step3();
  }
}

interface MyStepsRunnable {

  void step1();
  void step2();
  void step3();
}

使用此模式,您可以确保任何实现者都将按照此确切顺序执行。您可以再进一步一步,制作一个ExecutorFactory,它将使用自定义执行路径构建Executors。


另一条评论中,您将此称为“步骤构建器”实现,但事实并非如此。如果您有MyStepsRunnable的实例,则可以在步骤1之前调用步骤3。步骤构建器的实现将更类似于ideone.com/UDECgY。这个想法只是通过运行step1来获得带有step2的东西。因此,您不得不按正确的顺序调用方法。例如,请参阅stackoverflow.com/q/17256627/1281433
约书亚·泰勒

您可以将其转换为具有受保护方法(甚至是默认方法)的抽象类,以限制其使用方式。您将不得不使用执行程序,但我知道当前的实现可能存在一两个缺陷。
Silviu Burcea 2014年

但这仍然不能使其成为步骤构建器。在您的代码中,用户无法执行任何操作以在不同步骤之间运行代码。这个想法不仅对代码进行排序(无论是公共代码还是私有代码,还是封装起来的代码)。如您的代码所示,使用simple可以很容易地做到这一点step1(); step2(); step3();。步骤构建器的重点是提供一个暴露一些步骤的API,并强制执行这些步骤的调用顺序。它不应阻止程序员在步骤之间执行其他操作。
2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.