Skip to content

Latest commit

 

History

History
748 lines (532 loc) · 34.4 KB

File metadata and controls

748 lines (532 loc) · 34.4 KB

二、编写第一个模块化程序

在本章中,我们将使用模块化编程技术来实现一个非平凡的程序。在此过程中,我们将:

  • 了解分而治之的程序设计方法
  • 检查我们的程序需要执行的任务
  • 查看我们的程序需要存储的信息
  • 应用模块化技术将我们的程序分解为各个部分
  • 了解如何将每个部分实现为一个单独的 Python 模块
  • 了解各个模块如何协同工作以实现我们的程序功能
  • 按照此流程实施一个简单但完整的库存控制系统
  • 了解模块化技术如何允许您在最小化所需更改的同时向程序添加功能

库存控制系统

假设您被要求编写一个程序,允许用户跟踪公司的库存,即公司可供销售的各种物品。对于每个库存项目,您都被要求跟踪产品代码和项目的当前位置。新项目将在收到时添加,现有项目将在出售后删除。您的程序还需要生成两种类型的报告:一种是列出公司当前库存的报告,包括每种类型的商品在每个位置的数量,另一种是用于在库存商品售出后重新订购的报告。

从这些要求来看,我们显然需要存储三种不同类型的信息:

  1. 公司待售的不同类型的产品清单。对于每种产品类型,我们需要知道产品代码(有时称为 SKU 编号)、说明以及公司应在该类型产品的库存中拥有的所需商品数量
  2. 可存放库存物品的位置列表。这些位置可能是单独的商店、仓库或储藏室。或者,一个位置可以识别商店内的特定货架或过道。对于每个位置,我们需要有一个位置代码和一个描述来识别该位置。
  3. 最后是公司目前持有的存货清单。每个存货项目有一个产品代码和一个位置代码;这些标识用于标识产品类型和物品当前存放的位置。

运行程序时,终端用户应能执行以下操作:

  • 将新项目添加到库存中
  • 从库存中删除项目
  • 生成当前库存项目的报告
  • 生成需要重新订购的库存项目的报告
  • 退出程序

虽然这个程序并不太复杂,但这里有足够的功能可以从模块化设计中获益,同时我们的讨论仍然相对简短。现在我们已经了解了我们的程序需要做什么以及需要存储的信息,让我们开始将模块化编程技术应用到系统的设计中。

库存控制系统的设计

如果您后退一步,查看我们的库存控制程序的功能,您可以看到该程序需要支持三种基本类型的活动:

  • 存储信息
  • 与用户交互
  • 生成报告

虽然这是非常普遍的,但这种分解是有帮助的,因为它提供了一种组织程序代码的可能方法。例如,系统中负责存储信息的部分可以存储产品、位置和库存项目的列表,并根据需要提供这些信息。类似地,负责与用户交互的系统部分可以提示用户选择要执行的操作,要求他们选择产品代码,等等。最后,负责生成报告的系统区域将能够生成所需的每种类型的报告。

以这种方式思考系统,很明显,系统的这三个部分中的每一部分都可以作为一个单独的模块来实现:

  • 系统中负责存储信息的部分可以称为数据存储模块
  • 系统中负责与用户交互的部分可以称为用户界面模块
  • 系统中负责生成报表的部分可以称为报表生成器模块

顾名思义,这些模块中的每一个都有特定的用途。除了这些特殊用途的模块之外,我们还需要系统的另一部分:一个 Python 源文件,用户执行该文件来启动和运行库存控制系统。因为这是用户实际运行的部分,我们将其称为主程序,它通常存储在名为main.py的 Python 源文件中。

我们的系统现在有四个部分:三个模块加一个主程序。这些部件中的每一个都有特定的工作要做,不同的部件通常会相互作用以执行特定的功能。例如,报告生成器模块需要从数据存储模块获取可用产品代码的列表。下图中的箭头表示这些不同的交互:

Designing the inventory control system

现在我们有了一个关于我们的程序的整体结构的概念,让我们仔细看看这四个部分中的每一个,看看它们是如何工作的。

数据存储模块

这个模块将负责存储我们程序的所有数据。我们已经知道,我们需要存储三种类型的信息:产品列表、位置列表和库存项目列表。

