ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

面向对象编程实战:从类、封装、继承到SOLID设计原则的工程落地

2026/9/23 5:00:18 拓冰建站 浏览量
面向对象编程实战:从类、封装、继承到SOLID设计原则的工程落地 2. 核心细节解析与实操要点2.1 类的筛选不是所有东西都该变成对象很多初学者最容易犯的错就是恨不得把代码里每一个名词都变成类。User、Order、Product这些还好但有些人连StringUtils都要写成类——工具类确实有存在价值但它不该承载业务状态。我在实际项目里判断一个东西要不要建模成类就三个标准第一有没有独立的状态需要维护。比如订单的状态待支付、已支付、已发货这个状态会随着业务推进而变化那它就该是类。第二有没有一组高内聚的行为。比如EmailService它负责发邮件校验邮箱格式、拼接模板、调SMTP接口这些行为天然聚在一起那就用类包起来。第三会不会被多处复用。如果一段逻辑只在某个函数内部用到那把它写成类反而是过度设计。拿我最近做的一个电商项目举例。最开始我没有为“优惠券”单独建类直接在订单类里写了个discount字段结果后来要支持满减券、折扣券、免邮券订单类被改得面目全非。后来我专门抽了一个Coupon抽象类每种券一个子类订单类只管调用coupon.apply(amount)改动量瞬间小了很多。注意类的粒度过粗或过细都是灾难。过粗类内部逻辑互相纠缠过细整个项目几十个类每个类就一两行代码反而比过程式还难读。2.2 封装粒度属性该不该全用private教科书上通常会说“属性全部private通过getter/setter访问”但真实项目里我个人的经验是根据业务不可变性来决定。有些属性天生就不该被外部改比如订单号、创建时间。这些字段我甚至连setter都不给只在构造器里赋值。你在代码里看到order.setOrderNo(xxx)这种调用基本可以断定是设计出了问题——订单号是系统生成的凭什么让你随便改但有些属性则应该开放比如订单备注用户可以随时修改那给个setter没问题。还有一种情况内部属性需要“被打包”处理。比如地址它包含省市区和详细地址如果直接暴露四个字符串字段调用方每次都要自己拼。更好的做法是封装一个Address值对象提供getFullAddress()方法外部无感知地拿到完整地址。封装的核心不是“私有化”而是“控制变化点”。你把容易变化的逻辑藏在接口后面让调用方不感知变化这才是封装的价值。我见过很多同事写代码getter/setter全自动生成一个字段都没放过。这种“为了封装而封装”的写法实际效果和直接public字段没区别还多了几百行噪音代码。2.3 继承设计父类写什么、子类写什么、接口写什么继承是OOP里最容易被滥用、也最容易翻车的特性。我先说结论能用接口表达的契约尽量不要用继承去实现。继承适合表达“是什么”的关系is-a接口适合表达“能做什么”的关系can-do。举个例子“企鹅是一种鸟” —— 这是is-a但企鹅不会飞。如果你在鸟这个父类里定义了fly()方法企鹅子类就只能覆盖成抛异常这就是典型的继承误用。正确做法是Bird父类只有eat()、sleep()这些通用行为把fly()放到Flyable接口里。“订单可以取消” —— 这是can-do应该用接口而不是定义一个“可取消订单”父类。在规划继承结构时我的做法是三步走第一提取公共字段和方法。比如所有动物都有名字、年龄都吃饭睡觉这些放父类。第二识别变化点。叫声不同、移动方式不同这些不要试图在父类里统一实现留给子类覆盖。第三约束子类行为。父类里可以定义模版方法——把骨架定好具体步骤延迟到子类实现。比如makeSound()被定义为final它内部调用doMakeSound()调用子类的实现。实操心得写父类时尽量在文档注释里写清楚“子类必须覆盖哪些方法”“哪些方法不建议覆盖”“哪些是默认实现按需覆盖”。不要让后面接手的同事猜。3. 实操过程与核心环节实现3.1 完整案例设计一个简单的订单系统Java实现我拿一个项目中最常见的订单场景来演示OOP完整落地过程包括类和接口的设计、继承的使用、多态的展现。先看整体的类结构规划// 抽象类订单基类 public abstract class Order { protected String orderNo; protected BigDecimal amount; protected OrderStatus status; protected LocalDateTime createdAt; public Order(String orderNo, BigDecimal amount) { this.orderNo orderNo; this.amount amount; this.status OrderStatus.CREATED; this.createdAt LocalDateTime.now(); } // 模板方法定义订单处理的骨架 public final void processOrder() { validate(); calculateDiscount(); confirm(); } // 子类必须实现的校验逻辑 protected abstract void validate(); // 默认实现可按需覆盖 protected void calculateDiscount() { // 默认不打折 } private void confirm() { this.status OrderStatus.CONFIRMED; System.out.println(订单 orderNo 已确认金额 amount); } public String getOrderNo() { return orderNo; } public OrderStatus getStatus() { return status; } public BigDecimal getAmount() { return amount; } }这个抽象类里最核心的是processOrder()这个方法。它是一个模板方法把订单处理的流程固定下来先校验、再算折扣、最后确认。validate()是抽象方法每个子类必须自己实现calculateDiscount()有默认实现子类按需覆盖。为什么要这样做因为订单的校验逻辑完全不同——普通订单只需要检查库存秒杀订单还要检查限购数量、用户资格如果把这些逻辑全写在父类父类会变成一个巨大的“万能类”每加一种订单就要去改父类代码违反开闭原则。接下来定义两个子类public class NormalOrder extends Order { private String shippingAddress; public NormalOrder(String orderNo, BigDecimal amount, String shippingAddress) { super(orderNo, amount); this.shippingAddress shippingAddress; } Override protected void validate() { // 普通订单只有一个要求金额必须大于0 if (amount.compareTo(BigDecimal.ZERO) 0) { throw new IllegalArgumentException(订单金额必须大于0); } System.out.println(普通订单校验通过); } } public class SeckillOrder extends Order { private int userId; private static final int MAX_LIMIT 1; public SeckillOrder(String orderNo, BigDecimal amount, int userId) { super(orderNo, amount); this.userId userId; } Override protected void validate() { // 秒杀订单金额必须大于0且每个用户限购1件 if (amount.compareTo(BigDecimal.ZERO) 0) { throw new IllegalArgumentException(秒杀订单金额非法); } if (userId 0) { throw new IllegalArgumentException(非法用户); } System.out.println(秒杀订单校验通过用户ID userId); } Override protected void calculateDiscount() { // 秒杀订单直接半价 this.amount this.amount.multiply(new BigDecimal(0.5)); System.out.println(秒杀订单应用半价折扣); } }子类只做两件事实现自己特有的校验逻辑以及覆盖需要变更的折扣逻辑。新增一种订单类型只需要再写一个子类不需要改动父类。到这里多态的魅力就出来了。你可以在业务代码里这么写public class OrderService { public void handleOrder(Order order) { order.processOrder(); } }不管来的是普通订单还是秒杀订单handleOrder这个方法一行都不用改。新的订单类型加进来这个方法依然适用。这就是面向对象设计里“对扩展开放对修改关闭”的最朴素体现。3.2 接口定义在不改动主流程的前提下扩展功能上面的例子用抽象类已经能满足需求但如果你想让订单系统支持更多“横切”能力比如日志记录、消息通知接口会更灵活。我定义一个通知接口public interface Notifiable { void sendNotification(); }然后让订单类实现它public class NormalOrder extends Order implements Notifiable { // ... 前面的代码省略 Override public void sendNotification() { System.out.println(发送邮件通知用户订单号 orderNo); } }业务主流程不需要关心某个订单支不支持通知它只需要判断一下if (order instanceof Notifiable) { ((Notifiable) order).sendNotification(); }这个instanceof判断是安全的因为如果某个订单类型没实现Notifiable就说明它不需要通知逻辑。这比在抽象类里塞一个空的sendNotification()方法要干净得多。注意接口的设计原则是“小而专”。一个接口只表达一种能力不要搞出上百个方法的“上帝接口”。我常见到有人把UserService接口里塞了三十多个方法这本质上已经退化成类了接口隔离的意义荡然无存。3.3 Python版对照动态语言的OOP有什么不同同一个订单系统我再用Python写一遍。Python的OOP语法比Java轻量语义上也有些微妙差异。from abc import ABC, abstractmethod from datetime import datetime from enum import Enum from decimal import Decimal class OrderStatus(Enum): CREATED 1 CONFIRMED 2 CANCELLED 3 class Order(ABC): def __init__(self, order_no: str, amount: Decimal): self.order_no order_no self.amount amount self.status OrderStatus.CREATED self.created_at datetime.now() def process_order(self): self._validate() self._calculate_discount() self._confirm() abstractmethod def _validate(self): pass def _calculate_discount(self): # 默认不打折 pass def _confirm(self): self.status OrderStatus.CONFIRMED print(f订单 {self.order_no} 已确认金额{self.amount}) # 实现Notifiable接口的能力 def send_notification(self): raise NotImplementedError(当前订单类型不支持通知) class NormalOrder(Order): def __init__(self, order_no: str, amount: Decimal, shipping_address: str): super().__init__(order_no, amount) self.shipping_address shipping_address def _validate(self): if self.amount 0: raise ValueError(订单金额必须大于0) print(普通订单校验通过) def send_notification(self): print(f发送邮件通知用户订单号{self.order_no}) class SeckillOrder(Order): MAX_LIMIT 1 def __init__(self, order_no: str, amount: Decimal, user_id: int): super().__init__(order_no, amount) self.user_id user_id def _validate(self): if self.amount 0: raise ValueError(秒杀订单金额非法) if self.user_id 0: raise ValueError(非法用户) print(f秒杀订单校验通过用户ID{self.user_id}) def _calculate_discount(self): self.amount * Decimal(0.5) print(秒杀订单应用半价折扣) def send_notification(self): print(f发送短信通知用户订单号{self.order_no})Python里注意几件事命名约定不同。Java的抽象方法/公共方法没有命名区分Python用下划线开头表示“受保护/私有”是约定比如_validate。虽然Python没有强制私有但团队内部要遵守这个约定否则容易出现外部直接调内部方法的局面。Python的多态更自然。因为Python是鸭子类型只要对象有send_notification方法就能调用。我写业务代码时经常不判断类型直接用try-except兜底def handle_notification(order): notifier getattr(order, send_notification, None) if callable(notifier): notifier()这段代码不需要判断订单类型只需要看它有没有send_notification这个可调用属性。这种写法在Python社区很常见比instanceof和接口更动态。Python的抽象类强制程度低于Java。Java里子类没实现父类抽象方法根本编译不过Python里只要子类没有实例化过程漏掉抽象方法也不会立刻报错直到调用时才抛异常。所以用Python做OOP团队代码规范和自测就显得格外重要。4. 常见问题与排查技巧实录4.1 构造器里调用可覆盖方法隐藏的bug这是我调试过最隐蔽的一个OOP问题。看这段Java代码public abstract class Base { public Base() { init(); } protected abstract void init(); } public class Child extends Base { private String name default; public Child() { super(); this.name child; } Override protected void init() { System.out.println(name.length()); // 这里会空指针 } }你猜执行结果是什么NullPointerException。原因在于Java构造子类时先调用父类构造器而父类构造器里调用了子类覆盖的init()方法此时子类的字段还没来得及初始化name还是null。这个坑踩一次就长记性了构造器里不要调用可覆盖的方法。如果要初始化逻辑建议用一个private方法代替或者在子类构造器里显式调用。Python里也有对应的陷阱——在__init__里调用多态方法但此时子类属性还没定义class Base: def __init__(self): self.init() def init(self): pass class Child(Base): def __init__(self): super().__init__() self.name child def init(self): print(self.name) # AttributeError: Child object has no attribute name排查思路遇到这种问题先把构造器链路梳理一遍看父类构造器里有没有方法调用被子类覆盖。4.2 浅拷贝与深拷贝对象复制翻车现场面向对象编程里对象作为引用类型它复制时到底复制了什么常让人犯迷糊。看这个Python例子class Address: def __init__(self, city): self.city city class User: def __init__(self, name, address): self.name name self.address address addr Address(北京) u1 User(张三, addr) import copy u2 copy.copy(u1) # 浅拷贝 u3 copy.deepcopy(u1) # 深拷贝 u2.address.city 上海 print(u1.address.city) # 上海u2的address和u1是同一个对象 u3.address.city 广州 print(u1.address.city) # 上海深拷贝不会影响原对象实际问题里最常见的翻车场景是你复制了一个订单对象用于修改结果改动影响了数据库里原来的订单。排查这类问题优先检查对象内部是否含有可变引用类型列表、字典、自定义对象。Java里对应的还有Object.clone()的坑默认是浅拷贝要深拷贝必须自己重写clone()。实操心得能用不可变对象解决绝不开拷贝的口子。把字段设为final/不可变类型对象之间天然不会互相干扰。如果业务确实需要复制后修改我宁愿显式调用一个copy()工厂方法也不依赖语言默认的拷贝行为。4.3 equals/hashCode不配套集合操作的隐形炸弹这个坑主要针对Java。如果你重写了equals()但不重写hashCode()把对象放进HashSet或HashMap时会出现“两个逻辑相等的对象被反复添加”的诡异问题。我之前的一个业务代码里订单对象重写了equals比较金额和单号但忘了hashCode。结果在一个HashSetOrder里明明两个订单金额单号一样却能同时存在导致对账时重复计数。排查手段很简单在重写equals的类里检查hashCode是否同时被重写。用IDE的生成功能两个一起生成或者用Objects.hash()。Python里对应的可能是__eq__和__hash__的关系如果你定义了__eq__Python会自动把__hash__设为None导致对象不可哈希。需要手动定义__hash__。class Order: def __init__(self, order_no, amount): self.order_no order_no self.amount amount def __eq__(self, other): if not isinstance(other, Order): return False return self.order_no other.order_no and self.amount other.amount def __hash__(self): return hash((self.order_no, self.amount))5. 从面向对象语法到工程落地5.1 SOLID原则用五条规则检验你的设计面向对象编程学了语法不等于会设计。真正让代码活下来的是设计原则。我在代码审查时经常会拿SOLID原则逐条过一遍。单一职责S一个类只做一件事。我曾经接手过一个“上帝类”它既管用户登录、又管订单结算、还管短信发送整体超过两千行。改动任何一个逻辑都可能引爆另外两个功能。后来拆成三个类改动风险立刻降低。开闭原则O对扩展开放对修改关闭。这个在前面的订单示例里体现得很充分——新增订单类型不需要改已有代码。里氏替换L子类必须能替换父类且不破坏程序正确性。企鹅继承鸟但不会飞如果非要在鸟里定义fly()企鹅子类就会违反里式替换。设计继承关系时先问问自己子类能“完全替代”父类吗接口隔离I不要强迫使用者依赖它们用不到的方法。把大接口拆成小接口每个调用方只依赖自己需要的部分。依赖倒置D依赖抽象不依赖具体实现。业务代码里尽量不要new具体子类而是通过工厂或依赖注入把具体实现传进来。这里我列个自检清单写代码时对照一下检查项设计是否合理的追问类的职责这个类能不能用一句话说清楚它是干嘛的继承关系子类能完全替代父类吗是否存在“is-a”以外的继承理由接口粒度最小使用方依赖了多少方法有没有用不到的方法扩展方式新增需求时你需要修改旧文件还是新增文件依赖方向高层模块是否依赖于抽象依赖关系有没有反了5.2 组合优于继承什么时候不要用继承继承的坑这么多业界早就总结出了一条经验优先使用组合而不是继承。什么叫组合就是一个类持有另一个类的引用通过委托来复用功能。我举个例子。假设你要给订单增加“操作日志”能力记录谁在什么时候改了什么。用继承的话你可能会写一个LoggableOrder extends Order把日志逻辑塞进去。但如果以后还要支持“可导出的订单”“可审批的订单”类会发生爆炸LoggableExportableOrder、LoggableApprovableOrder、ApprovableExportableOrder……组合就简单了把日志、导出、审批都做成独立的能力类订单类按需持有它们public class Order { private final Logger logger; private final Exporter exporter; public Order(Logger logger, Exporter exporter) { this.logger logger; this.exporter exporter; } public void updateAmount(BigDecimal amount) { logger.log(修改订单金额 this.amount - amount); this.amount amount; } }用组合的好处是每个能力类独立变化、独立测试订单类只是把它们组织起来。需要加新能力订单类构造器加一个参数即可不需要制造一个新的子类。我判断该用继承还是组合就一个标准这个关系是永久的、本质的is-a还是暂时的、功能性的has-a。Student是Person用继承Order需要日志能力用组合。5.3 从“会写”到“会设计”刻意练习的几个层次最后聊点学习方法层面的东西。面向对象编程不是说你看完了教程、学会了类和对象语法就等于掌握了。它更像一种思维方式需要刻意练习。我自己的练法分三个阶段第一层读懂别人的OOP代码。不要只看业务逻辑要看作者为什么这样划分职责、为什么这里用继承而那里用接口、为什么这个方法是public那个是private。找一个优质开源项目逐行分析类之间的关系。第二层重构自己的过程式代码。把你以前纯函数式、脚本式写的工具代码尝试重新用类和对象组织。不需要改功能只改结构让代码从“一堆函数排排站”变成“一群对象互相协作”。这个过程会让你切身感受OOP带来的可维护性差异。第三层用OOP思考问题。拿到需求先在脑子里过一遍这个系统有哪些角色哪些数据属于同一个生命周期哪些行为将来可能变化等你想清楚了再打开编辑器动笔写代码。实操心得如果你连项目里“哪些东西会经常变”都判断不出来先把代码写出来跑通再回头重构。面向对象设计不是一次成型是在一次次重构中慢慢逼近“正确”的形状。没有谁是第一次设计就完美的。最后再分享一个小技巧写完一个类的第一版后先别急着往下写把这个类打印出来站在“三个月后的自己”的角度审视一遍——你能不能一眼看出这个类负责什么是否需要读完全部方法才能理解它如果答案是“要读很多遍”说明这个类的抽象层级还不够清晰继续拆。我在实际开发中几乎每个核心类都经历了三轮以上的重构才稳定下来这不是低效恰恰是把代码当成长期资产来打理的正确姿势。