CQS设计原则问题:实现队列

时间:2018-11-10 10:22:50

标签: design-patterns functional-programming

我是根据对这个问题的答案的评论中的一个小讨论创建这个问题的:design a method returning a value or changing some data but not both

@Kata指出,OP感兴趣的模式称为命令-查询分离,并认为这是构建代码的良好模型。

来自wikipedia

  

命令查询分离(CQS)是命令式计算机编程的原理。它是由Bertrand Meyer设计的,是他在Eiffel编程语言方面的开创性工作的一部分。

     

它指出,每个方法都应该是执行操作的命令,或者是将数据返回给调用方的查询,但不能两者都执行。换句话说,提出问题不应改变答案。1更正式地讲,方法仅在参照透明且没有副作用的情况下才应返回值。

我对这种设计原则的合理性提出了质疑,因为总体而言,它似乎会使您的代码更加乏味。例如:您无法执行 next = Queue.Dequeue(); 之类的简单语句。您将需要两条指令:一条用于修改数据结构,一条用于读取结果。

@Kata发现了一种替代的Stack实现,乍一看似乎满足了两个方面的最好要求:从函数式编程中获取一页,我们将Stack定义为不变的数据结构。每当我们push(x)时,我们都会创建一个新的Stack节点,该节点保存值x并维护指向旧头Stack实例的指针。每当我们pop()时,我们只是将指针返回到下一个Stack实例。因此,我们可以遵守命令查询分离原则。

示例堆栈实现:https://fsharpforfunandprofit.com/posts/stack-based-calculator/

但是,在这种情况下还不清楚的一件事是,如何在仍然遵循命令查询分离原理的情况下使对堆栈的多个引用保持同步?我没有看到一个明显的解决方案。因此,出于好奇,我正在向社区提出此问题,以查看是否找不到令人满意的解决方案:)

编辑:这是问题的一个示例:

s = new Stack();
s2 = s
...
s = s.push(x);
assert(s == s2); // this will fail

2 个答案:

答案 0 :(得分:1)

在函数式编程(FP)风格中,我们经常设计函数,这样就无需使这些引用保持同步。

请考虑以下情形:创建一个堆栈s,将其注入到Client对象中,然后将项目推入s并获得一个新的堆栈s2:< / p>

s = new Stack()
client = new Client(s)
s2 = s.push(...)

由于ss2不同步(即它们是不同的堆栈),因此在对象client内部,它仍然看到堆栈的旧版本({{1} }),这是您不想要的。这是s的代码:

Client

为解决此问题,功能方法不使用此类隐式引用,而是将引用作为显式参数传递给函数:

class Client {
    private Stack stack;
    // other properties
    public Client(Stack stack) { this.stack = stack; }
    public SomeType foo(/*some parameters*/) {
        // access this.stack
    }
}

当然,有时这会很痛苦,因为该函数现在具有一个额外的参数。 class Client { // some properties public SomeType foo(Stack stack, /*some parameters*/) { // access stack } } 的每个调用者都必须维护一个堆栈才能调用Client函数。这就是为什么在FP中您倾向于看到比OOP中具有更多参数的函数的原因。

但是FP的概念可以减轻这种痛苦:所谓的partial application。如果已经有了堆栈foo,则可以编写s来获得client.foo(s)的“升级”版本,该版本不需要堆栈,而只需要另一个foo。然后,您可以将升级后的some parameters函数传递给不维护任何堆栈的接收器。

尽管如此,值得一提的是有些人认为这种痛苦实际上会有所帮助。例如,Scott Wlaschin在他的文章Functional approaches to dependency injection中:

  

当然,缺点是该函数现在有五个额外的参数,看起来很痛苦。 (当然,OO版本中的等效方法也具有这五个依赖关系,但它们是隐式的。)

     

尽管在我看来,这种痛苦实际上是有帮助的!使用OO风格的接口,它们自然会随着时间的推移而增加粗粒。但是,使用像这样的显式参数,自然就会抑制太多的依赖!对诸如接口隔离原理之类的指南的需求已大大减少。

此外,Dependency Injection书的作者Mark Seemann在Dependency Rejection上也有一个有趣的系列。

以防万一您无法忍受那种痛苦,那么只需中断CQS并返回到Stack的传统实现即可。毕竟,如果一个函数(例如foo / pop)是众所周知的,并且知道它既返回内容又更改其内部数据,那么违反CQS的情况就不是那么糟糕了。

即使在这种情况下,某些FP语言也提供消息传递机制,以便您可以以不编写代码变异数据的方式(例如,使用赋值符号的代码)来实现可变堆栈。 F#中的MailboxProcessor是这种机制。

希望这会有所帮助:)

答案 1 :(得分:0)

由于功能的设计,您需要返回一个反映上下文的状态。

如果在以下最小参与方代码中用DequeueResult补充bool,则可以区分成功和失败以及其他潜在信息。

让出队= function bool(Result)

如果Head == Null返回 ... 返回true

可能更符合CQS

让出队=函数Node() 返回头

但是会要求Head拥有Node.Null的特殊值,以尝试区分失败和争用。

返回DequeueResult可能更好,您可以在结果中更多地指示失败。