程序设计入门:从需求分析到代码实现的完整流程

SEO教程5小时前发布 jdkjadmin
0 00
程序设计入门:从需求分析到代码实现的完整流程

程序设计入门:从需求分析到代码实现的完整流程

编写程序并不只是把想法翻译成代码。一个看起来简单的功能,往往还涉及输入格式、异常情况、结果精度、使用方式等问题。若这些问题没有提前想清楚,程序可能“能运行”,却在真实输入下给出错误结果,或让使用者无法判断该怎么操作。

本文以一个命令行费用分摊工具为例,演示从需求分析到代码实现、测试和改进的基本过程。程序接收消费金额、小费比例和人数,计算含小费的总金额,并将金额按分分配给每个人。示例使用 Python 3,不依赖第三方库,适合初学者练习。它用于说明程序设计方法,不是税务、会计或支付系统。

一、先确认问题,而不是马上写代码

需求通常以自然语言出现,例如:“聚餐结束后,算一下每个人要付多少钱。”这句话表达了目标,却没有说明金额是否含小费、人数如何输入、遇到无法整除的情况怎么办。直接动手编码,往往会把这些未说明的假设偷偷写进程序。

开始设计前,可以先回答四个问题:

  • 谁会使用它? 本例面向需要快速分摊账单的个人用户,运行环境是终端。
  • 输入是什么? 消费金额、小费比例、分摊人数。
  • 输出是什么? 含小费的总金额,以及每个人应承担的金额。
  • 什么情况不接受? 金额不是有效数字、金额为负数、人数不是正整数、小费比例超出约定范围。

这里的“约定范围”必须由需求定义,而不是从技术上凭空得出。本例规定小费比例为 0% 到 100%,包括两端;金额不能为负,允许为 0;人数必须至少为 1。这些限制适用于本文示例,并不是所有地区、行业或费用分摊场景的通用规则。

二、把需求写成可验证的规则

一条好的需求能够通过输入和输出验证。我们将本例整理成下面的规则:

  1. 程序依次读取金额、小费比例和人数。
  2. 金额必须是非负的十进制数,最多保留两位小数;不接受科学计数法、逗号分隔符或货币符号。
  3. 小费比例必须是 0 到 100 之间的十进制数,最多保留两位小数。
  4. 人数必须是大于或等于 1 的整数,不接受小数。
  5. 先对“消费金额加小费”进行四舍五入,保留到分。
  6. 总金额换算成整数分,再尽量平均分配。无法平均分配时,余下的分依次分给前面的人,确保各人金额之和等于总金额。
  7. 无效输入要给出说明,不应打印错误的计算结果。

“先算总额再分配”是一个重要的业务选择。比如总额为 10.00 元、分给 3 人时,程序会分配为 3.34 元、3.33 元、3.33 元,总和仍是 10.00 元。若改成先计算每人份额、各自四舍五入,有时各人金额之和会与总额相差一两分钱。

规则写清楚后,也要明确不做什么。本例不处理不同货币的汇率与最小单位,不支持多人承担比例不同,不支持读取账单文件,也不涉及收款、转账或身份验证。范围越清楚,越容易判断当前实现是否足够。

三、设计处理流程

把程序想成一条从输入到输出的流水线,通常更容易实现,也更便于检查:

  1. 读取用户输入。
  2. 检查输入格式与范围。
  3. 把金额和比例转换为可精确处理的数值。
  4. 计算含小费总额,并按分进行舍入。
  5. 将总额换算为整数分,平均分配余数。
  6. 格式化并显示结果。

可以先用伪代码表达核心计算:

输入 amount、tip_percent、people
验证 amount、tip_percent、people
total = 按规则舍入(amount × (1 + tip_percent / 100))
total_cents = total 换算为分
每人基础分 = total_cents 整除 people
剩余分 = total_cents 除以 people 的余数
按顺序为前 remaining 人多分 1 分
输出总额和每个人的金额

