首页 / 资讯中心 / 文章详情

Python类和对象:从语法到建模的避坑指南

Python类和对象:从语法到建模的避坑指南 ★ FEATURED ARTICLE
类和对象这几个字我在不同的学习笔记里抄过不下五遍真正开窍是在第三遍。标着 xinixn 的那本册子上关于类的第一页密密麻麻写着 class、__init__、self旁边画了一堆箭头现在回头看那些箭头至少有一半是错的。这篇东西只聊一件事类和对象——它到底是什么、能解决什么麻烦、学的时候该按什么顺序下手以及一个新手到中级之间最容易反复栽跟头的地方在哪。适合刚学完函数和循环、正准备跨进面向对象这道门槛的人也适合那些语法都会写、一到自己设计就不知道从哪下手的人。我不会只给你贴一段教科书式的定义而是把类当成一件工具来拆它为什么被造出来、什么场景下值得用、什么场景下用了反而更糟。读完你至少能做到两件事一是看到一个需求能判断该不该用类二是写出来的类别人接手时不用猜每个属性是干什么的。1. 类和对象解决的到底是什么问题1.1 从一段到处复制的代码说起假设要处理一批商品数据每个商品有名称、单价、库存需要算总价、判断是否缺货、按折扣改价。不用类最直接的写法是三个平行的列表或者一堆字典。刚开始只有两三个商品的时候这种写法很舒服names []、prices []、stocks []谁都能看懂。问题出在第四个字段加进来的时候你得同时改五六个函数的参数列表任何一个地方漏改数据就对不上了。再往后函数开始变多calc_total(name, price, stock)、apply_discount(name, price, stock, rate)、check_stock(name, price, stock)每个函数都拖着一串参数看着就累。这种写法的本质问题是数据和操作它的逻辑被拆散了。数据在一处逻辑在另一处两者之间靠参数列表这条线连着。线越多越容易断。类的第一个价值就在这儿——把一组总是同时出现的数据和总是作用于这组数据的函数捆成一个整体。捆完之后calc_total不再需要三个参数它只需要知道自己属于哪个商品。判断要不要用类我自己的经验标准是如果一组数据在超过三个函数里反复成组出现那就该考虑封装了。这里有个反面的坑要说清楚不是所有东西都该做成类。一个只做数学计算、不持有状态的工具函数塞进类里变成MathUtil.add()纯属给自己找麻烦。类适合有状态、且状态会变的东西。一个纯函数式的转换逻辑写成模块级函数反而更清晰、更好测试。我见过不少新手刚学完类恨不得把所有的代码都包进 class 里结果写出来一堆只有一个方法的类那不如直接写函数。1.2 图纸与房子类、对象、实例化三者的关系类class是图纸对象object也叫实例 instance是按图纸盖出来的房子。这个比喻老掉牙但它精准地解释了一个新手最容易混淆的点类本身不占业务数据的位置对象才占。一张户型图纸可以盖一百套房每套房里的家具摆放各不相同。对应到代码里类定义了有哪些房间——也就是有哪些属性每个对象各自持有自己的房间内容——也就是属性值类上定义的方法是所有对象共用的行为但执行时作用在各自的数据上。实例化就是照着图纸盖房这个动作语法上就是Product(键盘, 199.0, 20)这一下。这一下发生了三件事解释器分配了一块内存、把__init__里的初始化逻辑跑了一遍、返回一个指向这块内存的引用。很多教程只讲第一句创建对象剩下两句含糊带过结果学到后面self、引用传递、可变对象共享的时候全乱套。我用一张表把三个概念对齐一下这比背定义有用得多概念对应现实代码里的表现常见误解类图纸、模具class Product:以为类是一堆函数的集合对象/实例盖好的房子p Product(...)以为变量 p 装的是对象本身实例化盖房这个动作调用类名加括号以为只是调了个函数属性房子里的房间self.name以为类里写的变量都归对象方法房子里能做的事def total(self):以为方法跟普通函数没差别第三列最后一行那句以为变量 p 装的是对象本身值得单独说。在 Python 里p只是一个名字一个指向对象的引用。q p并不会复制出第二套房只是给同一套房挂了第二块门牌。改q.namep.name跟着变。这个认知不建立起来后面调试 bug 会非常痛苦。1.3 真正卡住人的不是语法是建模语法层面的类和对象说实话两个小时就能背完class关键字、__init__、self、点号访问。真正让人卡住一两个月的是另一件事——面对一个需求怎么决定要拆出哪几个类、每个类该有哪些属性和方法。这个能力叫建模它没有标准答案但有判断标准。我总结的判断标准有三条按优先级排单一职责一个类只因为一个原因被修改。如果需求变更时你发现改价格逻辑要动这个类改库存逻辑也要动这个类说明它承担了两个职责。高内聚类内部的属性和方法应该互相关联。如果某个属性只有一两个方法用得上其他方法碰都不碰它的归属可能有问题。低耦合类与类之间的依赖越少越好。A 类里直接new出 B 类并且深度调用 B 的内部细节一旦 B 改了A 就崩。举个具体的例子。要做个订单系统新手常见的第一版是这样一个巨大的Order类里面塞了商品列表、用户信息、优惠计算、库存扣减、日志打印、数据库保存七八百行。这版的典型症状是任何一个小改动都可能引发意料之外的连锁反应测试也没法写因为想测一个折扣计算得先构造出完整的订单和数据库连接。第二版通常按职责拆开Product管商品本身的属性和校验OrderItem管某商品买几个Order管条目集合和总价Discount单独管折扣策略持久化交给另一个Repository。拆完之后每个类都能独立测试改折扣算法不用碰订单结构。这个拆分过程不是一次到位的我自己的习惯是先按最直觉的方式写出来写到觉得别扭的地方再回头拆。别扭感就是建模信号。2. 最小可用闭环把一个类从无写到能跑2.1 属性和方法的边界怎么划拿到一个类第一件事是分清楚哪些东西该存成属性、哪些该做成方法。判断逻辑很简单属性是它有什么方法是它能做什么。但这个简单逻辑背后有个容易忽略的取舍——什么东西值得存下来。拿商品举例。单价和数量必须存因为它们各自独立、随时可能变。总价要不要存两种做法都有人用存成属性下单时算一次之后直接读做成方法或属性方法每次读的时候现算。选哪种取决于总价的计算成本以及单价数量变化的频率。如果单价和数量随时会改存下来的总价就是个定时炸弹改完单价忘了同步总价账就错了。如果计算很贵且数据基本不变缓存下来才划算。class OrderItem: def __init__(self, name, price, qty): self.name name self.price price self.qty qty property def total(self): return round(self.price * self.qty, 2)这里用property是个很实用的做法对外看起来像属性item.total调用方不用写括号内部却是现算的永远不会出现数据不同步。代价是每次访问都要跑一遍乘法数据量小的时候完全无所谓。我的默认选择是派生数据一律现算除非profile证明它是瓶颈。过早缓存带来的同步bug远比一点乘法的开销昂贵。2.2__init__不是构造函数它只负责初始化这是个名词误用引发的大面积误解。在很多语言里确实有构造函数这个概念但在 Python 里真正创建对象的是__new____init__干的事情是在对象已经被创建出来之后往里面填初始数据。两者的区别在 99% 的场景下感觉不到但在两个地方会咬人一是在__init__里 return 非 None 值会直接报错二是继承不可变类型比如继承str、tuple时必须在__new__里改数据__init__里改不了。新手阶段记住一句就够__init__负责出生时该有的东西都要有。这句话的实际意义是——一个对象只要构造成功就应该处于可用状态不需要再调一个setup()或者init_data()才能用。我见过不少代码是这样的p Product() p.set_name(键盘) p.set_price(199) # 忘了调 set_stock后面一用就炸这种半成品对象是 bug 温床。正确的做法是把必需参数全放进__init__缺一个就构造不出来错误在第一时间暴露而不是等到二十行之后才炸。2.3self到底是什么为什么必须写self大概是新手第一个真正的认知障碍。它看起来像关键字其实不是它只是个约定俗成的参数名位置在方法定义的第一个代表调用这个方法时作用在哪个对象上。class Counter: def __init__(self): self.count 0 def add(self, n1): self.count nc Counter()之后调c.add(3)Python 内部实际做的是Counter.add(c, 3)。也就是说c被自动塞进了第一个参数。self这个名字存在的唯一目的就是让你在方法体里能引用到这个对象。为什么 Python 要把self显式写出来而不像某些语言那样藏起来因为它让这个方法是操作自身数据还是操作外部数据这件事变得一目了然。看到self.count n你就知道它改了对象状态看到纯参数的运算就知道它没副作用。这种显式性在排查并发和状态问题时非常值钱。有个小技巧类方法里如果整个方法体都没有出现self那这个方法八成不该是实例方法改成静态方法或直接挪出去当普通函数。2.4 类属性和实例属性一个能让程序悄悄出错的坑类体里直接写的变量是类属性所有实例共享一份在__init__里用self.xxx 赋值的是实例属性每个对象一份。这个区别平时不显眼一碰上可变对象就出事。class Cart: items [] # 类属性所有购物车共享 def add(self, item): self.items.append(item)这段代码的本意显然是每辆购物车有自己的商品列表但items写在类体里成了一个共享列表。c1.add(键盘)之后c2.items里也会神奇地出现键盘。更阴险的是如果改成self.items self.items [item]它反而又正常了——因为这是重新绑定会创建实例属性。同一个字段.append出 bug不出 bug这种不一致能让人调半天。正确的写法是把可变类型的初始值一律放在__init__里class Cart: tax_rate 0.08 # 类属性所有购物车共用的常量合理 def __init__(self): self.items [] # 实例属性每辆车独立判断标准很简单不可变的值数字、字符串、元组可以放类属性当常量可变的list、dict、set必须放__init__。3. 封装、继承、多态看它们各自挡掉什么麻烦3.1 封装的核心是收窄变更面封装经常被简化成属性前面加两个下划线这是对它的矮化。封装的真正目的是让外部代码只依赖少数几个稳定接口内部怎么改都不影响外面。举个现实中的例子。一个BankAccount类余额如果直接暴露成self.balance外部代码可以随意写account.balance 999999任何校验都形同虚设。加一层方法之后class BankAccount: def __init__(self, balance0): self._balance balance property def balance(self): return self._balance def withdraw(self, amount): if amount 0: raise ValueError(取款金额必须为正) if amount self._balance: raise ValueError(余额不足) self._balance - amount外面只能读account.balance想改必须走withdraw所有校验都在这一个入口里。哪天要加单日取款上限或者手续费只改这个方法就够了。Python 里的下划线是约定而非强制_balance表示别从外面碰__balance两个下划线会触发名称改写稍微硬一点但也不是真正的私有。这套机制依赖团队共识但正因为它是共识代码可读性反而更好——看到_开头老手一眼就知道这是内部实现。实战经验属性不要一上来就全做成 property。先用最直接的公有属性等到真的需要校验或者需要改计算方式时再升格成 property。因为从公有属性改成 property 是向后兼容的反方向才痛苦。3.2 继承的适用边界与组合的替代方案继承解决的是是一种的关系。SavingsAccount是一种BankAccountDog是一种Animal这种关系用继承没问题。但实际项目里大量继承其实是在表达有一个或者用得上这就用错了。判断要不要继承我一般问三个问题子类真的是父类的一种特例吗还是只是复用了父类的几个方法子类能不能完全替代父类出现的位置而不破坏调用方的预期如果我以后再加三个子类继承树会不会变得难以理解第二个问题对应的是里氏替换原则。如果Square继承Rectangle但set_width会同时改高度那把这个正方形塞进任何按矩形逻辑写的代码里都会出问题。这种继承表面省事实际是给未来埋雷。继承用得不合适的时候组合通常是更好的替代# 继承写法Logger 硬编码在类里 class OrderService(BaseService): def place(self): self.log(下单) # 组合写法把能力当依赖传进来 class OrderService: def __init__(self, logger): self.logger logger def place(self): self.logger.info(下单)组合的好处是依赖可替换、可打桩、可测试而且不会形成深层继承链。继承链一深改父类一个方法能影响到五层以下的子类这种间接影响极其难排查。我的经验法则是继承层级不要超过两层超过两层就退回去想想能不能用组合。3.3 多态同一句调用不同行为多态听着玄其实就是调用方写同一句代码传进去的对象不同实际跑的逻辑不同。class FixedDiscount: def apply(self, amount): return max(0, amount - 20) class PercentDiscount: def apply(self, amount): return round(amount * 0.9, 2) def checkout(amount, policy): return policy.apply(amount)checkout(100, FixedDiscount())和checkout(100, PercentDiscount())走的是同一行调用结果不同。这个能力带来的最大好处是——加新策略不用改调用方。以后要加满减、加阶梯折扣只需要新增一个类checkout一个字都不用动。attention 一下多态在 Python 里靠的是鸭子类型不要求这些类有共同的父类只要有同名方法就行。这带来了极大的灵活性代价是错误发现得晚——如果你把policy.apply写成policy.calc只有真正运行到那一行才会报错。缓解办法有两个一是给这组类定义共同的抽象基类二是写单元测试覆盖每种实现。我一般两个都做。3.4 魔术方法让自定义对象融入语言生态魔术方法双下划线包围的方法不是炫技用的它解决的是我写的对象怎么像内置类型一样自然地用这个问题。最值得先掌握的是四个方法触发时机实际用处__repr__打印、调试、REPL 里查看让日志里看到的是有用信息而不是一串地址__eq__用比较按业务含义判断相等而不是比内存地址__len__len(obj)集合类对象的自然长度语义__iter__for x in obj让对象可被遍历调用方代码更简洁__repr__的优先级最高几乎每个自定义类都该写。默认的Product object at 0x7f8a在排查问题时毫无帮助而Product(name键盘, price199.0)一眼就能看出内容。注意__repr__是给开发者看的理想情况下它返回的字符串能直接复制回代码里重建对象。__eq__有个配套的坑一旦重写了__eq__对象的哈希值默认就不可用了放进 set 或当 dict 的键会报错。这时候需要同时实现__hash__而且两者要遵守同一个约定——相等的对象必须哈希相同。如果只是想要一个数据载体类别手写这一堆方法直接上dataclass。dataclass装饰器会自动生成__init__、__repr__、__eq__加个frozenTrue连不可变和哈希都一并解决。4. 完整实操写一个经得起用的类4.1 需求拆解与建模决策光看片段学不会得完整写一遍。需求定成做一个购物车模块支持加商品、删商品、算总价、应用折扣、清空。约束是同一个商品重复加入要合并数量折扣策略可扩展任何非法输入都要在第一时间报错。建模上有几个决策点值得说明决策一商品和条目分开。商品描述的是这个东西是什么条目描述的是这个东西买了几个。合并数量这件事发生在条目上如果混在一起商品本身的单价会被条目数量污染同一个商品在不同订单里就没法共用了。决策二折扣独立成策略。不写死在购物车里也不写成if-else分支。理由是折扣规则变化最频繁独立出去之后新增规则不用碰购物车逻辑。决策三购物车只管容器职责。不负责持久化、不负责打印、不负责发通知。这些交给外部协调购物车保持纯粹测试才容易写。4.2 代码落地先写值对象部分from dataclasses import dataclass, field dataclass(frozenTrue) class Product: sku: str name: str price: float def __post_init__(self): if self.price 0: raise ValueError(f商品 {self.sku} 价格不能为负) if not self.sku: raise ValueError(商品编号不能为空) dataclass class CartItem: product: Product qty: int 1 def __post_init__(self): if self.qty 0: raise ValueError(数量必须为正整数) property def subtotal(self): return round(self.product.price * self.qty, 2) def merge(self, other): if self.product.sku ! other.product.sku: raise ValueError(不同商品不能合并) self.qty other.qtyfrozenTrue让 Product 不可变这带来两个隐性收益一是可以安全地作为 dict 的键需要配合__hash__dataclass 在 frozen 时自动生成二是杜绝了某个地方偷偷改了单价导致全盘算错的可能。价格调整的正确做法是造一个新 Product而不是改老对象。__post_init__里做校验是 dataclass 场景下校验的入口。它会在自动生成的__init__末尾被调用正好用来挡住非法值。这里我把校验放在构造期是为了让错误尽可能早地暴露——一个价格为负的商品对象根本不应该存在。再写购物车本体class Cart: def __init__(self): self._items {} self._discount NoDiscount() def add(self, product, qty1): new_item CartItem(product, qty) old self._items.get(product.sku) if old: old.merge(new_item) else: self._items[product.sku] new_item def remove(self, sku): if sku not in self._items: raise KeyError(f购物车中没有商品 {sku}) del self._items[sku] def set_discount(self, policy): if not hasattr(policy, apply): raise TypeError(折扣策略必须实现 apply 方法) self._discount policy property def subtotal(self): return round(sum(i.subtotal for i in self._items.values()), 2) property def total(self): return self._discount.apply(self.subtotal) def clear(self): self._items.clear() def __len__(self): return sum(i.qty for i in self._items.values()) def __iter__(self): return iter(self._items.values()) def __repr__(self): return fCart(items{len(self._items)}, total{self.total})几个设计点展开说说。内部用 dict 以 sku 为键存条目而不是用 list。理由是查重和删除都是 O(1)重复加同一商品时直接命中已有条目合并不用遍历。代价是保持了插入顺序Python 3.7 的 dict 保证顺序所以遍历展示时顺序稳定这个副作用正好有用。set_discount里用了hasattr做运行期检查这是 Python 里模拟接口约束的常用手段。检查放在设置时而不是计算时能保证错误发生的位置离引发它的代码足够近。如果等total算到一半才报AttributeError排查成本会高很多。4.3 折扣策略与边界处理折扣单独一组都用同一个协议class NoDiscount: def apply(self, amount): return round(amount, 2) class FullReduction: def __init__(self, threshold, reduce): if threshold 0 or reduce 0: raise ValueError(门槛和优惠额必须为正) if reduce threshold: raise ValueError(优惠额不能大于等于门槛) self.threshold threshold self.reduce reduce def apply(self, amount): if amount self.threshold: return round(amount - self.reduce, 2) return round(amount, 2) class PercentOff: def __init__(self, rate): if not 0 rate 1: raise ValueError(折扣率必须在 (0,1) 之间) self.rate rate def apply(self, amount): return round(amount * self.rate, 2)FullReduction里那句优惠额不能大于等于门槛的校验看着像多此一举实际很有必要。如果允许reduce threshold满 100 减 120 就会算出负数总价。这种配置错误在真实场景里非常常见——运营活动改个数字就上线了。把校验挡在构造期总价永远不会为负。PercentOff的0 rate 1这个区间同理。写成rate 1会允许零折写成rate 0会允许加价折扣。这类边界不是理论洁癖是上线后真会遇到的。4.4 用测试把行为钉住类写完了不算完得有测试证明它按预期工作。测试重点放在行为而非实现上def test_merge_same_sku(): c Cart() p Product(K001, 键盘, 199.0) c.add(p, 1) c.add(p, 2) assert len(c) 3 assert c.subtotal 597.0 def test_negative_price_rejected(): with pytest.raises(ValueError): Product(K002, 鼠标, -1.0) def test_full_reduction_below_threshold(): c Cart() c.add(Product(K003, 耳机, 80.0), 1) c.set_discount(FullReduction(100, 20)) assert c.total 80.0 def test_unknown_sku_remove(): with pytest.raises(KeyError): Cart().remove(NOPE)测试里最有价值的是第二条和第四条它们验证的是错误路径。很多人的测试只覆盖正常流程结果线上出问题全在异常分支。判断一个类的鲁棒性看它的测试里有几条是raises断言就知道了。5. 踩坑记录与排查手册5.1 可变默认参数与共享状态这个坑的经典程度值得单列。函数或方法定义里写def __init__(self, items[])那个空列表只创建一次之后所有调用共享同一个。# 错误示范 class Box: def __init__(self, items[]): self.items items a Box(); b Box() a.items.append(1) print(b.items) # [1] 见鬼了改成def __init__(self, itemsNone): self.items items if items is not None else []就对了。逻辑是None作为哨兵值每次调用时才新建列表。这个坑之所以难查是因为在单实例场景下完全测不出来只有当同一个类被实例化两次以上才会暴露。我踩过的最惨一次是在一个长期运行的服务里某个容器的默认列表被所有请求共享症状是数据随机串台复现概率极低。记忆口诀函数默认值只在定义时求值一次。所以默认值必须是不可变对象。5.2 对象引用、浅拷贝与深拷贝a b不复制对象。想要副本得用copy.copy()或者copy.deepcopy()。这两者的区别是浅拷贝只复制最外层容器里面的元素还是共享的深拷贝递归复制到底。import copy original Cart() original.add(Product(K001, 键盘, 199.0), 2) shallow copy.copy(original) deep copy.deepcopy(original) original.remove(K001) print(len(shallow)) # 依然正常因为 dict 是新的 print(len(deep)) # 也正常这个例子还不够刺眼因为 Cart 的内部元素被删了但对象本身还在。更麻烦的场景是浅拷贝后的两个对象共享内部元素改其中一个元素的属性另一个跟着变。判断要不要深拷贝的标准是拷贝之后两个对象是否有任何共享的可变状态。如果有且你不希望它们互相影响就得深拷贝。不过深拷贝不是免费的对象图大的时候性能开销可观而且碰到循环引用需要额外处理。更好的做法通常是——让对象尽量不可变压根不需要拷贝。这也是前面 Product 用frozenTrue的理由之一。5.3 继承链上的方法解析与 super 的坑Python 支持多继承方法查找走的是 MRO方法解析顺序不是简单的深度优先。super()也不是调用父类而是沿着 MRO 找下一个。这两点在单继承下看不出区别一碰多继承就出问题。class A: def hello(self): print(A) class B(A): def hello(self): print(B) super().hello() class C(A): def hello(self): print(C) super().hello() class D(B, C): pass D().hello() # B - C - A输出是 B、C、A而不是 B、A。因为super()沿着 MRO 走D 的 MRO 是 D、B、C、A。如果误以为super()就是调父类这里必然理解不了。__init__里的super().__init__()更要注意参数传递。经典错误是父类__init__只接受两个参数子类传了三个或者子类忘了调父类初始化导致父类的属性不存在。后者的报错信息通常是AttributeError: X object has no attribute y而这个y明明在父类里写过——第一次遇到时极其茫然。排查方法在子类的__init__第一行加个断点确认有没有走到super().__init__()再看两边参数列表是否匹配。5.4 常见问题速查表现象大概率原因处理方向两个对象的属性互相影响属性定义在类体里且是可变类型挪进__init__self.xxx报不存在分支里漏初始化或super().__init__没调检查所有分支路径改了一个对象另一个也变赋值只是引用没做拷贝明确拷贝意图重写__eq__后 set 报 unhashable没实现__hash__一并实现或frozenTruesuper()行为诡异多继承下 MRO 不是直觉顺序打印Cls.mro()看实际顺序对象打印出来是一串地址没写__repr__补上__repr__子类改了父类方法调用报错父类接口被改子类未同步检查覆写方法的签名一致性实例化时报参数过多__init__签名与调用不匹配逐项对齐参数这张表可以贴在显示器边上前四个基本覆盖了新手期 80% 的求助。遇到问题先对表对不上再去打断点。6. 练习路线从会写到会设计6.1 分阶段练习题设计光看不会必须动手。我按难度排一个路线每一关都卡在一个明确的能力点上。第一关只练实例化与属性。写一个Rectangle有宽高能算面积和周长。这一关的目标是把__init__、self、方法调用的手感练熟十分钟搞定。做完之后可以加个is_square属性方法体会一下property的用法。第二关练封装与校验。写一个Temperature内部用摄氏度存对外提供摄氏和华氏两个属性。要求设置非法值时抛异常。这一关练的是内部表示和外部接口可以不同以及校验该放在哪。第三关练容器类。写一个TaskList支持添加、删除、按状态筛选、统计完成率并且可迭代。这一关要练__len__、__iter__、__repr__以及内部选什么数据结构。第四关练策略解耦。给上面的TaskList加一个排序策略支持按标题、按优先级、按截止时间排序。策略要能独立新增不改TaskList本身。这一关过不了说明多态还没真正理解。第五关练组合与依赖注入。写一个仓库类TaskRepo负责存取TaskList通过构造函数接收一个 repo。然后用一个内存版 repo 写测试完全不用碰数据库。这一关练的是可测试性设计。五关走完基本能应付日常业务代码里 90% 的类设计。再往上就是设计模式和架构层面的事了那是后话。6.2 读源码时该盯什么自己写是一回事看别人怎么组织类是另一回事。读开源代码时别从头顺着读效率太低。有个更快的路径先找__init__看这个类出生时需要什么这就圈定了它的依赖范围。然后扫一遍所有方法名看它的职责边界在哪方法名里出现save和calc往往意味着职责混杂。接着看有没有 property有的话说明作者在意内部状态的一致性。最后看它被谁实例化、被传给谁这一圈下来类与类的关系图就出来了。我自己的习惯是拿一张纸边读边画箭头A 持有 B、C 继承 A、D 依赖 A 的接口。画完之后再看一遍如果某个类被十几个地方依赖那它的稳定性要求就很高改动必须慎重。6.3 代码评审视角的自查清单写完一个类提交之前过一遍下面这几条能挡掉大部分返工这个类的每个属性都有明确的存在理由吗有没有哪个属性只有一两个方法在碰构造出来的对象是不是一定能直接用有没有必须额外调用才能工作的半成品状态所有可能出错的输入都在入口拦住了吗外部代码必须依赖我内部细节的地方能不能减到更少如果明天要加一种新的类型或策略我需要改几个文件有没有哪个方法是看着不像这个类该干的事的比如一个订单类里出现了发邮件的逻辑。最后一条尤其值得警惕。职责漂移是渐进发生的今天加一行日志明天加一个通知半年后这个类就成了什么都会的怪物。定期回头看一眼比一次大重构便宜得多。说点个人的体会。类和对象这部分内容我反复看过好几轮每一轮的收获都不一样。第一轮记住的是语法第二轮想明白的是职责划分第三轮才开始理解为什么有些设计宁愿多写二十行代码也不肯多一层继承。真正推动我跨过门槛的不是背下三大特性而是有次维护一个八百行的巨型类改一个折扣逻辑要跑一遍全量测试还总提心吊胆最后花了两天拆成六个小类之后每次改动都很轻松。那种拆开之后世界清净了的感受比任何教程都好使。所以你如果现在正卡在语法都会但不知道怎么用的阶段别继续看教程了去找一段自己以前写的、超过两百行的函数试着把它拆成三个类拆的过程比看十篇文章都管用。
阅读完成 · 觉得有帮助?
咨询建站