【问题标题】:Removing dependencies from statechart framework从状态图框架中删除依赖项
【发布时间】:2011-11-15 15:44:30
【问题描述】:

我目前正在进行的项目有很多问题。该项目已有 10 多年的历史,它基于 90 年代非常流行的商业 C++ 框架之一。问题在于状态图。该框架提供了相当常见的状态模式实现。每个状态都是一个单独的类,有进入动作、状态动作等。有一个开关可以根据接收到的事件设置当前状态。

魔鬼隐藏在细节中。那个项目是巨大的。大约2000 KLOC。肯定有太多的状态图(我见过使用状态图实现的“for”循环)。更重要的是......框架允许将状态图嵌入到另一个状态图中,因此有许多状态图具有七个甚至更多级别的嵌套。因为状态图在不同的线程中运行,并且可以在状态图之间发送事件,所以我们有很多同步问题(以及接口中的大混乱)。

我必须承认这个问题的规模是巨大的,我不知道如何去解决它。我的第一个想法是尽可能多地从状态图中删除代码并将其放入单独的类中。然后从状态图中委派这些类来完成工作。但结果我们将有许多单独的函数,它们在逻辑上没有任何特定的功能,并且状态图架构的任何更改也需要更改这些类和函数。

所以我寻求帮助: 你知道任何可以帮助我解决这个问题的书籍/文章/魔法文物吗?我想至少在不引入任何隐藏依赖项的情况下,尽可能多地从状态图中分离代码,并保持分离的代码可维护、可测试和可重用。

如果您对如何处理此问题有任何建议,请告诉我。

【问题讨论】:

  • 这个系统不是偶然发生的。有人以一种你不理解的对他们有用的方式构建它;你最好弄清楚这一点。那么这个系统是使用状态图编辑器/代码生成器构建的吗?如果你丢失了它,你就丢失了实际的源代码;您正在处理的是生成器编译的“目标代码”。还是所有这些状态图都是手工生成的?你怎么知道? (确实注定:)
  • ...考虑到这一点,我想知道问题是否真的是状态图它们只是一种组织控制的方式,很像 if 语句和 while 循环,具有允许并行性的额外优势子图表。这应该有助于了解代码的工作原理;你可以只用“低级 C++”结构来编码这些东西,这有点像只使用 goto 来编码结构化控制流。除非最初的编码员完全是傻瓜,否则他们可能会像理解结构化控制流一样理解这个流程。
  • ira baxter:有一个用于状态图设计的图形工具,状态图存储在某种伪代码中。第一件事是不可能将这些文件作为文本文件保存在 svn 中,因为合并对它们不起作用。第二个:不幸的是,这个工具被错误地使用了。开发人员和架构师在架构设计和应用程序框架实现的第一个也是最重要的阶段学习这个工具。现在我们有了基于框架相关事件的接口,而且代码根本不可移植。
  • 对于 2 MSLOC 的代码?对于没有学会如何使用他们的工具的人来说,这是一大块东西。一般来说,面对一个大系统时,你应该对最初的设计师仁慈。他们可能很聪明,并且可能对他们的意图有所了解,甚至在某种程度上实现了它。你是这里缺乏理解的人,或者至少这是你应该下的赌注,除非你真的,真的知道你在做什么。可能他们都搞砸了。但是,如果它们是有组织的,而您却不知道是如何组织的,那么您就是受到伤害的人。
  • 您关于“无法在 svn 中存储为文本文件”的评论...那又如何?您仍然可以存储它们。如果它们不是文本也没关系;重要的是它们代表了您的一些代码结构,在您确定可以丢弃它们之前,您不应该这样做。 (你这样想的事实表明你不是真的,真的确定你在做什么)。 YMMV。

标签: c++ design-patterns architecture refactoring state-pattern


【解决方案1】:

在我看来,你最好的选择是(大吃一惊!)如果它像你想象的那样糟糕透顶,可能会从头开始。有文件吗?你能开始基于文档构建一些更健全的软件吗?

