【问题标题】:CQS Design Principle Problem: Implementing a QueueCQS设计原则问题:实现一个队列
【发布时间】:2018-11-11 23:11:05
【问题描述】:

我是根据我在 cmets 中对这个问题的答案的一个小讨论来创建这个问题的: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

【问题讨论】:

    标签: design-patterns functional-programming


    【解决方案1】:

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

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

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

    因为ss2 不同步(即它们是不同的堆栈),所以在对象client 内部,它仍然可以看到旧版本的堆栈(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 的每个调用者都必须维护一个堆栈才能调用 foo 函数。这就是为什么在 FP 中你往往会看到比在 OOP 中具有更多参数的函数。

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

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

    当然,缺点是函数现在有五个额外的参数,看起来很痛苦。 (当然OO版本中的等价方法也有这五个依赖,但都是隐含的)。

    但在我看来,这种痛苦实际上是有帮助的!使用 OO 风格的界面,随着时间的推移,它们会自然而然地增加杂物。但是对于像这样的显式参数,有太多的依赖项自然会抑制!对接口隔离原则等指南的需求已大大减少。

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

    如果您不能忍受这种痛苦,那么只需打破 CQS 并返回传统的 Stack 实现即可。毕竟,如果一个函数(比如pop/dequeue)是众所周知的,并且清楚地知道它既返回了一些东西又改变了它的内部数据,那么违反 CQS 并不是那么糟糕。

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

    希望这会有所帮助:)

    【讨论】:

    • 我确实需要更多地使用部分应用程序;非常强大 :) 我仍然认为 CQS 作为一个通用的设计原则没有多大意义,但是,因为增加了不必要的复杂性。最好说它对特定场景有益,而不是作为一般设计原则。
    【解决方案2】:

    因为函数的设计,需要返回一个反映上下文的状态。

    如果您在以下最小方代码中使用 DequeueResult 补充 bool,这可以让您区分成功和失败以及可能的其他信息。

    让 Dequeue = function bool(Result)

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

    更符合 CQS 的可能是

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

    但是需要 Head 具有 Node.Null 的特殊值来尝试区分失败和争用。

    返回 DequeueResult 可能会更好,因为您可以在结果中指出更多关于失败的信息。

    【讨论】:

    • 此版本的 Dequeue 不返回元素。您可以通过在出队前查看来解决此问题,但随后您又回到了我最初对 CQS 的抱怨:您使您的代码变得更加乏味,因为您现在需要多个操作来完成您可以在一个步骤中完成的相同基本任务不遵循 CQS。
    • 那么你想要 Let Dequeue = function Node() Return Head 你必须有一个特殊的 Node 操作符或者有一个 Node.Null
    • 但是如何解决保持多个堆栈引用同步的问题呢?请参阅 OP 中的示例代码。
    • 除非我们假设 JavaScript,否则您将无法将 Node 分配给 Stack 变量,在这种情况下 const 是解决方案
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-05
    • 1970-01-01
    • 2010-09-10
    相关资源
    最近更新 更多