伪代码不关心 Python 的语法细节,重点是让逻辑在编码之前变得可见。如果此时发现规则有歧义,修改伪代码通常比修改一段已经交织在一起的程序容易。

四、选择适合问题的数据类型

处理金额时,初学者常用浮点数,也就是 Python 的 float。浮点数适合很多近似计算,但十进制小数通常不能用二进制浮点数精确表示。例如某些计算可能得到接近 0.3、但并不严格等于 0.3 的结果。若金额经过多步计算,直接依赖浮点数可能导致意外的尾数或舍入差异。

本例使用标准库 decimal.Decimal 进行十进制运算,并使用 ROUND_HALF_UP 明确规定舍入方式。需要注意,任何舍入规则都是业务规则的一部分;若系统必须遵循特定地区的法律、合同或会计政策,应按相应要求确认舍入方法,而不能照搬示例。

另一个选择是用整数保存“分”。这是许多金额系统的常见思路:将 12.34 元保存为 1234 分,以整数进行加减和分配。但涉及比例计算时,仍须规定如何舍入。因此本文先用 Decimal 计算总额,再转成整数分进行分配。

五、实现可运行的 Python 程序

将下面代码保存为 split_bill.py,使用 Python 3 运行。程序只使用 Python 标准库,不需要安装额外依赖。

from decimal import Decimal, InvalidOperation, ROUND_HALF_UP


CENT = Decimal("0.01")
MAX_INPUT_DECIMAL_PLACES = 2


def parse_decimal(text: str, field_name: str) -> Decimal:
    """解析最多两位小数的十进制输入。"""
    value_text = text.strip()
    if not value_text:
        raise ValueError(f"{field_name}不能为空。")

    try:
        value = Decimal(value_text)
    except InvalidOperation as exc:
        raise ValueError(f"{field_name}必须是有效数字。") from exc

    if not value.is_finite():
        raise ValueError(f"{field_name}必须是有限数值。")

    decimal_places = max(0, -value.as_tuple().exponent)
    if decimal_places > MAX_INPUT_DECIMAL_PLACES:
        raise ValueError(
            f"{field_name}最多允许 {MAX_INPUT_DECIMAL_PLACES} 位小数。"
        )

    return value


def parse_people(text: str) -> int:
    """解析正整数人数。"""
    value_text = text.strip()
    if not value_text:
        raise ValueError("人数不能为空。")

    try:
        people = int(value_text)
    except ValueError as exc:
        raise ValueError("人数必须是整数。") from exc

    if people < 1:
        raise ValueError("人数必须大于或等于 1。")

    return people


def calculate_shares(
    amount: Decimal,
    tip_percent: Decimal,
    people: int,
) -> tuple[Decimal, list[Decimal]]:
    """计算含小费总额,并将总额按分平均分配。"""
    if not amount.is_finite() or amount < 0:
        raise ValueError("金额必须是非负的有限数值。")
    if not tip_percent.is_finite() or not (
        Decimal("0") <= tip_percent <= Decimal("100")
    ):
        raise ValueError("小费比例必须在 0 到 100 之间。")
    if people < 1:
        raise ValueError("人数必须大于或等于 1。")

    total = (amount * (Decimal("1") + tip_percent / Decimal("100"))).quantize(
        CENT,
        rounding=ROUND_HALF_UP,
    )

    total_cents = int(total * 100)
    base_cents, remainder = divmod(total_cents, people)

    shares = [
        Decimal(base_cents + (1 if index < remainder else 0)) / Decimal("100")
        for index in range(people)
    ]

    return total, shares


def main() -> None:
    try:
        amount = parse_decimal(input("消费金额:"), "消费金额")
        tip_percent = parse_decimal(input("小费比例(0-100):"), "小费比例")
        people = parse_people(input("分摊人数:"))

        total, shares = calculate_shares(amount, tip_percent, people)

    except ValueError as exc:
        print(f"输入错误:{exc}")
        return

    print(f"含小费总额:{total:.2f}")
    print("每人应付:")
    for index, share in enumerate(shares, start=1):
        print(f"  第 {index} 人:{share:.2f}")