为了使我们的程序尽可能简单,我们将对数据存储模块做出两个主要的设计决策:

  • 产品和位置列表将硬连线到我们的程序中
  • 我们将在内存中保存库存项目列表,并在列表发生更改时将其保存到磁盘

我们的库存控制系统的更复杂的实现将把这些信息存储在数据库中,并允许用户查看和编辑产品代码和位置列表。然而,在我们的例子中,我们对程序的整体结构更感兴趣,因此我们希望实现尽可能简单。

虽然产品代码列表将是硬连线的,但我们不一定要将此列表构建到数据存储模块本身中。数据存储模块负责存储和检索信息。定义产品代码列表不是数据存储模块的工作。因此,我们需要数据存储模块中的一个函数,可以调用该函数来设置产品代码列表。此函数将如下所示:

def set_products(products):
    ...

我们已经决定,对于每种产品,我们都要存储产品代码说明以及用户希望保存在该类型产品库存中的所需数量。为了支持这一点,我们将产品列表(在set_products()函数的products参数中提供)定义为(code, description, desired_number)元组列表。例如,我们的产品列表可能如下所示:

[("CODE01", "Product 1", 10),
 ("CODE02", "Product 2", 200), ...
]

定义产品列表后,我们可以提供一个函数,根据需要返回此列表:

def products():
    ...

这将简单地返回产品列表,允许您的代码根据需要使用此列表。例如,您可以使用以下 Python 代码扫描产品列表:

for code,description,desired_number in products():
    ...

这两个函数允许我们定义(硬连线)产品列表,并在需要时检索该列表。现在,让我们为位置列表定义等效的两个函数。

首先,我们需要一个函数来设置硬连线位置列表:

def set_locations(locations):
    ...

locations列表中的每一项都将是一个(code, description)元组,其中code是一个位置的代码,description是一个描述位置的字符串,以便用户知道它在哪里。

然后,我们需要一个函数根据需要检索此位置列表:

def locations():
    ...

再次返回位置列表,允许我们根据需要使用这些位置。

我们现在需要决定数据存储模块将如何允许用户存储和检索库存项目列表。库存项目定义为产品代码加位置代码。换句话说,库存项目是特定位置的特定类型的产品。

要检索库存项目列表,我们将使用以下函数:

def items():
    ...

按照我们用于products()locations()函数的设计,items()函数将返回一个库存项列表,其中每个库存项都是一个(product_code, location_code)元组。

但是,与产品和位置列表不同,库存项目列表不会硬连线:用户将能够添加和删除库存项目。为了支持这一点,我们还需要两个函数:

def add_item(product_code, location_code):
    ...

def remove_item(product_code, location_code):
    ...

我们只需要设计数据存储模块的另一部分:因为我们知道我们将在内存中存储库存项目列表,并根据需要将其保存到磁盘,所以我们需要某种方法在程序启动时将库存项目从磁盘加载到内存中。为了支持这一点,我们将为我们的模块定义一个初始化函数

def init():
    ...

我们现在决定为数据存储模块提供总共八个功能。这八个功能构成了我们模块的公共接口。换句话说,系统的其他部分将使用以下八种功能与我们的模块交互:

The data storage module

请注意我们在这里经历的过程:我们从查看模块需要做什么开始(在本例中是存储和检索信息),然后根据这些需求设计模块的公共接口。对于前七个功能,我们使用我们的业务需求来帮助我们设计接口,而对于最后一个功能,init(),我们使用我们对模块内部工作方式的了解来更改接口,以便模块能够完成其工作。这是一种常见的工作方式:业务需求和技术需求都将有助于塑造模块的接口以及模块与系统其余部分的交互方式。

现在我们已经设计了数据存储模块,让我们对系统中的其他模块重复这个过程。

用户界面模块

用户接口模块将负责与用户交互。这包括向用户询问信息,以及在屏幕上显示信息。为了简单起见,我们将为我们的库存控制系统使用一个简单的基于文本的界面,使用print()语句显示信息,input()要求用户输入内容。

我们的库存控制系统的更复杂实现将使用带有窗口、菜单和对话框的图形用户界面。这样做将使库存控制系统更加复杂,远远超出我们在这里试图实现的范围。然而,由于系统的模块化设计,如果我们重写用户界面以使用菜单、窗口等,我们只会更改这一个模块,系统的其余部分不会受到影响。

