跳转至

439_SOLID原则详解


一句话说明

SOLID 是面向对象设计的五大原则,记住这五个字母,写出来的代码更容易修改、扩展和测试,是面试必考设计题的答题框架。


核心知识点

白话理解

SOLID 就像建房子的五条规矩: - S 一个工人只干一种活(单一职责) - O 加新功能不改旧代码(开闭) - L 子类能完全替换父类(里氏替换) - I 不强迫别人实现用不上的接口(接口隔离) - D 依赖抽象不依赖具体(依赖倒置)

五大原则

字母原则核心思想
SSingle Responsibility一个类只有一个改变的理由
OOpen/Closed对扩展开放,对修改关闭
LLiskov Substitution子类可以替换父类而不破坏程序
IInterface Segregation接口要小而精,不塞用不到的方法
DDependency Inversion高层依赖抽象,不依赖低层实现

经典题目与解法

题目1:违反 SRP 的代码识别与修复

题意:以下 User 类同时处理用户数据、邮件发送、数据存储,违反 SRP,请重构。

# ===== 违反SRP =====
class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

    def get_user_info(self):
        return f"{self.name}: {self.email}"   # 数据展示逻辑

    def send_email(self, message):
        print(f"发送邮件到 {self.email}: {message}")  # 邮件逻辑

    def save_to_db(self):
        print(f"保存 {self.name} 到数据库")   # 持久化逻辑


# ===== 遵守SRP:每个类只负责一件事 =====
class User:
    """只负责用户数据的承载"""
    def __init__(self, name: str, email: str):
        self.name = name
        self.email = email

    def __repr__(self):
        return f"User({self.name}, {self.email})"  # 数据展示

class EmailService:
    """只负责邮件发送"""
    def send(self, user: User, message: str):
        print(f"发送邮件到 {user.email}: {message}")

class UserRepository:
    """只负责用户数据持久化"""
    def save(self, user: User):
        print(f"保存 {user.name} 到数据库")

    def find_by_name(self, name: str):
        print(f"从数据库查询 {name}")

# 使用:各司其职
user = User("张三", "zhangsan@example.com")
email_service = EmailService()
user_repo = UserRepository()

email_service.send(user, "欢迎注册!")  # 发邮件
user_repo.save(user)                   # 存库

题目2:用依赖倒置原则(DIP)解耦数据库

题意:业务逻辑直接依赖 MySQL 实现,切换数据库很麻烦,用 DIP 改造。

from abc import ABC, abstractmethod

# ===== 抽象层(高层和低层都依赖它)=====
class UserRepositoryInterface(ABC):
    """用户仓储抽象接口——这是"约定",不是实现"""

    @abstractmethod
    def save(self, user_id: int, name: str) -> bool:
        pass

    @abstractmethod
    def find(self, user_id: int) -> dict:
        pass


# ===== 低层:具体实现(可替换)=====
class MySQLUserRepository(UserRepositoryInterface):
    """MySQL具体实现"""
    def save(self, user_id: int, name: str) -> bool:
        print(f"[MySQL] 保存用户 {user_id}: {name}")
        return True

    def find(self, user_id: int) -> dict:
        print(f"[MySQL] 查询用户 {user_id}")
        return {'id': user_id, 'name': '张三'}


class RedisUserRepository(UserRepositoryInterface):
    """Redis缓存实现(切换数据库只需换这个类)"""
    def save(self, user_id: int, name: str) -> bool:
        print(f"[Redis] 缓存用户 {user_id}: {name}")
        return True

    def find(self, user_id: int) -> dict:
        print(f"[Redis] 从缓存读取用户 {user_id}")
        return {'id': user_id, 'name': '李四(缓存)'}


# ===== 高层:业务逻辑(依赖抽象,不管底层是什么)=====
class UserService:
    def __init__(self, repo: UserRepositoryInterface):
        """依赖注入:接收抽象接口,不关心具体实现"""
        self.repo = repo

    def register_user(self, user_id: int, name: str):
        """注册用户:不知道也不关心底层用什么数据库"""
        if self.repo.save(user_id, name):
            print(f"用户 {name} 注册成功")

    def get_user(self, user_id: int):
        return self.repo.find(user_id)


# 测试:切换实现无需改业务代码
mysql_repo = MySQLUserRepository()
redis_repo = RedisUserRepository()

service1 = UserService(mysql_repo)   # 使用MySQL
service1.register_user(1, "张三")

service2 = UserService(redis_repo)   # 切换Redis,业务代码不变
service2.register_user(2, "李四")

时间复杂度:O(1) 接口查找
设计收益:业务代码与存储实现完全解耦,单元测试可以注入 Mock


面试技巧

  1. 举例胜过背定义:说"SRP就是一个类只有一个改变的理由"不够,要举出具体例子
  2. OCP 和策略模式绑定:开闭原则通常用策略模式/注册表实现
  3. LSP 常见违反:子类重写父类方法后抛出父类不抛的异常,就违反了 LSP
  4. DIP = 依赖注入的理论基础:面试问依赖注入时,先聊 DIP 原则
  5. 不要生搬硬套:SOLID 是指导原则,不是铁律,过度设计比不设计更糟

速查表

原则违反迹象解决方案
SRP类改动原因有多个按职责拆分类
OCP加功能要改已有代码策略模式/注册表
LSP子类方法比父类限制多重新设计继承关系
ISP实现接口只用到部分方法拆分细粒度接口
DIP高层模块 import 低层模块依赖注入+抽象接口
# DIP 依赖注入模板
class HighLevelService:
    def __init__(self, dependency: AbstractInterface):  # 依赖抽象
        self.dep = dependency

# 注册时注入具体实现
service = HighLevelService(ConcreteImplementation())