if __name__ == "__main__":
    main()

代码里的 tuple[Decimal, list[Decimal]] 是 Python 3.9 及更高版本支持的类型注解写法。若运行环境较旧,可移除该返回类型注解,或使用 typing.Tuple 和 typing.List。程序本身不需要类型注解才能执行。

六、理解代码的几个关键点

1. 把输入解析和业务计算分开

parse_decimal 和 parse_people 负责把文本转换为程序需要的数据,并检查格式;calculate_shares 负责业务计算;main 负责终端交互和展示。这样的分工让计算函数可以独立测试,也避免把输入提示、校验、计算和输出全部塞进一个长函数。

函数不仅是为了让代码看起来整齐,更是为了明确责任。例如需要改成人数比例分摊时,可以重点修改计算逻辑;需要换成网页表单时,则可以复用计算函数,而替换终端输入输出部分。

2. 显式检查非有限数字

Decimal 可以表示 NaN 和无穷大等特殊值,不能只用“能成功转换”来判断输入有效。is_finite() 会排除这些值。对于金额和比例,程序还进一步检查业务范围,格式正确不代表业务上允许。

本例对金额允许零,但不允许负数;对小费比例允许 0 和 100。这些检查分别出现在解析流程和计算函数中。计算函数保留检查,是为了避免它被其他代码调用时绕过输入校验。真实项目中,应根据实际调用边界决定如何组织校验,但不能假设所有调用者都会先经过命令行解析。

3. 限制输入的小数位数

代码只允许输入最多两位小数。因此 12、12.3 和 12.34 可接受,12.345 会被拒绝。该限制是示例的输入政策,不是 Decimal 的技术上限。若业务需要允许更精细的金额,应该同时调整输入限制、单位换算、舍入规则和测试,不能只放开一处。

示例不接受 1,000.00、¥10、空格分隔的格式,也不支持科学计数法。这是为了让输入规则简单且明确。若要支持这些格式,应先定义地区格式、货币符号和逗号的含义,再实现可靠的解析,避免把格式错误的文本误读成另一个数值。

4. 用余数保证分配结果相加正确

divmod(total_cents, people) 同时得到每人基础分和余数。例如 1000 分分给 3 人,结果是基础 333 分、余数 1 分。程序给第一人 334 分,其他人各 333 分。这个规则具有确定性,但也意味着列表顺序中的前几人可能多付一分。若产品要求随机分配、轮换分配,或由用户选择承担余数的人,应另行设计并明确规则。

代码先用 int(total * 100) 将已经量化到两位小数的总额转换为分,再对整数分进行分配。不要把“按每人金额单独四舍五入”误认为必然能保持总和一致。

七、运行程序并观察结果

在终端中进入保存文件的目录,运行:

python split_bill.py

某些系统中命令名称是 python3,可以相应运行:

python3 split_bill.py

输入消费金额 10.00、小费比例 0、人数 3,预期输出如下:

消费金额:10.00
小费比例(0-100):0
分摊人数:3
含小费总额:10.00
每人应付:
  第 1 人:3.34
  第 2 人:3.33
  第 3 人:3.33

再试试以下情况,确认程序行为符合需求:

  • 金额输入 -1:提示金额不能为负数。
  • 比例输入 100.01:提示比例超出范围。
  • 人数输入 2.5:提示人数必须是整数。
  • 金额输入 abc:提示金额必须是有效数字。
  • 金额输入 0、比例输入 0、人数输入 3:总额和各人金额均为零。

八、测试不仅是“跑一下看看”

手动试运行有价值,但不够全面。测试应覆盖正常情况、边界情况和错误输入,尤其是会改变程序分支的情况。对于本例,至少要测试:刚好整除、不能整除、零金额、比例范围的上下界、非法小数位、非正人数,以及分配结果总和是否等于总额。