这实际上有点过于简单化了。用 GUI 替换基于文本的界面需要对系统进行许多更改,并且可能需要我们稍微更改模块的公共功能,就像我们必须向数据存储模块添加一个init()功能以允许其内部工作方式一样。然而,由于我们设计系统的模块化方式,如果我们重写用户界面模块以使用 GUI,其他模块将不会受到影响。

让我们从用户与系统交互的角度来思考我们的库存控制系统需要执行的各种任务:

  1. 用户需要能够选择要执行的操作。
  2. 当用户想要添加新的库存项目时,我们需要提示用户新项目的详细信息。
  3. 当用户想要删除库存项目时,我们需要提示用户输入要删除的库存项目的详细信息。
  4. 当用户希望生成报告时,我们需要能够向用户显示报告的内容。

让我们一次一个地完成这些交互:

  1. 要选择要执行的操作,我们将有一个prompt_for_action()函数,它返回一个字符串,标识用户希望执行的操作。让我们定义此函数可以为用户可以执行的各种操作返回的代码:

    |

    行动

    |

    动作码

    | | --- | --- | | 添加一个库存项目 | ADD | | 删除库存项目 | REMOVE | | 生成当前库存项目的报告 | INVENTORY_REPORT | | 生成需要重新订购的库存项目的报告 | REORDER_REPORT | | 退出程序 | QUIT |

  2. To add an inventory item, the user will need to be prompted for the details of the new item. Because an inventory item is defined as a given product at a given location, we actually need to prompt the user to choose both the product and the location for the new item. To prompt the user to select a product, we will use the following function:

    def prompt_for_product():
        ...

    用户将看到可用产品的列表,然后从列表中选择一个项目。如果他们取消,prompt_for_product()将返回None。否则,它将返回所选产品的产品代码。

    同样,为了提示用户选择位置,我们将定义以下函数:

    def prompt_for_location():
        ...

    再次显示可用位置的列表,用户可以从列表中选择位置。如果他们取消,我们返回None。否则,我们将返回所选位置的位置代码。

    使用这两个功能,我们可以要求用户识别一个新的库存项目,然后使用数据存储模块的add_item()功能将其添加到列表中。

  3. 由于我们将其作为一个简单的基于文本的系统来实现,因此删除库存项目的过程与添加项目的过程几乎相同:系统将提示用户输入产品和位置,并删除该位置的库存项目。因此,我们不需要任何附加函数来实现此功能。

  4. To generate a report, we will simply call the report generator module to do the work, and then we display the resulting report to the user. To keep things simple, our reports won't take any parameters, and the resulting report will be displayed in plain-text format. Because of this, the only user interface function that we will need is a function to display the plain-text contents of the report:

    def show_report(report):
        ...

    report参数只是包含生成报告的字符串列表。show_report()函数需要做的就是打印这些字符串,一次打印一个,以便向用户显示报告的内容。

本完成了用户界面模块的设计。本模块总共需要实现四个公共功能。

报表生成模块

报表生成模块负责生成报表。由于我们需要生成两种类型的报告,因此我们只需在报告生成器模块中提供两个公共函数,每种类型的报告对应一个函数:

def generate_inventory_report():
    ...

def generate_reorder_report():
    ...

每个函数都将生成给定类型的报告,并以字符串列表的形式返回报告内容。请注意,这些函数没有参数;因为我们尽可能简单,所以报告不会使用任何参数来控制如何生成它们。

主程序

主程序不是模块。相反,它是用户运行以启动系统的标准 Python 源文件。主程序将导入它需要的各种模块,并调用我们定义的函数来完成所有工作。从某种意义上说,我们的主程序是粘合系统所有其他部分的粘合剂。

在 Python 中,当源文件打算运行时(而不是被其他模块或从 Python 命令行导入和使用),通常对源文件使用以下结构:

def main():
    ...

if __name__ == "__main__":
    main()

程序的所有逻辑都写在main()函数中,然后由文件中的最后两行调用。if __name__ == "__main__"行是 Python 的一种魔力,它基本上意味着如果这个程序正在运行。换句话说,如果用户正在运行此程序,则调用main()函数来完成所有工作。

