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路线图对照复习

设计原则

SOLID、DRY 与 YAGNI。

30 分钟

设计原则:SOLID、DRY 与 YAGNI

这一课你将能做到

  1. 用判题平台真实业务讲清 SOLID 五条原则各自解决什么问题
  2. 识别代码里的"重复"和"过度设计",知道 DRY 与 YAGNI 的边界
  3. 给一个违反 SOLID 的类做最小重构,让它符合 OCP 和 DIP

为什么要学

如果说整洁代码是"一行代码怎么写",那设计原则就是"类和类怎么协作"。判题平台里 SubmissionService、JudgeEngine、NotificationSender、PracticeRecordRepository 之间的依赖关系、扩展点、复用方式,全都由这些原则决定。

面试几乎必考 SOLID,但光背定义没用——面试官会追问"举一个你项目里的例子"。这一课就给你备好例子。

先建立图像

S 单一职责   一个类只被一个角色驱动修改
O 开闭       对扩展开放,对修改关闭
L 里氏替换   子类必须能替换父类
I 接口隔离   别强迫实现方实现用不到的方法
D 依赖倒置   依赖抽象,不依赖具体

DRY    重复逻辑只写一次
YAGNI  用不到的别提前写

跟着做

步骤 1:违反 SRP 的"上帝类"

// 反例:SubmissionService 干了太多事
public class SubmissionService {
    public void submit(String code, long userId, long lessonId) { /* ... */ }
    public void judge(long submissionId) { /* ... */ }
    public void sendEmail(long userId) { /* ... */ }
    public void exportReport(LocalDate from, LocalDate to) { /* ... */ }
    public void updateVipExpire(long userId) { /* ... */ }
}

这个类会被"判题工程师、运营、报表开发、会员产品"四种角色修改,任何改动都可能影响别人。

重构:按角色拆成 SubmissionService、JudgeService、NotificationService、ReportService、MembershipService。每个类只对一个角色负责。

步骤 2:用 OCP 设计判题器

新增"Python 判题"时不改老代码:

public interface JudgeEngine {
    boolean supports(String language);
    JudgeResult judge(Submission submission);
}

@Component
public class JavaJudgeEngine implements JudgeEngine {
    public boolean supports(String language) { return "java".equals(language); }
    public JudgeResult judge(Submission s) { /* 编译、跑用例、对比输出 */ }
}

@Component
public class PythonJudgeEngine implements JudgeEngine {
    public boolean supports(String language) { return "python".equals(language); }
    public JudgeResult judge(Submission s) { /* ... */ }
}

@Service
public class JudgeDispatcher {
    private final List<JudgeEngine> engines;   // Spring 自动注入所有实现
    public JudgeDispatcher(List<JudgeEngine> engines) { this.engines = engines; }

    public JudgeResult dispatch(Submission submission) {
        return engines.stream()
                .filter(e -> e.supports(submission.getLanguage()))
                .findFirst()
                .orElseThrow(() -> new IllegalStateException("no engine for " + submission.getLanguage()))
                .judge(submission);
    }
}

新增语言 = 新增一个 @Component,老代码一行不动。这就是"对扩展开放,对修改关闭"。

步骤 3:用 DIP 切断对 MySQL 的硬编码

// 反例:业务层直接依赖 MyBatis Mapper
public class PracticeRecordService {
    @Autowired private PracticeRecordMapper mapper;   // 绑死 MySQL
}

// 正解:依赖抽象接口,由基础设施层实现
public interface PracticeRecordRepository {
    Optional<PracticeRecord> findByUserAndLesson(long userId, long lessonId);
    void save(PracticeRecord record);
}

@Repository
public class MybatisPracticeRecordRepository implements PracticeRecordRepository {
    private final PracticeRecordMapper mapper;
    /* 委托给 MyBatis 实现 */
}

业务层不再关心是 MySQL、Redis 还是内存。换存储、写单测都简单了。

步骤 4:DRY 与 YAGNI 的边界

// DRY:两处都需要的逻辑,抽出来
public boolean canAccessLesson(CfUser user, CfLesson lesson) {
    return lesson.isFree() || user.isVip();
}

// YAGNI:现在不需要"团队空间",就别提前加 teamId 字段、别预留 teamService
// 等真的有需求再加,否则你维护的是想象出来的复杂度