下面是一个可独立运行的测试文件。保存为 test_split_bill.py,并确保它与 split_bill.py 在同一目录:

import unittest
from decimal import Decimal

from split_bill import calculate_shares, parse_decimal, parse_people


class SplitBillTests(unittest.TestCase):
    def test_remainder_is_distributed_and_sum_is_preserved(self):
        total, shares = calculate_shares(
            Decimal("10.00"),
            Decimal("0"),
            3,
        )
        self.assertEqual(total, Decimal("10.00"))
        self.assertEqual(
            shares,
            [Decimal("3.34"), Decimal("3.33"), Decimal("3.33")],
        )
        self.assertEqual(sum(shares), total)

    def test_tip_is_included_before_splitting(self):
        total, shares = calculate_shares(
            Decimal("100.00"),
            Decimal("10"),
            2,
        )
        self.assertEqual(total, Decimal("110.00"))
        self.assertEqual(shares, [Decimal("55"), Decimal("55")])

    def test_zero_amount_is_allowed(self):
        total, shares = calculate_shares(
            Decimal("0"),
            Decimal("0"),
            2,
        )
        self.assertEqual(total, Decimal("0.00"))
        self.assertEqual(shares, [Decimal("0"), Decimal("0")])

    def test_negative_amount_is_rejected(self):
        with self.assertRaises(ValueError):
            calculate_shares(Decimal("-1"), Decimal("0"), 1)

    def test_tip_above_limit_is_rejected(self):
        with self.assertRaises(ValueError):
            calculate_shares(Decimal("10"), Decimal("100.01"), 1)

    def test_too_many_decimal_places_are_rejected(self):
        with self.assertRaises(ValueError):
            parse_decimal("1.234", "金额")

    def test_non_integer_people_are_rejected(self):
        with self.assertRaises(ValueError):
            parse_people("2.5")


if __name__ == "__main__":
    unittest.main()

运行测试:

python -m unittest -v

测试使用的 assertEqual 不只检查程序有没有抛出异常,也检查了结果的具体内容,以及分配后的金额之和是否等于总额。对金额分配程序来说,“总额守恒”是比单独检查某个人应付多少更关键的性质之一。

九、常见错误与修正思路

错误一:未经确认就把输入当作正确

如果直接调用 float(input(...)),用户输入空字符串、字母或不支持的格式时,程序会抛出异常。捕获并处理预期内的输入错误,让用户得到明确反馈;不要用宽泛的 except Exception 把所有问题都吞掉,否则代码缺陷也可能被伪装成普通输入错误。

错误二:金额使用浮点数并随意格式化

round()、格式化小数位数和业务舍入并不是同一件事。格式化只能改变显示形式,不能自动证明计算过程符合目标规则。涉及金额时,应明确采用的数值表示、计算顺序和舍入方式,再针对规则编写测试。

错误三:只检查界面输入,不检查核心函数

如果计算函数被其他程序导入调用,命令行解析处的限制可能完全不起作用。核心业务函数保留关键参数检查,能够减少调用者误用时产生的问题。不过,检查不能替代接口设计,也不能保证函数在所有环境中都安全。

错误四:把实现细节当成需求

例如“余下的一分给第一个人”是一个具体实现规则,不一定符合所有使用者的预期。在需求阶段应明确谁承担余数、能否选择人员、是否需要公平轮换。没有说明时,即使程序算术正确,也可能在业务上不合适。

错误五:只测典型输入

只测试 100 元、两个人这样的整除情况,无法发现余数分配错误。测试至少要包括边界值和异常值。对输入范围为 0 到 100 的比例,应测试 0、100、低于 0 和高于 100;对人数,应测试 1、0、负数和非整数。

错误六:过早追求抽象

初学者有时会在一个小程序里加入配置框架、插件、复杂类结构和多层继承。抽象应该回应已知变化,而不是为了显得“工程化”。本例用几个职责清晰的函数即可;当程序增加货币、多人不同权重或文件输入时,再根据实际需要提取新的模块。