我们可以将所有程序的逻辑放在if __name__ == "__main__"语句下面,但将程序的逻辑放在单独的函数中有一些好处。通过使用单独的函数,我们可以在想要退出时简单地从该函数返回。它还使错误处理更容易,并且代码组织得更好,因为我们的主程序代码与检查我们是否实际运行程序的代码是分开的。

我们将用于我们的主程序,将所有实际功能放在一个名为main()的函数中。

我们的main()函数将执行以下操作:

  1. 对需要初始化的各个模块调用init()函数。
  2. 提供产品和位置的硬接线列表。
  3. 要求用户界面模块提示用户输入命令。
  4. 响应用户输入的命令。

步骤 3 和 4 将无限期重复,直到用户退出。

实施库存控制系统

现在我们已经对系统的总体结构有了很好的了解,我们的各个模块将是什么,它们将提供什么功能,现在是我们开始实施系统的时候了。让我们从数据存储模块开始。

实现数据存储模块

在方便的地方创建一个目录,在那里可以存储库存控制系统的源代码。您可能需要调用此目录inventoryControl或类似的名称。

在这个目录中,我们将放置各种模块和文件。首先创建一个名为datastorage.py的新的空 Python 源文件。这个 Python 源文件将保存我们的数据存储模块。

在为模块选择名称时,我们遵循使用所有小写字母的 Python 约定。一开始你可能会觉得有点尴尬,但很快就会变得容易阅读。请参考https://www.python.org/dev/peps/pep-0008/#package-和模块名称了解有关这些命名约定的更多信息。

我们已经知道,我们将需要八个不同的函数来组成此模块的公共接口,因此请继续向此模块添加以下 Python 代码:

def init():
    pass

def items():
    pass

def products():
    pass

def locations():
    pass

def add_item(product_code, location_code):
    pass

def remove_item(product_code, location_code):
    pass

def set_products(products):
    pass

def set_locations(locations):
    pass

pass语句允许我们将函数留空,这些只是我们将要编写的代码的占位符。

现在让我们来实现函数。这将在系统运行时初始化数据存储模块。由于我们将库存项目列表保存在内存中,并在它们发生变化时将其保存到磁盘上,因此我们的init()功能必须将库存项目从磁盘上的文件加载回内存中,以便在我们需要时可以使用它们。要做到这一点,我们将定义一个私有函数,我们将其称为_load_items(),并从init()函数中调用它。

提示

请记住,前导下划线表示某些内容是私有的。这意味着_load_items()功能不会成为我们模块的公共接口的一部分。

init()函数的定义更改为如下所示:

def init():
    _load_items()

_load_items()函数将把库存项目列表从磁盘上的一个文件加载到一个私有全局变量中,我们称之为_items。现在,我们通过在模块末尾添加以下内容来实现此功能:

def _load_items():
    global _items
    if os.path.exists("items.json"):
        f = open("items.json", "r")
        _items = json.loads(f.read())
        f.close()
    else:
        _items = []

请注意,我们将库存项目列表存储在名为items.json的文件中,并且我们正在使用json模块将_items列表从文本文件转换为 Python 列表。

提示

JSON 是保存和加载 Python 数据结构的一种很好的方法,生成的文本文件很容易阅读。由于json模块内置在 Python 标准库中,我们不妨利用它。

因为我们现在使用的是 Python 标准库中的一些模块,所以您需要在模块顶部添加以下import语句:

import json
import os.path

在进行此操作时,让我们编写一个函数将库存项目列表保存到磁盘。将以下内容添加到模块末尾:

def _save_items():
    global _items
    f = open("items.json", "w")
    f.write(json.dumps(_items))
    f.close()

由于我们已经将库存项目列表加载到一个名为_items的私有全局变量中,现在我们可以实现items()函数以使此数据可用。编辑items()函数的定义,如下所示:

def items():
    global _items
    return _items

现在让我们实现add_item()remove_item()函数,让系统的其余部分操作我们的库存项目列表。编辑这些函数,使其看起来如下所示:

def add_item(product_code, location_code):
    global _items
    _items.append((product_code, location_code))
    _save_items()

