学到更多

第8章:逐步细化


在很大程度上,编程是通过计算机解决问题的科学。由于问题通常很困难,因此解决方案以及实施这些解决方案的程序也很困难。为了使您更容易开发这些解决方案,您需要采用一种方法和规程,将复杂程度降低到可管理的规模。

在编程的早期阶段,计算作为一门科学的概念或多或少是一厢情愿的实验。当时没有人对编程有太多了解,很少有人认为它是传统意义上的工程学科。然而,随着编程的成熟,这种学科开始出现。该学科的基石是理解编程是在程序员必须一起工作的社交环境中完成的。如果你进入行业,你几乎肯定会成为许多致力于开发大型程序的程序员之一。此外,该程序几乎肯定会继续存在并且需要超出其最初预期应用程序的维护。有人会希望程序包含一些新功能或以某种不同的方式工作。当发生这种情况时,新的程序员团队必须进入并对程序进行必要的更改。如果程序是以个人风格编写的,几乎没有共同点,那么让每个人高效地合作是非常困难的。

为了解决这个问题,程序员开始开发一组统称为的编程方法 软件工程 。使用良好的软件工程技能不仅使其他程序员更容易阅读和理解您的程序,而且还使您更容易编写这些程序。软件工程中最重要的方法学进步之一就是策略 自上而下的设计 要么 逐步细化 ,包括从整体问题入手解决问题。你将整个问题分解成碎片,然后解决每一块,必要时将它们进一步分解。这种自上而下的策略是补充 迭代测试 在继续之前确保解决方案的较小部分正常工作。

逐步细化的练习

为了说明逐步细化的概念,让我们教Karel解决一个新问题。想象一下卡雷尔现在生活在一个看起来像这样的世界:

在每个列上,有一个锥体 s高度未知的塔,尽管有些列(例如样本世界中的第7和第9列)可能是空的。卡雷尔的工作是收集每座塔中的所有锥体 ,将它们放回第一排的最东端,然后返回其起始位置。因此,当卡雷尔在上面的例子中完成其工作时,目前在塔中的所有25个锥体应该堆叠在第9列和第1行的角落,如下所示:

重要的是,您可以假设卡雷尔最初启动零锥体秒。 锥体拾取的每个锥体添加到其包中。当把锥体放在角落时,卡雷尔可以使用 beepersInBag() 测试。

解决这个问题的关键是以正确的方式分解程序,同时仍然可以随时测试。此任务比您看到的其他任务更复杂,这使得选择适当的子问题对于获得成功的解决方案更为重要。

自上而下的设计原则

逐步细化的关键思想是你应该从顶部开始设计你的程序,它指的是概念上最高和最抽象的程序级别。在这个层面上, 锥体塔问题显然分为三个独立的阶段。首先,卡雷尔必须收集所有的锥体 。其次,卡雷尔必须将它们存放在最后的十字路口。第三,卡雷尔必须回到原来的位置。这个问题的概念分解表明该程序的run方法将具有以下结构:

   public void run() {
      收集所有锥体S();
      全部锥体秒();
      回家();
   }

在这个级别,问题很容易理解。当然,还有一些尚未编写的方法形式的细节。即便如此,重要的是要查看每个级别的分解,并说服自己,只要您相信您要编写的方法将正确解决子问题,您就可以找到问题的解决方案作为一个整体。

随你进行迭代测试

现在您已经为整个程序定义了结构,现在是第一个子问题move时间,它包括收集所有锥体 。这个任务本身比前面章节中的简单问题更复杂。收集所有锥体 s意味着你必须在每个塔中拿起锥体 ,直到你到达最后一个角落。你需要为每个塔重复一个操作的事实表明你需要一个while循环。 while循环将重复该过程 收集一个塔 然后移动。

警告: 尝试编写整个程序是危险的 测试 随你而去如果你弄错了,就很难找到错误。我们知道我们将重复收集一座塔的过程。让我们写和 测试 在我们放入之前收集一个塔 收集一个塔 在for循环中处理。从而temporariliy我们可以从收集所有锥体S的以下定义开始:

   private void 收集所有锥体S() {
      /* 临时实施用于测试目的 */
      收集一个塔();
      move();
   }

作为一个指导原则,如果你有一个复杂的循环,测试身体在编写整个循环之前的循环。

精炼收集塔

当收集一个塔被叫时,卡雷尔要么站在锥体秒的塔底,要么站在一个空角落。在前一种情况下,您需要在塔中收集锥体 。在后者,你可以简单地move 。这种情况听起来像是if语句的一个应用程序,你可以在其中编写如下内容:

   if(beepersPresent()){
      收集实际的塔();
   }