讲清楚要点

  1. SRP:一个类只有一个被修改的理由,按"角色"或"参与者"切分。
  2. OCP:通过抽象 + 多态新增行为,而不是修改老代码加 if-else。
  3. LSP:子类任何地方能替换父类而不出错。Square extends Rectangle 是经典反例。
  4. ISP:客户端不该被迫依赖它用不到的方法。JudgeEngine 不该继承 NotificationSender。
  5. DIP:高层业务不依赖低层细节,双方都依赖抽象。Spring DI 就是 DIP 的工业化实现。
  6. DRY:同一份业务知识只表达一次;重复代码 ≠ DRY 违反,重复"业务规则"才是。
  7. YAGNI:你当下没有的需求不要提前实现;预留接口、预留字段都是债。

你可能会懵的地方

  • OCP 会不会导致类爆炸? 会多一点类,但每个类很小、很聚焦,远比一个 2000 行的 if-else 上帝类好维护。Spring 的 List<Strategy> 注入让 OCP 几乎零成本。
  • 里氏替换在 Java 里具体怎么违? 子类抛了父类不会抛的异常、子类加强了前置条件("我只接受正数"而父类接受所有 int)、子类削弱了后置条件。判题平台里 VipCfUser extends CfUser 如果重写 canAccess 反而更严格,就违反 LSP。
  • 接口隔离和单一职责什么关系? SRP 看类,ISP 看接口;都是"别塞没用的东西"。一个 UserRepository 不该同时继承 EmailSender。
  • DRY 是不是不允许任何重复? 不是。两处相似的代码如果将来变化方向不同,就不该合并。"偶然重复"强行抽象,会埋耦合炸弹。
  • YAGNI 和开闭原则矛盾吗? 不矛盾。OCP 是给"已知会变"的维度留扩展点(比如判题语言);YAGNI 是"别为猜不到的维度预留"。区别在于"变化是否有明确信号"。

错法对照

错法问题正解
一个 Service 2000 行多角色修改,互相踩按角色拆 SRP
if (lang=="java")...else if(lang=="py")加语言要改老代码策略接口 + Spring 注入列表
业务层直接 @Autowired Mapper绑死 MySQL抽象 Repository 接口
extends ArrayList 复用继承了不该暴露的方法组合:内部持有一个 List
提前预留 tenantId 字段当前没多租户需求YAGNI:需要时再加

卡点急救

  1. 不知道该怎么拆:列出这个类所有的 public 方法,按"谁会让它改"分组,每组拆成一个类。
  2. 想抽接口但不知道抽哪些方法:问"调用方真正用到哪些?"只把这些放进接口,其他留在实现里。
  3. 怕被说过度设计:问"这个扩展点在未来 6 个月有明确需求吗?"没有就先 if-else,有就抽策略。

面试常问

  • 举一个你项目里 SOLID 的应用? 答:判题平台用策略模式做 JudgeEngine,每个语言一个实现,Spring 注入 List<JudgeEngine>,加语言不改老代码(OCP);业务层依赖 PracticeRecordRepository 接口而不是 MyBatis Mapper(DIP);通知服务独立成 NotificationService,不混在 SubmissionService(SRP)。
  • LSP 举个反例? 答:Square extends Rectangle,正方形 setWidth 会改 height,违反"父类出现的地方子类可替换"。判题平台里如果 FreeLesson extends CfLesson 重写 canAccess 抛异常"免费课无需判断",也是 LSP 违反。
  • DRY 和"复制粘贴"的边界? 答:DRY 重复的是"业务知识"。两个类恰好都写了 user.getVip() == 1 是重复业务知识,该抽 user.isVip()。两个类都写 log.info(...) 是巧合重复,不该合并。

自测三问

  1. 一个类同时被"判题工程师、运营、会员产品"修改,违反了哪条原则?怎么改?
  2. 为什么"接口里塞太多方法"也是一种设计病?它违反哪条原则?
  3. 在判题平台中,新增"Rust 判题"应该怎么动代码?

小结一句话

SOLID 让类之间的关系可扩展、可替换、可测试;DRY 让知识只写一次;YAGNI 让你别为想象中的需求付利息。

延伸

  • 《敏捷软件开发:原则、模式与实践》Robert C. Martin
  • 《设计模式:可复用面向对象软件的基础》GoF,第 1 章
  • Spring 官方文档 - IoC 容器
  • 下一课:面向对象——继承、组合、多态在判题平台的具体落地。
加载本节练习…
← 上一课整洁代码下一课 →面向对象