def remove_item(product_code, location_code):
    global _items
    for i in range(len(_items)):
        prod_code,loc_code = _items[i]
        if prod_code == product_code and loc_code == location_code:
            del _items[i]
            _save_items()
            return True
    return False

请注意,remove_item()函数返回True,如果项目被成功删除,则返回False;这将告诉系统的其余部分删除库存项目的尝试是否成功。

我们现在已经实现了datastorage模块中与库存项目相关的所有功能。接下来,我们将实现与产品相关的功能。

因为我们知道我们将要硬连线产品列表,set_products()函数将是微不足道的:

def set_products(products):
    global _products
    _products = products

我们只需将产品列表存储在名为_products的私有全局变量中。然后,我们可以通过products()功能提供此列表:

def products():
    global _products
    return _products

同样,我们现在可以实现set_locations()功能来设置位置的硬连线列表:

def set_locations(locations):
    global _locations
    _locations = locations

最后,我们可以实现locations()功能来提供这些信息:

def locations():
    global _locations
    return _locations

这就完成了datastorage模块的实现。

实现用户界面模块

正如前面提到的,用户界面模块将尽可能简单,使用print()input()语句与用户交互。在这个系统更全面的实现中,我们将使用图形用户界面(GUI)来显示和询问用户信息,但我们希望代码尽可能简单。

考虑到这一点,让我们继续实现我们的第一个用户界面模块功能。创建一个名为userinterface.py的新 Python 源文件来保存我们的用户界面模块,并将以下内容添加到此文件中:

def prompt_for_action():
    while True:
        print()
        print("What would you like to do?")
        print()
        print("  A = add an item to the inventory.")
        print("  R = remove an item from the inventory.")
        print("  C = generate a report of the current inventory levels.")
        print("  O = generate a report of the inventory items to re-order.")
        print("  Q = quit.")
        print()
        action = input("> ").strip().upper()
        if   action == "A": return "ADD"
        elif action == "R": return "REMOVE"
        elif action == "C": return "INVENTORY_REPORT"
        elif action == "O": return "REORDER_REPORT"
        elif action == "Q": return "QUIT"
        else:
            print("Unknown action!")

如您所见,我们提示用户键入与每个操作对应的字母,显示可用操作的列表并返回一个字符串,该字符串标识用户选择的操作。这不是实现用户界面的好方法,但它可以工作。

我们要实现的下一个功能是prompt_for_product(),它要求用户从可用产品代码列表中选择一个产品。为此,我们必须要求数据存储模块提供产品列表。在userinterface.py模块末尾添加以下代码:

def prompt_for_product():
    while True:
        print()
        print("Select a product:")
        print()
        n = 1
        for code,description,desired_number in datastorage.products():
            print("  {}. {} - {}".format(n, code, description))
            n = n + 1

        s = input("> ").strip()
        if s == "": return None

        try:
            n = int(s)
        except ValueError:
            n = -1

        if n < 1 or n > len(datastorage.products()):
            print("Invalid option: {}".format(s))
            continue

        product_code = datastorage.products()[n-1][0]
        return product_code

在这个函数中,我们显示一个产品列表,每个产品旁边都有一个数字。然后用户输入所需产品的号码,我们将产品代码返回给调用者。如果用户没有输入任何内容,我们将返回None-这允许用户在不想继续的情况下按enter键而不输入任何内容。

现在,让我们实现一个等效函数,要求用户识别一个位置:

def prompt_for_location():
    while True:
        print()
        print("Select a location:")
        print()
        n = 1
        for code,description in datastorage.locations():
            print("  {}. {} - {}".format(n, code, description))
            n = n + 1

        s = input("> ").strip()
        if s == "": return None

        try:
            n = int(s)
        except ValueError:
            n = -1

        if n < 1 or n > len(datastorage.locations()):
            print("Invalid option: {}".format(s))
            continue

        location_code = datastorage.locations()[n-1][0]
        return location_code

此功能再次在每个位置旁边显示一个数字,并要求用户输入所需位置的数字。然后,我们返回所选位置的位置代码,如果用户取消,则返回None

由于这两个函数使用了数据存储模块,我们必须在模块顶部添加以下import语句:

import datastorage

我们只需要实现另外一个功能:show_report()功能。现在让我们这样做:

