代码之家  ›  专栏  ›  技术社区  ›  Ryan Pierce Williams

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

  •  1
  • Ryan Pierce Williams  · 技术社区  · 6 年前

    我根据我在回答这个问题的评论中的一个小讨论提出这个问题: design a method returning a value or changing some data but not both

    @Kata指出OP感兴趣的模式叫做 命令查询分离 并认为这是一个很好的代码结构模型。

    wikipedia :

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

    它指出,每个方法要么是执行操作的命令,要么是向调用方返回数据的查询,但不能两者兼而有之。换句话说,问一个问题不应该改变答案。 1 更正式地说,只有当方法是引用透明的,因此没有副作用时,它们才应该返回一个值。

    我对这个设计原则的合理性提出了质疑,因为一般来说,它会使您的代码更加乏味。例如:您不能执行像这样的简单语句 next=Queue.Dequeue();

    @Kata发现了另一种堆栈实现,乍一看似乎满足了这两个方面的最佳要求:从函数式编程中取一页,我们将堆栈定义为不可变的数据结构。每当我们按下(x)键时,我们都会创建一个新的堆栈节点,该节点保存值x并维护一个指向旧头堆栈实例的指针。每当我们弹出()时,我们只返回指向下一个堆栈实例的指针。因此,我们可以坚持命令-查询分离原则。

    https://fsharpforfunandprofit.com/posts/stack-based-calculator/

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

    编辑:以下是问题的示例:

    s = new Stack();
    s2 = s
    ...
    s = s.push(x);
    assert(s == s2); // this will fail
    
    2 回复  |  直到 4 年前
        1
  •  2
  •   Community dbr    4 年前

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

    考虑这个场景:创建堆栈 s Client 对象,然后将项目推送到 S 然后得到一个新的堆栈 s2

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

    s2 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
        }
    }
    

    当然,有时这会很痛苦,因为函数现在有一个额外的参数。每一个打电话的人 客户 foo 功能。这就是为什么在FP中,您往往会看到比OOP中具有更多参数的函数。

    partial application S ,你可以写 client.foo(s) 获取的“升级”版本 它不需要堆栈,只需要另一个堆栈 some parameters . 然后你就可以通过那次升级了 函数对不维护任何堆栈的接收器执行。

    Functional approaches to dependency injection :

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

    但在我看来,这种痛苦实际上是有益的!对于OO风格的接口,随着时间的推移,它们自然会增加crud。但是有了这样的显式参数,自然会抑制依赖性过多!对界面分离原则等指南的需求大大减少。

    还有,马克·希曼——这本书的作者 Dependency Injection --有一个有趣的系列节目 Dependency Rejection .

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

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

    希望这有帮助:)

        2
  •  0
  •   Jay    6 年前

    如果您在下面的最小参与方代码中使用出列结果来补充bool,这将允许您区分成功和失败以及潜在的其他信息。

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

    更符合CQS的可能是

    让出列=功能节点()

    但需要Head有一个特殊的Node.Null值,以尝试区分失败和争用。

    如果可以在结果中指出更多有关失败的信息,则返回出列结果可能会更好。