如果无法选择完全重写(而且在我的经验中从来没有),我会尝试以下一些方法:

  1. 如果您还没有,请绘制整个系统的架构图。勾勒出所有应该如何协同工作,这将帮助您将系统分解为潜在的可管理/可测试的部分。
  2. 您是否有任何类型的要求或测试计划?如果没有,您是否可以编写一个并开始为已经存在的各种代码/功能块进行单元测试?如果你能做到这一点,你就可以开始重构事物,而不会破坏当前工作的所有所做
  3. 一旦您对事物进行了一些分解,就可以开始将您的单元测试构建成集成测试,从而将更多的功能整合在一起。

我自己没有读过这些书,但我听说过这些书的好东西,可能有一些你可以参考的建议:

  1. Refactoring: Improving the Design of Existing Code (Object Technology Series)
  2. Working Effectively with Legacy Code (Robert C. Martin)

祝你好运! :-)

【讨论】:

  • 从零开始使用 2 M SLOC?你疯了吗?
  • 基本上,这是我们试图遵循的方式。单元和模块测试的质量很差,但我们从测试改进开始重构项目。编写一个涵盖 2000000 行代码项目中所有需求的测试会消耗大量资源,但是如果您有需求和测试,为什么不从头开始重写所有内容:)?该任务的资源分配对我来说至关重要。我只有四个人试图修复以前由 40 位开发人员开发的代码。感谢您对书籍的建议。
  • 真的有要求吗,或者只是一些关于人们在 5 年前认为问题出在哪里的尘土飞扬的文件?假设您的代码库是 50% 垃圾,并且您只需要 1 个 MSLOC 代码。每天调试 50 行代码(你可以做得更好?不是 Capers Jones 所说的),你只需要 1000000/50 -> 20,000 人日或 10 人年。你有一年和10个人? 6个月20个人?等等,你有 4 个人,好吧,2.5 年?尝试完全重写的人通常会失败,如果没有其他原因,因为管理层会在很长一段时间后拔掉插头。
  • 仅仅因为有 2M SLOC 并不意味着它已经全部使用,没有大量重复,当然也不意味着它是正确的。 OP 给我的印象是他想把它做好,如果它像他所说的那样坏,那么这可能是 最好的做法。如果您尝试正确...,开发人员的数量和/或特定的时间限制实际上并没有什么区别
  • ..._but_,有时没有正确的答案(至少在可用的时间尺度和资源的情况下)。有时您必须在糟糕的情况下尽力而为,在这种情况下,我同意;从头开始很可能最终以不完整的产品结束。尽可能地修复代码库最终可能会得到一个表面上看起来更完整但可能充满错误的产品。
【解决方案2】:

状态图(包括嵌套状态图)是指定、理解甚至模拟/验证复杂控制流的强大方法。但是要获得好处,您需要在合适的工具中使用状态图模型(我以前使用过 Statemate,不确定它是否仍然可用),以及从图表到代码的可靠映射(Statemate 用于生成代码) - 那么你可以忘记状态管理代码(大部分)!在您的情况下,如果您没有模型,我会尝试从代码中反转一个 - 正如 Ira 所说,原始开发人员很有可能拥有某种形式的模型,您可能会发现代码很多模型出现时的意义。如果这行得通,您将拥有一个非常好的代码规范/模型,这将使​​未来的代码编辑更加容易(即使您不想进行自动代码生成,并手动维护代码/模型映射(但是你需要一丝不苟!!))

