CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
软件设计与架构入门/课时/面向对象
RawBlame
课时目录12 节

软件设计与架构入门

  1. 01整洁代码
  2. 02设计原则
  3. 03面向对象
  4. 04编程范式
  5. 05设计模式
  6. 06架构原则
  7. 07架构风格
  8. 08架构模式
  9. 09领域驱动设计
  10. 10企业模式
  11. 11综合架构练习
  12. 12路线图对照复习

面向对象

继承、多态与封装。

30 分钟

面向对象:继承、组合与多态

这一课你将能做到

  1. 用判题平台 CfUser、CfLesson、CfSubmission 三大领域对象讲清"是什么"、"有什么"、"能做什么"
  2. 判断一段关系该用继承还是组合,并用 Java 17 的 sealed 和 record 表达受限继承与值对象
  3. 写出"一个接口多种实现"的多态结构,让 JudgeEngine、SubmissionValidator 都能被替换

为什么要学

面向对象不是语法课,是"怎么把现实世界拆成代码"的课。判题平台里有用户、课程、提交记录、练习记录,它们之间既有"是一个"(ACSubmission 是 Submission),也有"有一个"(Submission 有代码、有状态、有提交时间)。用错继承会导致类树失控,用错组合会导致贫血对象。本课帮你建立判断标准。

先建立图像

继承(is-a)              组合(has-a)
─────────────            ─────────────
JavaSubmission           Submission
  is a                    has a CodeSnippet
  Submission                + has a UserRef
                            + has a JudgeResult

多态:JudgeEngine 接口
         /        \
  JavaJudge     PythonJudge
  同一个 judge()   不同行为

跟着做

步骤 1:区分继承与组合

// 继承:只有当"子类完全能替代父类"时才用
public abstract class CfUser {
    private final Long id;
    private final String nickname;
    private final LocalDateTime registerAt;

    public abstract boolean canAccess(CfLesson lesson);
}

public class FreeUser extends CfUser {
    @Override
    public boolean canAccess(CfLesson lesson) {
        return lesson.isFree();
    }
}

public class VipUser extends CfUser {
    private final LocalDateTime vipExpireAt;
    @Override
    public boolean canAccess(CfLesson lesson) {
        if (vipExpireAt.isBefore(LocalDateTime.now())) return false;
        return true; // VIP 可看全部
    }
}
// 组合:Submission 有多个部分,不是"是一种"的关系
public record SubmissionMeta(long userId, long lessonId, LocalDateTime submittedAt) {}

public class Submission {
    private final Long id;
    private final SubmissionMeta meta;
    private CodeSnippet code;
    private JudgeResult result;
    private SubmissionStatus status;

    public void assignResult(JudgeResult result) {
        this.result = result;
        this.status = result.isAccepted() ? SubmissionStatus.ACCEPTED : SubmissionStatus.REJECTED;
    }
}

步骤 2:用 sealed 限制继承

Java 17 的 sealed 让继承白名单化:

public abstract sealed class Lesson
    permits FreeLesson, VipLesson, TeamLesson {
    private final Long id;
    private final String title;
    public abstract boolean isAccessibleBy(CfUser user);
}

public final class FreeLesson extends Lesson {
    @Override public boolean isAccessibleBy(CfUser user) { return true; }
}
public final class VipLesson extends Lesson {
    @Override public boolean isAccessibleBy(CfUser user) { return user.isVip(); }
}
public final class TeamLesson extends Lesson {
    @Override public boolean isAccessibleBy(CfUser user) { return user.inSameTeam(this.teamId()); }
}

编译器确保只有这三类 Lesson,别处不能随意 extends Lesson,继承树永远可控。

步骤 3:多态实战——Validator 链

public interface SubmissionValidator {
    ValidationResult validate(Submission submission);
}

@Component
public class SyntaxValidator implements SubmissionValidator {
    @Override
    public ValidationResult validate(Submission s) {
        return s.getCode().isBlank() ? ValidationResult.fail("代码不能为空") : ValidationResult.ok();
    }
}

@Component
public class LengthValidator implements SubmissionValidator {
    @Override
    public ValidationResult validate(Submission s) {
        return s.getCode().length() > 100_000
            ? ValidationResult.fail("代码过长")
            : ValidationResult.ok();
    }
}

@Service
public class SubmissionService {
    private final List<SubmissionValidator> validators;

    public SubmissionService(List<SubmissionValidator> validators) {
        this.validators = validators;
    }

    public void submit(Submission submission) {
        for (SubmissionValidator v : validators) {
            ValidationResult r = v.validate(submission);
            if (!r.ok()) throw new IllegalArgumentException(r.reason());
        }
        // ... 入库
    }
}