在将这样的语句添加到代码之前,您应该考虑是否需要进行此测试。通常,通过观察最初似乎特殊的案例可以用与更一般情况完全相同的方式来处理,可以使程序变得更加简单。在当前的问题中,如果你确定每条大道上有一座锥体秒的塔楼,但其中一些塔楼的零点锥体高?会发生什么?利用这种洞察力简化了程序,因为您不再需要测试特定大道上是否有塔。

收集一个塔方法仍然足够复杂,以便按顺序进行额外的分解。为了收集塔中的所有锥体 ,卡雷尔需要采取以下步骤:

  1. 向左转,面向塔内的锥体 。
  2. 收集塔中的所有锥体 ,当不再找到锥体秒时停止。
  3. 转身面向世界的底部。
  4. 回到代表地面的墙上。
  5. 左转准备move到下一个角落。

再一次,这个大纲为收集一个塔方法提供了一个模型,如下所示:

   private void 收集一个塔(){
      turnLeft();
      收集锥体的行();
      turnAround();
      move到墙();
      turnLeft();
   }

方法前置条件和后置条件

收集一个塔方法开头和结尾的转左命令对于该程序的正确性都是至关重要的。当收集一个塔被召唤时,卡雷尔总是在第一排朝东的某个地方。当它完成其操作时,只有当Karel再次朝向同一角落的东方时,整个程序才能正常工作。在调用方法之前必须为true的条件称为 先决条件 ;方法完成后必须应用的条件称为 后置条件

定义方法时,如果准确记下前置条件和后置条件,将会遇到更少的麻烦。完成后,您需要确保您编写的代码始终满足后置条件,假设前提条件满足开始。例如,想想当Karel面向东方的第一排时,如果你打电话给收集一个塔会发生什么。第一个转左命令使卡雷尔朝北,这意味着卡雷尔与代表塔的锥体列正确对齐。收集锥体的行方法 - 尚未编写,但仍执行一项您在概念上理解的任务 - 只需move秒即可完成。因此,在对收集倍体行的召唤结束时,卡雷尔仍将面向北方。因此,回转电话让卡雷尔朝南。就像收集锥体的行一样, move到墙的方法不涉及任何转弯,而只是move秒,直到它撞到边界墙。因为Karel朝南,所以这个边界墙将是屏幕底部的一个,就在第一排的下方。因此,最后的转左命令使卡雷尔在第一排朝东,这满足了后置条件。

重复这个过程

你run你的程序,它成功地清除了一个塔,并在承诺的后置条件下离开卡雷尔。嗬!你刚刚解决了这个艰巨任务的里程碑!我们现在必须重复使用while循环清除一个塔的过程。

但是这个while循环是什么样的呢?首先,你应该考虑条件测试。你希望Karel在行尾撞到墙壁时停下来。因此,只要前面的空间清晰,你就希望卡雷尔继续前进。因此,您知道收集所有锥体S方法将包含使用前面很清楚测试的while循环。在每个位置,您希望Karel从该角落开始收集塔中的所有锥体 。如果你给那个操作命名,这可能就像收集一个塔一样,你可以继续为收集所有锥体S方法写一个定义,即使你还没有填写详细信息。

但是,你必须要小心。收集所有锥体S的代码看起来不像这样:

   private void 收集所有锥体S(){
      /* 越野车循环! */
      while(frontIsClear()) {
         收集一个塔();
         move();
      }
   }

这个实现是错误的,原因与第6章的第一版Place Place体行未能完成其工作完全相同。此版本的代码中存在fencepost错误,因为Karel需要测试最后一个大道上是否存在锥体塔。正确的实施是:

   private void 收集所有锥体S(){
      while(frontIsClear()) {
         收集一个塔();
         move();
      }
      收集一个塔();
   }

请注意,此方法与第6章中提供的放置锥体行程序的主程序具有完全相同的结构。唯一的区别是此程序调用收集一个塔,其中另一个称为把蜂鸣器。这两个程序都是一般策略的示例,如下所示:

   private void 收集所有锥体S(){
      while(frontIsClear()) {
          执行一些操作。
         move();
      }
       对最后一个角落执行相同的操作。
   }

无论何时需要在每个角上执行操作,您都可以使用此策略,如沿着路径终止于墙的move 。如果您还记得此策略的一般结构,则只要遇到需要执行此操作的问题,就可以使用它。这种可重复使用的策略在编程中经常出现,并被称为 编程习语 要么 模式 。您知道的模式越多,您就越容易找到适合特定类型问题的模式。

整理起来

虽然已经完成了艰苦的工作,但仍有一些需要解决的松散目标。主程序调用两个方法 - 全部锥体秒和回家 - 这些方法尚未成文。同样,收集一个塔调用收集锥体的行和move到墙。幸运的是,所有这四种方法都很简单,无需进一步分解即可编码,特别是如果在回家的定义中使用move到墙。这是完整的实现:


下一章