十、参数限制与安全边界

本例的参数限制包括:金额非负且最多两位小数,小费比例在 0 到 100 之间且最多两位小数,人数为正整数。代码没有规定金额或人数的业务上限。因此,极端大的输入仍可能产生不符合实际需要的结果或消耗过多资源。若程序面向真实用户,应根据业务容量设置合理上限,并验证输入长度,尤其是处理外部传入数据时。

输入校验是数据质量控制,不等于安全认证,也不等于支付安全。示例只读取终端文本,不执行用户输入,也不访问文件、网络或数据库;它没有用户账户、权限控制、加密、审计日志或防欺诈机制。因此不能把它用于存储敏感支付信息、发起扣款或作为生产结算系统。

即使未来把输入改为网页或 API,客户端限制也不能代替服务端校验。服务端应重新验证类型、范围和业务约束;如需保存或处理敏感数据,还必须按具体产品和适用规则设计权限、加密、留存和审计措施。不能仅因某段输入“看起来是数字”,就认为它适合进入财务流程。

此外,人数没有业务上限,意味着用户输入很大的数量时程序会创建相应数量的结果元素,占用内存并生成大量输出。生产系统应依据使用场景设置人数上限,并测试上限处的行为。这个限制不是 Python 自动替开发者做出的,而是需求和运行环境共同决定的。

十一、从示例迈向更完整的程序

当需求扩大时,可以按变化逐步改进,而不是一次性增加所有功能:

  • 支持多种货币:明确币种、最小单位、显示符号和舍入规则。不能默认所有货币都以两位小数表示。
  • 支持不同比例分摊:定义比例输入方式、总和是否必须为 100%,以及无法分到整分时如何处理。
  • 支持历史记录:先说明保存内容、保存位置、保留时间和访问权限,再决定文件或数据库格式。
  • 提供图形界面或网页界面:将计算逻辑与界面分离,复用业务函数,并在服务端重复验证输入。
  • 适配正式结算:确认实际业务规则、审计要求和适用政策,通过领域专家审查,并为并发、权限和数据完整性设计专门机制。

每次扩展都可能改变边界条件。例如支持其他货币后,输入小数位数就不能继续固定为两位;支持比例分摊后,余数分配规则也会不同。因此要同步更新需求、代码和测试,而不是只补一个新按钮或新参数。

十二、可复用的程序设计流程

费用分摊只是练习案例,背后的方法可以用于许多小型程序。遇到一个新问题时,可以按以下顺序推进:

  1. 描述目标:程序要为谁解决什么问题?使用环境是什么?
  2. 列出输入和输出:输入类型、单位、格式和预期结果是什么?
  3. 定义边界:哪些值允许?空值、极端值、非法格式如何处理?
  4. 选定数据表示:根据问题选择整数、十进制数、日期或其他类型,避免未经考虑地使用不合适的数据类型。
  5. 拆解步骤:先用列表或伪代码表达处理流程,再写具体语法。
  6. 划分职责:把解析、验证、计算和输出分开,让重要逻辑可以独立复用和测试。
  7. 实现最小版本:先满足已确认的核心需求,不要提前加入与目标无关的复杂功能。
  8. 测试正常、边界和异常情况:验证结果,也验证程序拒绝不合法输入时的行为。
  9. 复查安全与运行限制:确认程序是否接触敏感数据、外部输入、网络、文件或资源上限。
  10. 根据反馈迭代:将新发现的规则写回需求和测试,避免让关键约定只存在于代码作者的记忆里。

一个可靠的小程序,不是代码行数越多越好,而是目标明确、边界说得清楚、重要规则能够被验证。对于初学者而言,先学会把需求拆成可检查的输入、处理规则和输出,再练习用小函数实现并测试,通常比一开始追求复杂架构更有效。掌握这条流程后,无论编写费用工具、文本处理脚本还是简单的数据转换程序,都能少一些猜测,多一些可解释、可复现的结果。

© 版权声明

相关文章