Spring 自动注入所有 SubmissionValidator 实现,加新校验规则 = 新建一个 @Component,不改动 SubmissionService。

步骤 4:record 做值对象

判题结果是不变的值:

public record JudgeResult(boolean accepted, long timeMs, long memoryKb, String output) {
    public boolean isAccepted() { return accepted; }
}

record 自动生成 equals、hashCode、toString、不可变字段,天然适合值对象。

讲清楚要点

  1. 继承 = is-a,组合 = has-a:"Java提交 是 提交"用继承;"提交 有 代码"用组合。
  2. 优先组合:继承是强耦合——父类改签名,所有子类都要跟。组合可以运行时替换行为。
  3. sealed 控制继承:Java 17 用 sealed/permits/final/non-sealed 组合,让继承树在编译期就有边界。
  4. 多态是接口的能力:不是父类指针指向子类对象这么简单,而是"调用方不关心具体类型,只关心契约"。
  5. record 表达不可变值对象:DTO、结果集、坐标、金额都适合用 record,省去样板代码。

你可能会懵的地方

  • 什么时候该用继承? 只有当子类"完整替代父类"(LSP 成立)且变化方向单一(Lesson 类型)时才用。如果行为差异很大、变化方向多,用策略接口 + 组合。
  • sealed 会不会限制扩展? 会——这正是目的。如果你希望"只有这三种 Lesson",sealed 就是设计意图的代码化。以后真需要第四种,改 permits 就行,编译器会告诉你所有 switch 需要补 case。
  • 组合和依赖注入什么关系? 组合是"持有一个对象",DI 是"把对象从外部塞进来"。Spring 让你不用 new,但关系本质还是组合。
  • record 和 class 怎么选? 不变、值语义、无行为(或极少行为)→ record;有状态变化、需要自定义 setter、复杂继承 → class。
  • 多态和重载的区别? 多态(重写)是运行时根据对象实际类型决定行为;重载是编译期根据参数类型决定。多态是设计,重载是语法糖。

错法对照

错法问题正解
class Dog extends Animal 但 Dog 不能飞违反 LSP抽 Flyable 接口,能飞的才实现
class Utils extends BaseService工具类不该继承 Service组合:Utils 持有 Service 实例
所有字段都 public没有封装private + 必要 getter,业务方法封装规则
用 record 当 Entity(带 @Id)record 不可变,不适合有生命周期的实体Entity 用 class,DTO/值对象用 record
不用接口直接用实现类失去多态和替换能力List<JudgeEngine> 注入,而不是 JavaJudgeEngine

卡点急救

  1. 不知道该继承还是组合:问"子类是父类吗?"如果犹豫,先用组合;继承是最后手段。
  2. 类树越改越深:把"差异化行为"抽成接口,用组合持有接口引用,而不是每层都重写方法。
  3. Java 17 新特性不会用:sealed 解决"怕别人乱继承";record 解决"DTO 写太多样板"。先从这两个场景用起。

面试常问

  • Java 里多态是怎么实现的? 答:编译看类型(接口或父类),运行看实际对象。JVM 通过虚方法表(vtable)动态绑定。判题平台里 List<JudgeEngine> 注入后,运行时根据实际注入的实现类调用对应 judge()。
  • 组合优于继承,给一个场景? 答:Submission 有代码、有结果、有元信息,这些部分独立变化,用组合。如果用 JavaSubmission extends Submission、PythonSubmission extends Submission,将来要加"同时支持多语言提交"就炸了。
  • sealed class 解决了什么问题? 答:解决了"继承树失控"。之前 switch (lesson) 要补 default,sealed + permits 让编译器知道所有子类,switch 可以 exhaustive,同时防止外部乱扩展。

自测三问

  1. FreeLesson、VipLesson、TeamLesson 为什么适合用继承(sealed)?如果未来出现"按学习进度解锁的课程",你会怎么设计?
  2. Submission 为什么适合用组合?如果改成 JavaSubmission extends Submission 会有什么坑?
  3. 把 JudgeResult 改成 record 后,哪些好处是自动获得的?

小结一句话

继承表达"是什么"(严格受控),组合表达"有什么"(灵活优先),多态让调用方只认契约不认实现。

延伸

  • 《设计模式》GoF,第 1 章:组合优于继承
  • 《Effective Java》Joshua Bloch,第 18/20 条
  • JEP 360: Sealed Classes
  • JEP 395: Records
  • 下一课:编程范式——函数式与反应式在判题平台的应用。
加载本节练习…
← 上一课设计原则下一课 →编程范式