def show_report(report):
    print()
    for line in report:
        print(line)
    print()

由于我们是使用文本接口来实现这个功能的,所以这个函数非常简单。不过,它确实有一个重要的用途:通过实现将报表显示为单独功能的过程,我们可以重新实现此功能,以更有用的方式显示报表(例如,在 GUI 中的窗口中显示报表),而不会影响系统的其余部分。

实现报表生成模块

报告生成器模块将有两个公共功能,一个用于生成每种类型的报告。无需进一步的 ado,让我们实现这个模块,它将存储在名为reportgenerator.py的 Python 源文件中。创建此文件,并在其中输入以下内容:

import datastorage

def generate_inventory_report():
    product_names = {}
    for product_code,name,desired_number in datastorage.products():
        product_names[product_code] = name

    location_names = {}
    for location_code,name in datastorage.locations():
        location_names[location_code] = name

    grouped_items = {}
    for product_code,location_code in datastorage.items():
        if product_code not in grouped_items:
            grouped_items[product_code] = {}

        if location_code not in grouped_items[product_code]:
            grouped_items[product_code][location_code] = 1
        else:
            grouped_items[product_code][location_code] += 1

    report = []
    report.append("INVENTORY REPORT")
    report.append("")

    for product_code in sorted(grouped_items.keys()):
        product_name = product_names[product_code]
        report.append("Inventory for product: {} - {}"
                      .format(product_code, product_name))
        report.append("")

        for location_code in sorted(grouped_items[product_code].keys()):
            location_name = location_names[location_code]
            num_items = grouped_items[product_code][location_code]
            report.append("  {} at {} - {}"
                          .format(num_items,
                                  location_code,
                                  location_name))
        report.append("")

    return report

def generate_reorder_report():
    product_names   = {}
    desired_numbers = {}

    for product_code,name,desired_number in datastorage.products():
        product_names[product_code] = name
        desired_numbers[product_code] = desired_number

    num_in_inventory = {}
    for product_code,location_code in datastorage.items():
        if product_code in num_in_inventory:
            num_in_inventory[product_code] += 1
        else:
            num_in_inventory[product_code] = 1

    report = []
    report.append("RE-ORDER REPORT")
    report.append("")

    for product_code in sorted(product_names.keys()):
        desired_number = desired_numbers[product_code]
        current_number = num_in_inventory.get(product_code, 0)
        if current_number < desired_number:
            product_name = product_names[product_code]
            num_to_reorder = desired_number - current_number
            report.append("  Re-order {} of {} - {}"
                          .format(num_to_reorder,
                                  product_code,
                                  product_name))
    report.append("")

    return report

不要太担心这些函数的细节。如您所见,我们从数据存储模块获取库存项目列表、产品列表和位置列表,根据这些列表的内容生成一个简单的基于文本的报告。

实施主程序

我们需要实现的系统的最后一部分是我们的主程序。创建另一个名为main.py的 Python 源文件,并在此文件中输入以下内容:

import datastorage
import userinterface
import reportgenerator

def main():
    pass

if __name__ == "__main__":
    main()

这只是我们主程序的整体模板:我们导入我们创建的各种模块,定义一个main()函数,在其中完成所有工作,并在程序运行时调用它。我们现在需要编写我们的main()函数。

我们的第一个任务是初始化其他模块,并定义产品和位置的硬连线列表。现在让我们这样做,重写我们的main()函数,如下所示:

def main():
    datastorage.init()

    datastorage.set_products([
        ("SKU123", "4 mm flat-head wood screw",        50),
        ("SKU145", "6 mm flat-head wood screw",        50),
        ("SKU167", "4 mm countersunk head wood screw", 10),
        ("SKU169", "6 mm countersunk head wood screw", 10),
        ("SKU172", "4 mm metal self-tapping screw",    20),
        ("SKU185", "8 mm metal self-tapping screw",    20),
    ])

    datastorage.set_locations([
        ("S1A1", "Shelf 1, Aisle 1"),
        ("S2A1", "Shelf 2, Aisle 1"),
        ("S3A1", "Shelf 3, Aisle 1"),
        ("S1A2", "Shelf 1, Aisle 2"),
        ("S2A2", "Shelf 2, Aisle 2"),
        ("S3A2", "Shelf 3, Aisle 2"),
        ("BIN1", "Storage Bin 1"),
        ("BIN2", "Storage Bin 2"),
    ])

