design-patterns - CQS设计原理题: Implementing a Queue

标签 design-patterns functional-programming

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

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

来自 wikipedia :

Command–query separation (CQS) is a principle of imperative computer programming. It was devised by Bertrand Meyer as part of his pioneering work on the Eiffel programming language.

It states that every method should either be a command that performs an action, or a query that returns data to the caller, but not both. In other words, Asking a question should not change the answer.1 More formally, methods should return a value only if they are referentially transparent and hence possess no side effects.

我质疑这个设计原则的合理性,因为通常它会使您的代码显得更加乏味。例如:您不能执行像 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

最佳答案

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

考虑这种情况:您创建一个堆栈 s,将其注入(inject)到 Client 对象中,然后将一个项目推送到 s 并获得一个新堆栈 s2:

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

因为 ss2 不同步(即,它们是不同的堆栈),在对象 client 中,它仍然看到旧版本的堆栈 (s) 这是您不想要的。这是客户端的代码:

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 的每个调用者都必须维护一个堆栈,以便调用 foo 函数。这就是为什么在 FP 中您倾向于看到比在 OOP 中具有更多参数的函数。

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

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

The downside of course, is that there are now five extra parameters for the function, which looks painful. (Of course, the equivalent method in the OO version also had these five dependencies, but they were implicit).

In my opinion though, this pain is actually helpful! With OO style interfaces, there is a natural tendency for them to accrete crud over time. But with explicit parameters like this, there is a natural disincentive to have too many dependencies! The need for a guideline such as the Interface Segregation Principle is much diminished.

此外,Mark Seemann -- 这本书的作者 Dependency Injection -- 在 Dependency Rejection 上有一个有趣的系列.

如果你不能忍受那种痛苦,那么就打破 CQS,回到 Stack 的传统实现。毕竟,如果一个函数(比如 pop/dequeue)是众所周知的并且很清楚它既返回了一些东西又改变了它的内部数据,那么违反 CQS 是还不错。

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

希望这有帮助:)

关于design-patterns - CQS设计原理题: Implementing a Queue,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53237990/

相关文章:

java - 如何消除同一包中两个类之间的循环依赖?

c# - 业务逻辑层和数据访问层 : circular dependency

haskell - Haskell 中列表列表的笛卡尔积

Javascript:对相邻对象的函数调用?

design-patterns - MVVM和MVA之间的区别(模型 View 适配器)

c - 有没有一种简单的方法可以在 ANSI C 中编写适合 11"MacBook Air 屏幕的策略(或其他)设计模式?

java - 概述/了解 Eclipse 中的体系结构代码

haskell - 如何通过 Haskell 中的弱指针缓存构建具有重复消除的无限树

f# - 在 F# 中,在有区别的联合中严格计算和内存

recursion - 了解 Peter Norvig 在 PAIP 中的置换解决方案