【讨论】:

    【解决方案3】:

    状态图模式旨在专门用于删除 switch 语句,所以这听起来像是一种可怕的滥用。此外,状态应该只在异步事件上改变。如果您正在处理一个事件并且您通过多个状态(或 for 循环等)进行更改,那么这也是对模式的可怕滥用。

    我将从这两点开始,因为它们将解决您的大部分并发问题,只需修复它们即可。您需要确定的是:

    1. 您对系统的外部异步事件是什么?这些是唯一应该确定状态转换的事情,而不是在事件处理期间发生的事情。一个事件可能会导致 0 或 1 个状态转换。获得这些状态转换的列表后,您就可以重建系统的实际状态。如果您了解 UML 状态图,这将是在图表程序中绘制一个草图的最佳时机,不仅对您自己(尽管它会极大地帮助您),而且对将来必须返回到项目。如您所知,这种情况会发生。
    2. 既然您知道什么是真正的状态,请列出代码中不应该出现的状态。这通常表明某些东西可以“功能分解”。而不是每个状态对象,可能只需要一个单独的函数。这将减少状态对象的大量开销,并且应该极大地清理代码。
    3. 现在是时候解决您提到的那些可怕的 switch 语句了。如果它们真的基于状态,那么您根本不需要一个。相反,您应该能够直接调用状态机。

    类似:

    myStateMachine->myEvent();
    

    它应该可以在没有任何开关的情况下工作。但请注意,即使对于某些不能跨异步事件工作的对象,情况也可能如此。这也表明您可以在哪里使用继承来获得相同的效果。如果你有:

    switch (someTypeIdentifier)
    {
    case type1:
      doSomething();
      break;
    
    case type2:
      doSomethingElse();
      break;
    }
    

    通常正确的 OOP 方法是创建两个实际类型 Type1、Type2,它们都派生自 abstract 基础 TypeBase,并使用虚拟方法 doSomething() 来满足您的需要。这很有用的原因是因为它意味着您可以“关闭”处理(在打开/关闭原则的含义中),并且仍然通过根据需要添加新的派生类型来扩展功能(使其对扩展开放)。这疯狂地节省了错误,因为它让开发人员摆脱了那些可能会变得非常丑陋和复杂的 switch 语句,而是将每个单独的行为封装在单独的类中。

    4 - 现在寻求解决您的线程问题。识别从多个线程中使用的所有对象。做一个列表。现在,这些是如何使用的?其中一些总是一起使用吗?开始组团。这里的目标是找到最适合这些对象的封装级别,将对象分成单独的类来控制它们自己的同步,找出对象实际“事务”的原子级别,并制作类的方法公开那些有意义的事务,用适当的互斥锁、条件变量等在幕后进行包装。

    您可能会说“听起来工作量很大!为什么要做所有这些而不是自己写一遍?”好问题! :) 原因实际上很简单:如果您要自己完成所有操作,那么无论如何您都应该执行这些步骤。您应该识别您的状态、您的动态多态性并处理多线程事务。但是,如果您从现有代码开始,您还会拥有所有那些从未记录在案的未说出口的业务规则,并且可能会导致各种意外错误。你不必把所有东西都带过来——如果你怀疑这是一个错误,请与过去使用过该系统的人(如果有的话)、QA 或任何可能识别错误的人讨论逻辑,看看它是否真的应该结转。但是您需要实际评估其中的错误是什么,否则您可能不会编写真正需要编码的东西。

    最后,这是一个手动过程,是软件工程的一部分。有一些 CASE 工具可以帮助绘制状态图,甚至将它们发布到代码中,还有一些重构工具,比如许多 IDE 中的工具,可以帮助在函数和类之间移动代码,以及可以帮助识别线程需求的类似工具.但是,不应该为单个项目拾取这些东西。他们需要在你的整个职业生涯中学习,在多年的工作中学习并更深入地学习它们,因为它们是成为软件工程师的一部分。他们不是为你做的。您仍然需要知道原因和方法,它们只会帮助您更有效地完成工作。

    【讨论】:

      猜你喜欢
      • 2019-11-13
      • 2015-06-18
      • 2011-12-05
      • 2014-03-16
      • 2015-03-07
      • 2010-09-05
      • 2018-05-10
      • 2014-07-29
      • 1970-01-01
      相关资源
      最近更新 更多