接下来,我们需要询问用户希望执行的操作,然后做出适当的响应。我们将首先使用while语句询问用户操作,以便重复执行:

    while True:
        action = userinterface.prompt_for_action()

接下来我们需要响应用户选择的动作。显然,我们需要为每一个可能的行动这样做。让我们从QUIT动作开始:

break语句将退出while True语句,其作用是退出main()功能并关闭程序。

接下来,我们要实施ADD行动:

        if action == "QUIT":
            break
        elif action == "ADD":
            product = userinterface.prompt_for_product()
            if product != None:
                location = userinterface.prompt_for_location()
                if location != None:
                    datastorage.add_item(product, location)

请注意,我们调用用户界面函数来提示用户输入产品,然后输入位置代码,只有在函数未返回None时才继续。这意味着如果用户没有取消,我们只会提示输入位置或添加项目。

我们现在可以实现REMOVE动作的等效功能:

        elif action == "REMOVE":
            product = userinterface.prompt_for_product()
            if product != None:
                location = userinterface.prompt_for_location()
                if location != None:
                    if not datastorage.remove_item(product,
                                                   location):
                        pass # What to do?

这与添加项目的逻辑几乎相同,但有一个例外:datastorage.remove_item()功能可能会失败(通过返回False),如果该产品和位置代码没有库存项目。正如pass声明旁边的评论所暗示的,当这种情况发生时,我们将不得不采取一些措施。

在模块化编程过程中,我们现在已经达到了一个非常共同的点:我们设计了我们认为需要的所有功能,但后来发现我们遗漏了一些东西。当用户试图删除不存在的库存项目时,我们希望显示一条错误消息,以便用户知道出了什么问题。由于所有用户交互都发生在userinterface.py模块中,因此我们希望将此功能添加到该模块中。

我们现在就开始吧。返回编辑userinterface.py模块,最后增加以下功能:

def show_error(err_msg):
    print()
    print(err_msg)
    print()

再一次,这个是一个令人尴尬的简单功能,但它让我们可以将所有用户交互都保存在userinterface模块中(并允许稍后重写程序以使用 GUI)。现在,让我们用一些适当的错误处理代码替换main.py程序中的pass语句:

                    ...
                    if not datastorage.remove_item(product,
                                                   location):
 userinterface.show_error(
 "There is no product with " +
 "that code at that location!")

必须返回并更改模块的功能是非常常见的。幸运的是,模块化编程使这一过程更加独立,因此在执行此操作时,不太可能出现副作用和其他错误。

现在用户可以添加和删除库存项目,我们只需要执行另外两个操作:INVENTORY_REPORT操作和REORDER_REPORT操作。对于这两个操作,我们只需调用相应的报告生成器函数来生成报告,然后调用用户界面模块的show_report()函数来显示结果。现在让我们在main()函数的末尾添加以下代码:

        elif action == "INVENTORY_REPORT":
            report = reportgenerator.generate_inventory_report()
            userinterface.show_report(report)
        elif action == "REORDER_REPORT":
            report = reportgenerator.generate_reorder_report()
            userinterface.show_report(report)

这就完成了我们main()功能的实现,以及整个库存控制系统的实现。继续运行它。尝试输入一些库存项目,删除一两个库存项目,并生成这两种类型的报告。如果您已经输入了本书中的代码或下载了本章的示例代码,那么该程序应该可以运行,为您提供了一个简单但完整的库存控制系统,但更重要的是,它向您展示了如何使用模块化编程技术实现程序。

总结

在本章中,我们设计并实现了一个非平凡的程序来跟踪公司的库存。使用分而治之的方法,我们将程序分为各个模块,然后查看每个模块需要提供的功能。这使我们对每个模块内的功能进行了更详细的设计,然后我们能够一步一步地实现整个系统。我们发现有些功能被忽略了,必须在设计完成后添加,并了解了模块化编程如何降低这些类型的更改破坏系统的可能性。最后,我们对库存控制系统进行了快速操作,以确保其正常工作。

在下一章中,我们将进一步了解模块和包如何在 Python 中工作。