刚带完一届学生的OOP课程,我发现一个很有意思的现象:很多人在学到"类和对象"的时候,觉得概念都懂,一做题就懵。抽象、封装、继承、多态,背得滚瓜烂熟,但真要动手写个像样的程序,还是无从下手。
所以我一直觉得,学面向对象的最好方式,不是刷概念题,而是早一点做一个完整的小项目。与其分布式地学一堆零碎语法,不如挑一个"麻雀虽小五脏俱全"的实战练手——简易通讯录就是这样一个绝佳的载体。
这堂课我想完整复盘一下我带着学生做通讯录的整个思路和落地过程。这个项目看起来简单,但恰好把"面向对象"和"文件持久化"这两个最核心的技能点串了起来:一方面用类和对象去组织业务逻辑,另一方面让数据能存下来、能读出来,程序关掉再打开,数据还在。这种"能跑起来、能存数据"的成就感,是纯语法练习完全给不了的。
这篇文章适合正在学面向对象但不知道怎么上手的初学者,也适合带学生的老师参考一下我的授课思路。我会把设计拆解、代码实现、坑点排查全部讲透,全程用一个可直接运行的Python项目为例。
1. 项目需求拆解与设计思路
先别急着写代码。做任何项目,第一步永远是搞清楚"我要做一个什么东西"。通讯录的需求看起来很直观,但如果只凭直觉直接干,很容易写出一个又臭又长的面条式代码——所有功能堆在几个大函数里,互相调用乱成一锅粥,后面加一个功能都得小心翼翼。
1.1 功能清单梳理:从用户视角出发
我让学生做的第一步,是把自己当成使用这个通讯录的人,把"希望这个软件能干什么"全部写下来。通常会得到这样一组需求:
- 能新增联系人,录入姓名、电话、邮箱等信息
- 能按姓名查到联系人,看到详细信息
- 能修改联系人的信息,比如换手机号了
- 能删除联系人
- 能列出所有人的信息,方便浏览
- 程序关掉再打开,数据不能丢
前五条属于业务功能,最后一条就是持久化的需求。没有最后一条,这只是一个内存玩具;加上最后一条,它才真正像一个"软件"。
很多初学者会忽略这个步骤,觉得是浪费时间。实际上梳理需求最大的价值,是让你明确"我要做什么",避免写着写着无限加功能——比如有学生做通讯录做着做着就想加分组、加头像、加标签,这个方向不是不对,只是对第一次练手来说,会严重拖慢进度。
1.2 为什么通讯录是OOP入门的最佳实战项目
我选通讯录作为实战案例,不是因为它新颖,恰恰是因为它够经典、够简单、够完整。它天然就有一个清晰的实体——"联系人",每个联系人都有属性(姓名、电话、邮箱),这几乎是为"对象"量身定制的模型。
更妙的是,它有两个关键操作点:一是对不同联系人的统一管理,二是数据的存取。这正好对应面向对象的两大核心思想:抽象和封装。
对比一下其它候选案例就能看出差别。图书管理系统比通讯录复杂,但有大量业务逻辑是围绕"借还"展开的,对初学者容易绕晕;学生成绩管理系统涉及排序、统计,容易把注意力引到算法上。通讯录的操作路径就是增删改查,没有任何弯弯绕,可以把全部注意力放在"怎么用类来组织代码"上。
1.3 建模之前先想清楚:这一层抽象值不值
这里我想多说一句"抽象"这件事。很多初学者抽象对象时,会把现实中所有的属性都塞进去——一个人有姓名、性别、年龄、生日、身高、体重、职业、爱好……全都写上。这其实是反模式。
面向对象里的抽象,不是"把现实搬进代码",而是"只保留当前系统需要的特征"。在通讯录场景里,一个人就是姓名、电话、邮箱;如果以后要加生日提醒,再加一个生日字段。简单说,模型服务于需求,而不是反过来。
这个理念我在课上反复强调,因为它是后面所有OOP设计的基石。你设计一个类,不是为了"像"现实,而是为了"够用且清晰"。
2. 核心类设计与面向对象落地
需求梳理清楚了,接下来是关键的建模阶段。这个阶段决定了整个项目的骨架优不优秀。如果类设计得合理,后面写功能会越写越顺;如果设计得糟糕,后面每加一个功能都会想重构。
2.1 类的职责划分:两个类的边界到底怎么切
带学生做通讯录时,我遇到的第一个争论是:到底需要几个类?
有学生认为一个Person类就够了,所有功能都写进去,什么添加、查找、保存全部堆里面。有学生认为应该严格分层,多搞几个类出来显得更面向对象。
我的建议是:类的数量以"职责清晰"为准,不迷信多,也别贪少。像通讯录这种体量,两个类就够了:
Contact类:负责描述"联系人"这个实体,只管数据的持有和展示格式,不碰业务操作。AddressBook类:负责管理一批联系人,实现增删改查,以及加载和保存数据。
有人可能会问,为什么不让Contact自己实现"修改自己的信息"?从表面上看,让contact.update(name, phone)好像也没毛病。但仔细想想,修改操作往往是"调用方先找到某一个联系人,再决定改什么",这个"找"的逻辑应该归通讯录管,而不是联系人自己管。把职责分开,后续维护起来才清晰。
还有学生问我,要不要再拆一个"文件存储类"?对这个体量的项目来说没必要,但我会在AddressBook里把"加载/保存"单独成方法,而不是散落在增删改查逻辑里。这样既简洁,又不至于把所有代码揉在一起。
2.2 Contact类:从数据类到业务实体的实现
Contact这个类承载的是"一个人"的所有信息。它的实现其实不复杂,但有几个细节值得注意。
我的第一版代码是这样的:
class Contact: """联系人实体类,负责存储单个联系人的信息""" def __init__(self, name, phone, email=""): self.name = name self.phone = phone self.email = email def __str__(self): return f"姓名: {self.name}, 电话: {self.phone}, 邮箱: {self.email or '未填写'}" def to_dict(self): """转换为字典,方便序列化存储""" return { "name": self.name, "phone": self.phone, "email": self.email, } @classmethod def from_dict(cls, data): """从字典创建联系人对象""" return cls(data["name"], data["phone"], data["email"])这里有一个细节值得解释:to_dict和from_dict这两个方法。它们的作用是把联系人对象和字典互相转换。为什么需要这一步?因为当我们要把数据存到文件里时,文件格式(我后面会讲,用的是JSON)本身不认识Python对象,它只认字典、列表这些基础数据类型。所以我们需要把对象"翻译"成字典,存进去;读出来时再"翻译"回对象。
这就引出了一个非常重要的面向对象知识点:对象本身不是数据,对象是数据的载体。持久化时,我们要的是"数据",取回来的数据如果想要继续操作,又得恢复成"对象"。这一来一回的转换,就是这个领域里经常说的"序列化与反序列化"。
第二个细节是__str__方法。Python里双下划线开头的方法叫"魔法方法",__str__决定了打印这个对象时显示什么样的字符串。我让学生必须重写这个方法,因为调试阶段可以随时print(contact)看到可读性良好的信息,而不是默认的那串内存地址。这个习惯看起来小,实际开发里能帮你节省大量排查时间。
2.3 AddressBook类:管理所有联系人操作
光有联系人模型还不够,还得有一个"容器"来管理所有的联系人。这个容器就是AddressBook类。它的核心是维护一个list,里面装若干个Contact对象。
class AddressBook: """通讯录管理类,负责联系人的增删改查以及数据持久化""" def __init__(self, storage_file="contacts.json"): self.storage_file = storage_file self.contacts = [] self.load() # 初始化时即从文件加载数据 def add_contact(self, name, phone, email=""): contact = Contact(name, phone, email) self.contacts.append(contact) self.save() return contact def find_contact(self, name): for contact in self.contacts: if contact.name == name: return contact return None def update_contact(self, name, new_phone=None, new_email=None): contact = self.find_contact(name) if not contact: return False if new_phone is not None: contact.phone = new_phone if new_email is not None: contact.email = new_email self.save() return True def delete_contact(self, name): for i, contact in enumerate(self.contacts): if contact.name == name: del self.contacts[i] self.save() return True return False def list_contacts(self): return self.contacts这个类里有几个设计上的决策,我觉得值得展开说。
首先是add_contact方法的实现。我故意让调用方传name, phone, email这几个字段,而不是传一个已经创建好的Contact对象。为什么不直接让调用方先创建对象再传进来?因为这样可以把"创建对象"的逻辑内聚在通讯录类里。调用方只需要关心"我要添加一个人",而不需要知道"我要创建一个Contact对象再放进列表"。这是封装的一种体现——调用方依赖的是AddressBook提供的能力,而不是内部数据的构造方式。
其次是每个修改操作后都调用self.save()。这意味着每次增删改,都会立即把数据同步到文件里。这么做有一个好处:程序崩溃了,数据也最多丢一条操作的数据,不会丢全部。坏处是频繁写文件,但对通讯录这种低频操作场景完全够用。我特别跟学生强调:持久化策略要和业务频率匹配。通讯录一天才加几条数据,每次都写文件是最稳妥的;如果是日志系统,一秒写几百次,这个策略就得改。
2.4 用接口思维理解类与类之间的协作
这节标题涉及的热词里有"面向对象接口"。虽然Python没有Java那种明晃晃的interface关键字,但接口思想在Python里同样存在,而且超好用。
我给学生解释接口时会说:接口就是"一份约定",它规定了这个类能干什么、怎么用,但不规定内部怎么实现的。在通讯录项目里,AddressBook对调用方暴露的方法(add, find, update, delete, list)就是一套接口。
为什么要强调这个?因为人在写代码时会忍不住依赖"内部实现"。比如有人往AddressBook里加数据,直接写book.contacts.append(Contact(...)),这就是破坏封装。表面上也没出错,但假设哪天contacts从list改成dict了,所有调用方全部崩溃。如果你都走add_contact这个接口,内部无论怎么改,外部调用代码一行都不用变。
说得再通俗一点:接口是"按钮",内部实现是"电路"。你要做的是按按钮,而不是掀开机箱去接电线。按按钮可能看起来多了一步,但它保证了安全和稳定。这个思维模式,在这个项目的编码过程里,会得到实实在在的锻炼。
3. 文件持久化的方案选型与技术实现
做完了类和对象的核心设计,下一个重头戏就是"文件持久化"。这也是很多初学者过不去的坎。因为业务逻辑还好理解,但"数据怎么存下来"涉及到文件读写、格式转换、编码问题,坑很多。
3.1 三种存储方案对比:为什么最后选了JSON
给我学生讲方案选型时,我列了三种常见方案,让他们自己分析优缺点。
第一是CSV格式。它本质上是用逗号分隔的文本文件,Excel可以直接打开,非常"亲民"。但CSV的问题在于它表达能力有限。比如联系方式以后想加个"多个电话",CSV就不好处理了。另外CSV没有天然的层级结构,嵌套数据写起来反人类。
第二是Pickle格式。这是Python自己的序列化方案,用法极其简单,两行代码就能把整个对象列表存下来。但它最大的问题是:生成的文件是二进制格式,人没法直接阅读;而且它强依赖Python版本,换版本可能读不了老文件。我个人觉得,作为学习项目,选pickle会损失"眼见为实"的反馈感——你存了之后打开文件看一眼,全是乱码,不利于理解持久化到底发生了什么。
第三是JSON格式。它和Python的字典语法高度相似,通俗易懂;通用性极好,不光是Python,任何语言都能解析;而且它是纯文本,出了问题可以用记事本直接打开看。对教学项目来说,JSON几乎是完美的选择。
方案对比总结如下:
| 方案 | 可读性 | 表达能力 | 适用场景 | 缺点 |
|---|---|---|---|---|
| CSV | 较好 | 弱,难表达嵌套结构 | 简单表格,Excel直开 | 复杂数据难表达 |
| Pickle | 差,二进制乱码 | 强,Python原生 | 临时存储,缓存 | 依赖Python版本,不可移植 |
| JSON | 好,文本可见 | 强,支持嵌套 | 配置文件、数据传输 | 部分类型(如自定义对象)需转换 |
我用这个表格跟学生强调的是:技术选型不是"哪个最好",而是"哪个最适合当前场景"。通讯录这个项目,我们希望数据能被检查、能被其他工具读取、格式本身不成为理解门槛,JSON就是最合适的。
3.2 序列化与反序列化:对象和数据的双向翻译
前面提过,JSON文件里存储的不是Python对象,而是基础的字典和列表。那么把对象变成字典、再变成字符串写进文件的过程,就是"序列化";反过来从文件读字符串、解析成字典、再组装成对象,就是"反序列化"。
在AddressBook里,这两个动作分别由save和load完成。我的实现是这样:
import json def save(self): """把通讯录里的联系人保存到JSON文件""" data = [contact.to_dict() for contact in self.contacts] with open(self.storage_file, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) def load(self): """从JSON文件加载联系人""" try: with open(self.storage_file, "r", encoding="utf-8") as f: data = json.load(f) self.contacts = [Contact.from_dict(item) for item in data] except FileNotFoundError: # 文件不存在说明是第一次运行,直接使用空列表 self.contacts = [] except json.JSONDecodeError: # 文件损坏时避免崩溃,用一个空列表继续跑 print("警告:存储文件损坏,已启动空通讯录") self.contacts = []这里有两个细节,是我教学中反复强调的重点。
第一个是ensure_ascii=False。如果不设置这个参数,json.dump会把所有非英文字符转成\uXXXX形式的Unicode转义序列。也就是说你在Python里看到的"张三",存到文件里会变成"\u5f20\u4e09"。虽然数据没丢,但人眼阅读文件极其痛苦。加了ensure_ascii=False,中文就按原样存下来。
第二个是异常处理。load方法里有两个except:一个处理文件不存在,一个处理文件损坏。前者是正常的"第一次运行",后者是"文件被人改坏了"。两种情况下程序都不应该崩溃,而是降级为"空通讯录"继续运行。这个设计思路叫"优雅降级",看起来只是几行代码,但它是一个程序是否具备鲁棒性的分水岭。
关于为什么初始化时要调用self.load(),我多说一句。很多学生把load()放在main函数里手动调,我总是反对。因为通讯录创建之后就应该处于"已就绪"状态,而不是"空壳"状态。如果每次都要手动调用load,就要求调用方必须记得这件事——而人是最容易忘记事情的。让AddressBook在构造时自动完成加载,是"封装"思想里"对象自管理"的体现。
3.3 以文本形式理解JSON结构
有一节实操课,我会让学生做一件事:程序跑完,打开contacts.json文件,亲眼看看里面的内容。通常是这样:
[ { "name": "张三", "phone": "13800138000", "email": "zhangsan@example.com" }, { "name": "李四", "phone": "13900139000", "email": "" } ]这个动作看着不起眼,但其实价值很大。第一,它让学生直观理解了"对象变成了什么"——每个联系人对应了一个字典,整体是一个列表。第二,一旦程序读取出了问题,你可以手动修改这个文件来排查是数据的问题还是代码的问题。
我还会让学生故意把JSON文件里加一个多余的逗号,再重新运行程序,看看会发生什么。这时候就会触发json.JSONDecodeError,程序打印出警告并启动空通讯录——这个过程让学生真正理解了"异常处理不是摆设,它是在真实场景中保命的"。
4. 完整功能实现与调用方代码设计
模型层建好了,持久化也搞定了,接下来就是把所有功能串起来,写一个能跟用户交互的程序入口。这一步同样有讲究。
4.1 主程序循环:命令分发器的设计
通讯录需要一个交互界面,最简单的方式是命令行菜单。我采用的是"命令分发"模式:用户输入命令,程序根据命令调用相应的方法。
def main(): book = AddressBook("contacts.json") while True: print("\n===== 简易通讯录 =====") print("1. 添加联系人") print("2. 查找联系人") print("3. 修改联系人") print("4. 删除联系人") print("5. 显示所有联系人") print("6. 退出") choice = input("请选择操作(1-6): ") if choice == "1": name = input("姓名: ") phone = input("电话: ") email = input("邮箱(可留空): ") book.add_contact(name, phone, email) print(f"联系人 {name} 添加成功!") elif choice == "2": name = input("要查找的姓名: ") contact = book.find_contact(name) if contact: print(contact) else: print(f"未找到联系人 {name}") elif choice == "3": name = input("要修改的联系人姓名: ") new_phone = input("新电话(留空则不修改): ") new_email = input("新邮箱(留空则不修改): ") if book.update_contact(name, new_phone or None, new_email or None): print("修改成功!") else: print(f"未找到联系人 {name}") elif choice == "4": name = input("要删除的联系人姓名: ") if book.delete_contact(name): print("删除成功!") else: print(f"未找到联系人 {name}") elif choice == "5": contacts = book.list_contacts() if not contacts: print("通讯录是空的,先添加一个吧。") else: for contact in contacts: print(contact) elif choice == "6": print("再见!数据已保存。") break else: print("无效的选项,请重新输入。") if __name__ == "__main__": main()这段代码看起来很长,但结构其实非常清晰:一个无限循环、一个条件分支、每个分支调用AddressBook的一个接口方法。这就是"MVC"思想的最简化版本——Contact和AddressBook是模型层,main函数是视图和控制器,负责接收输入、展示输出、调度模型。
我特意让学生留意一个细节:主程序里没有任何直接操作contacts列表或生成Contact对象的代码。所有动作都是通过book的各种方法完成的。这意味着,主程序只需要知道"book能干什么",完全不需要知道"book内部是怎么干的"。这种解耦带来的好处是:以后想换存储格式(比如从JSON换成数据库),只需要改AddressBook内部代码,主程序一行不动。
4.2 边界情况与用户输入校验
写交互程序最怕什么?最怕用户不按套路出牌。我带着学生做了一遍"恶意测试",逼他们正视输入校验的问题。
场景一:用户输入空姓名。如果允许空姓名的联系人入库,后面查找、修改都会出问题,因为你没法定位一个"无名氏"。所以我们至少要加一道简单的校验:
if not name.strip(): print("姓名不能为空!") continue场景二:用户输入重复姓名。同一个通讯录里,两个"张三"是合法的吗?从需求角度讲应该不允许。所以add_contact应该先检查重名。我在AddressBook里加了一个方法:
def find_contact(self, name): for contact in self.contacts: if contact.name == name: return contact return None然后在add_contact开头加判断:
if self.find_contact(name): print(f"联系人 {name} 已存在,添加失败") return None场景三:电话号码格式。严格校验电话号码需要正则表达式,对新手有点复杂。我建议先用一个最简单的宽松校验——检查长度大于5且纯数字:
if not phone.isdigit() or len(phone) < 5: print("电话格式不正确,至少5位数字") continue这样既不会劝退初学者,又保证了基本数据质量。别小看这些边界检查,它们是一个程序从"能跑"到"好用"的分水岭。
4.3 完整流程演示:从空文件到全功能可用
所有代码写完后,我习惯让学生在课堂上跟随我完整地跑一遍流程,亲眼看看每一步的效果。
第一次运行程序,contacts.json不存在,AddressBook初始化时捕获了FileNotFoundError,得到一个空通讯录。选5显示"通讯录是空的"。然后添加张三、添加李四,退出程序。此时打开contacts.json,能看到两条记录。
第二次再运行程序,数据自动加载回来了。选5,两个联系人都在。这看起来平淡无奇,但对初学者来说,这就是"见证奇迹"的时刻——程序关闭再打开,数据还在。很多学生做完这个演示才恍然大悟:原来"持久化"这三个字,背后就是这么一套扎实的机制。
接下来演示修改、删除,每做一步,都可以顺手打开JSON文件看看内容的变化。这种"操作→存储变化"的即时反馈,比任何抽象讲解都更让人印象深刻。
5. 项目踩坑实录与经验技巧
在这个项目的教学过程中,我遇到了大量学生共性问题。有的问题属于"一听就懂、一写就错"的类型,有的问题则是真实开发中才会遇到的隐藏雷。我整理了一下,挑几个最常见且最有价值的坑分享出来。
5.1 新人最容易踩的坑清单
第一个高频坑:文件编码问题。很多学生在Windows上用默认方式打开文件写数据,结果读回来的时候中文全是乱的。原因很简单——写入时用的编码和读取时不一致。解决办法就是我在代码里写的统一使用encoding="utf-8"。这个习惯越早养成越好,以后接触网页、数据库、接口,编码问题都是绕不过去的坎。
第二个高频坑:忘记在修改后保存。有学生写完update_contact,发现在内存里改了,但重启程序后数据又是旧的。排查半天,原来是忘了调用self.save()。我提醒他们:增删改之后,只要你想让这个改动"活到下次启动之后",就必须保存。可以把"save"想象成网文的"发布"按钮,你不点发布,观众永远看不到更新。
第三个高频坑:return和print混用。有些学生把find_contact写成"查到了就print出来",这样表面看效果一样,但其实把"数据返回"和"界面展示"混在了一起。如果以后要用这个查找方法来修改联系人,就麻烦了。所以领域里的原则是:模型层的方法负责返回数据,视图层(主程序)负责展示数据。
第四个坑比较隐蔽:文件名路径问题。如果通讯录程序放在桌面,但当前工作目录在别的地方,用相对路径contacts.json可能找不到文件。我建议在课堂上用一个简单粗暴的方式解决——把存储文件的路径作为AddressBook的构造参数传进去,这样调用方可以自己决定文件放哪,而不是在类里写死一个路径。这也是我前面代码里保留storage_file参数的原因。
5.2 调试技巧:不会写日志,就用好print
正规的工程级项目应该做日志记录,但对一个教学项目来说,我没要求学生上loguru或者logging模块,而是让他们先学会"有策略地print"。
调试阶段,我让他们在三个位置加print:一是每次save之后,打印"已保存X个联系人到文件";二是load之后,打印"已加载X个联系人";三是在关键的增删改操作前后各打印一条。这样整个程序运行时,各个阶段的状态就像心电图一样清晰可见,出问题马上能定位到是哪一步。
有学生问我,这样会不会太啰嗦?我的看法是:调试时的啰嗦,是为了换来排错时的清晰。真正上线时把这些print删掉就行,但开发阶段,清晰比简洁重要得多。
我还推荐另一个我常用的技巧:给AddressBook加一个临时的debug参数。当为True时,每个方法内部会打印操作详情;为False时保持安静。这样不用来回改代码,只要切换运行时参数就能控制输出量。代码很简单,但非常实用。
def add_contact(self, name, phone, email="", debug=False): ... self.save() if debug: print(f"[DEBUG] 已添加联系人 {name},当前总数: {len(self.contacts)}")这种"开关式调试输出"的思路,在真实项目里常常会演变为loguru的logger.debug()。虽然现在看起来简单,但它埋下的是"可观测性"这个工程思维的种子。
5.3 面向对象设计中的"过度设计"提醒
讲完了具体实现,最后我想专门提醒一个反向问题:过度设计。
有些学生在做完通讯录之后,觉得不过瘾,试图引入各种设计模式——单例模式管理AddressBook、观察者模式监听数据变化、工厂模式创建联系人……我看了之后哭笑不得。
对这样一个体量的代码,这些设计模式带来的不是便利,而是负担。每次加一个联系人要过三层抽象,每次读代码要跳五个文件。学习阶段,我们要做的是"恰到好处"的设计:用类来组织代码,用封装来隔离变化,用异常处理来提升健壮性,这就够了。等你的系统真的复杂到那一步,设计模式自然会有它的用武之地,而不是为了用而用。
还有一个我几乎每届都要强调的点:不要在这个阶段追求"一行流"代码。有些学生会把add_contact写成一行嵌套调用,觉得这样厉害。实际上,清晰可读的三行代码,永远好过炫技的浓缩一行。今后的真实开发里,你写的代码首先是要给同事看的,其次才是给机器跑的。
6. 项目扩展方向与进阶思考
如果通讯录这版做完想要更进一步,我有几个实操过的推荐方向,按难度递增排列。
第一,把存储格式换成SQLite。JSON文件能满足这个阶段的需求,但当你需要按条件模糊查找、多用户并发写、数据量上十万条时,文件存储就会开始吃力。SQLite是Python内置支持的轻量级数据库,连接、建表、增删改查的流程,和这个项目天然衔接,非常适合作为下一个台阶。
第二,给项目加一个简单的GUI界面。比如用tkinter做一个窗口程序,每个联系人是一行条目,点击按钮增删改查。这个扩展的价值在于理解"界面层和模型层的分离"——当你把命令行主程序换成GUI界面时,你会发现AddressBook类几乎不用改,只需要替换调用方式。这就是当初良好封装的回报。
第三,给Contact类增加分组和标签功能。这一扩展会引出一个新概念:类似"Group"的新类,以及Contact和Group之间的关联关系。它会让你的模型从"一对一"走向"一对多",思维方式也会从"单对象"走向"对象之间的关系"。这是真正进阶OOP设计的一步。
第四,引入单元测试。用unittest或pytest为AddressBook的增删改查写自动化测试。这不仅是验证功能正确性,更是倒逼你写出可测试的代码——如果你发现某个方法特别难测,那往往是设计的坏味道。
我个人在教学中的体会是:从完成通讯录这个项目开始,你会逐步意识到,面向对象给你的不是某种"语法",而是一种"组织代码的心智模式"。有了清晰的对象模型,增删改查只是接口方法的组合;有了文件持久化,程序的状态才真正跨越了单次运行的生命周期。这两件事打通了,后面学GUI、学数据库、学Web框架,都会有一种"原来如此"的顺滑感。
如果你刚开始做这个项目,卡住了不要慌。先把需求写清楚,再把类设计出来,最后才动手写具体逻辑。看着难,拆开做,每一步都没那么吓人。