【问题标题】:Spartan Programming斯巴达编程
【发布时间】:2010-09-08 05:50:59
【问题描述】:

我真的很喜欢Jeff's post 上的Spartan Programming。我同意这样的代码阅读起来很有趣。不幸的是,我不太确定与它一起工作是否一定会很愉快。

多年来,我一直阅读并坚持“每行一个表达式”的做法。当许多编程书籍用示例代码反驳这个建议时,我已经打了一场好仗并坚持了自己的立场:

while (bytes = read(...))
{
   ...
}

while (GetMessage(...))
{
   ...
}

最近,出于更实际的原因,我提倡每行一个表达式 - 调试和生产支持。从生产中获取一个在“第 65 行”声明 NullPointer 异常的日志文件,内容如下:

ObjectA a = getTheUser(session.getState().getAccount().getAccountNumber());

令人沮丧,完全可以避免。如果没有专家可以选择“最有可能”为空的对象的代码......这是一个真正的实际痛苦。

在单步执行代码时,每行一个表达式也有很大帮助。我假设大多数现代编译器可以优化掉我刚刚创建的所有多余的临时对象...

我尽量保持整洁 - 但是用显式对象弄乱我的代码有时确实感觉很费力。它通常不会使代码更易于浏览 - 但在跟踪生产中的内容或单步执行我或其他人的代码时,它确实派上了用场。

提倡什么风格,你能在实际意义上合理化它吗?

【问题讨论】:

    标签: language-agnostic styles spartan


    【解决方案1】:

    每行一个表达式。

    没有理由混淆您的代码。输入几个额外术语所花费的额外时间,可以节省调试时间。

    【讨论】:

      【解决方案2】:

      我认为理想的解决方案是在极端之间找到平衡。没有办法编写适合所有情况的规则;它带有经验。将每个中间变量声明在自己的行中,会使代码的阅读变得更加困难,这也会增加维护的难度。同理,如果内联中间值,调试会困难得多。

      “甜蜜点”在中间的某个地方。

      【讨论】:

        【解决方案3】:

        我倾向于在可读性方面犯错,不一定是可调试性。您给出的示例绝对应该避免,但我觉得明智地使用多个表达式可以使代码更简洁和易于理解。

        【讨论】:

          【解决方案4】:

          实用程序员中,Hunt 和 Thomas 谈到了一项他们称之为 Demeter 定律的研究,该研究侧重于将功能与模块以外的模块耦合。通过允许一个函数在它的耦合中永远不会达到第 3 级,您可以显着减少错误的数量并提高代码的可维护性。

          所以:

          ObjectA a = getTheUser(session.getState().getAccount().getAccountNumber());

          接近重罪,因为我们在老鼠洞里有 4 个物体。这意味着要更改其中一个对象中的某些内容,我必须知道您在此方法中调用了整个堆栈。好痛啊。

          更好:

          Account.getUser();

          请注意,这与现在在模拟软件中非常流行的表达性编程形式背道而驰。代价是无论如何你都有一个紧密耦合的接口,而富有表现力的语法让它更容易使用。

          【讨论】:

          • 您所说的流畅 API 通常不会沿着有状态对象的树(就像您的第一个示例那样),而是返回相同的对象或封装状态的新内部对象。因此,虽然他们违反了 LoD 的文字,但他们并没有违反其精神。
          • @Kyoryu,我同意你的说法,但问题集中在每行 1 条语句上。流畅的 API 仍然使调试变得有点困难,因为您在一行上执行多个操作。
          • 我完全明白你的意思。良好的流畅接口使用“多个语句”作为实现单个逻辑语句的更好方法,而不是实际上是多个原子调用。对于您的示例,我什至认为 Account.GetUser() 不是一个特别好的示例,因为通常随后会对用户采取行动。我宁愿看到 Account.ActualOperation()。 (我认为我们大体上同意,我们仍然赞成,我们在 2% 上争论不休)
          【解决方案5】:

          我通常属于“越短越好”的阵营。你的例子很好:

          ObjectA a = getTheUser(session.getState().getAccount().getAccountNumber());
          

          如果我看到它超过四行而不是一行,我会感到畏缩——我认为这不会使它更容易阅读或理解。你在这里展示它的方式,很明显你正在挖掘一个对象。这不是更好:

          obja State = session.getState();
          objb Account = State.getAccount();
          objc AccountNumber = Account.getAccountNumber();
          ObjectA a = getTheUser(AccountNumber);
          

          这是一种妥协:

          objb Account = session.getState().getAccount();
          ObjectA a = getTheUser(Account.getAccountNumber());
          

          但我还是更喜欢单行表达式。这是一个轶事原因:我现在很难重新阅读和错误检查 4-liner 是否存在愚蠢的错别字;单行没有这个问题,因为字符更少。

          【讨论】:

          • 但是如果使用 ObjectA 引发 NullPointerException a = getTheUser(session.getState().getAccount().getAccountNumber());火车上的哪个物体是空的? Session、State、Account、AccountNumber 还是从原始函数返回的用户?
          【解决方案6】:

          可维护性和可读性是王道。幸运的是,更短通常意味着更易读。

          以下是我喜欢使用的一些对代码进行切片和切块的技巧:

          • 变量名称:您会如何向团队中的其他人描述这个变量?你会说“numberOfLinesSoFar 整数”。你会说“numLines”或类似的东西 - 易于理解且简短。不要假装维护者根本不知道代码,但要确保你自己能弄清楚变量是什么,即使你忘记了自己编写它的行为。是的,这很明显,但它比我看到的许多编码人员投入的更多努力更值得,所以我先列出它。
          • 控制流:一次避免大量结束子句(C++ 中的一系列 })。通常,当您看到这种情况时,有一种方法可以避免它。一个常见的情况是

          if (things_are_ok) {
            // Do a lot of stuff.
            return true;
          } else {
            ExpressDismay(error_str);
            return false;
          }
          

          可以替换为

          if (!things_are_ok) return ExpressDismay(error_str);
          // Do a lot of stuff.
          return true;
          

          如果我们可以让 ExpressDismay(或其包装器)返回 false。

          另一种情况是:

          • 循环迭代:越标准越好。对于较短的循环,当变量从不使用时,最好使用单字符迭代器,除非作为单个对象的索引。

          我要在这里争论的特殊情况是反对使用 STL 容器的“正确”方式:

          for (vector<string>::iterator a_str = my_vec.begin(); a_str != my_vec.end(); ++a_str)
          

          更加冗长,并且需要在循环中重载指针运算符 *a_str 或 a_str->size()。对于具有快速随机访问的容器,以下内容更容易阅读:

          for (int i = 0; i < my_vec.size(); ++i)
          

          在循环体中引用了 my_vec[i],这不会让任何人感到困惑。

          最后,我经常看到编码人员对他们的行数计数感到自豪。但重要的不是行号!我不确定实现这一点的最佳方法,但如果你对你的编码文化有任何影响,我会尝试将奖励转移给那些有紧凑类的人:)

          【讨论】:

            【解决方案7】:

            很好的解释。我认为这是一般Divide and Conquer 心态的版本。

            【讨论】:

              【解决方案8】:
              ObjectA a = getTheUser(session.getState().getAccount().getAccountNumber());
              

              这是一个不好的例子,可能是因为你只是从头顶写了一些东西。 您正在为 ObjectA 类型的名为 a 的变量分配名为 getTheUser 的函数的返回值。
              所以让我们假设你写了这个:

              User u = getTheUser(session.getState().getAccount().getAccountNumber());
              

              我会这样打破这个表达:

              Account acc = session.getState().getAccount();
              User user = getTheUser( acc.getAccountNumber() );
              

              我的理由是:我会如何看待我正在使用这段代码做什么?
              我可能会想:“首先我需要从会话中获取帐户,然后使用该帐户的号码获取用户”。

              代码应该按照您的想法阅读。变量应指涉及的主要实体;与其属性无关(所以我不会将帐号存储在变量中)。

              要记住的第二个因素是:在这种情况下,我是否需要再次引用该实体?
              例如,如果我要从会话状态中提取更多内容,我会介绍 SessionState state = session.getState()

              这一切似乎都很明显,但恐怕我很难用语言表达为什么它是有道理的,因为不是以英语为母语的人。

              【讨论】:

                猜你喜欢
                • 2021-09-11
                • 1970-01-01
                • 1970-01-01
                • 2017-10-22
                • 2020-09-23
                • 2021-05-27
                • 1970-01-01
                • 1970-01-01
                • 2021-04-24
                相关资源
                最近更新 更多