在SOLID五大原则中,单一职责原则(SRP) 是最基础却也最容易被误解的一个。很多开发者认为SRP就是"一个类只有一个方法",实则不然。真正的SRP是:一个类应该只有一个引起它变化的原因。
今天,我们将深入探讨如何通过接口定义实现职责分离,这是SRP实践中最优雅且类型安全的方式,尤其在TypeScript严格模式下。
一、为什么接口是职责分离的最佳工具?
接口的本质是契约定义。当我们将大职责拆分成多个小接口时,实际上是在定义多个独立的契约。实现类可以选择性地签署这些契约,从而避免承担不必要的职责。
核心优势:
-
编译期校验:TypeScript严格模式会在编译期强制检查接口实现
-
依赖精准:依赖方只需知道自己需要的接口,无需关心实现细节
-
测试友好:可以轻松mock单个接口行为
-
扩展灵活:新增接口不影响现有实现
二、案例一:文件处理系统(入门)
❌ 违反SRP:一个类处理所有文件操作
TypeScript
class FileHelper {
// 职责1:读取文件
readFile(path: string): string {
if (!this.exists(path)) throw new Error('File not found');
return 'file content';
}
// 职责2:写入文件
writeFile(path: string, content: string): void {
// 写入逻辑
}
// 职责3:压缩文件
compress(path: string): void {
// 压缩逻辑
}
// 职责4:上传文件
upload(path: string, url: string): Promise<void> {
return fetch(url, { method: 'POST', body: path }).then();
}
// 职责5:删除文件
delete(path: string): void {
// 删除逻辑
}
// 职责6:检查文件是否存在
private exists(path: string): boolean {
return true;
}
}
问题分析:
-
修改压缩算法 → 影响上传功能
-
更换存储方式(如从本地到云端) → 必须修改整个类
-
单元测试需要mock所有方法,哪怕只测试读取功能
✅ 遵守SRP:通过接口分离
TypeScript
// ============= 接口定义:每个接口一个职责 =============
// 职责契约1:可读
interface IReadable {
read(path: string): string;
}
// 职责契约2:可写
interface IWritable {
write(path: string, content: string): void;
}
// 职责契约3:可压缩
interface ICompressible {
compress(path: string): void;
}
// 职责契约4:可上传
interface IUploadable {
upload(path: string, url: string): Promise<void>;
}
// 职责契约5:可删除
interface IDeletable {
delete(path: string): void;
}
// 职责契约6:可检查存在性
interface IExistenceCheckable {
exists(path: string): boolean;
}
// ============= 实现类:按需签署契约 =============
// 实现类1:本地文件系统(只读、写、删除、检查)
class LocalFileHandler implements IReadable, IWritable, IDeletable, IExistenceCheckable {
read(path: string): string {
if (!this.exists(path)) throw new Error('File not found');
return 'local file content';
}
write(path: string, content: string): void {
console.log(`Writing to local: ${path}`);
}
delete(path: string): void {
console.log(`Deleting local: ${path}`);
}
exists(path: string): boolean {
return true;
}
}
// 实现类2:云存储文件(可读、可写、可上传、可删除)
class CloudFileHandler implements IReadable, IWritable, IUploadable, IDeletable {
constructor(
private readonly bucket: string, // 严格模式:readonly防止修改
private readonly apiKey: string
) {
if (!bucket || !apiKey) throw new Error('Cloud credentials required');
}
read(path: string): string {
return 'cloud file content';
}
write(path: string, content: string): void {
console.log(`Writing to cloud bucket ${this.bucket}: ${path}`);
}
async upload(path: string, url: string): Promise<void> {
// 严格模式:必须处理Promise拒绝
try {
await fetch(url, { method: 'POST', body: path });
} catch (error) {
throw new Error(`Upload failed: ${(error as Error).message}`);
}
}
delete(path: string): void {
console.log(`Deleting cloud: ${path}`);
}
}
// 实现类3:只读文件处理器(仅实现读取)
class ReadOnlyFileHandler implements IReadable {
read(path: string): string {
return 'read-only content';
}
}
// ============= 使用方:只依赖需要的接口 =============
class DataProcessor {
// 只依赖IReadable,不关心实现
constructor(private readonly fileReader: IReadable) {}
process(path: string): string {
const content = this.fileReader.read(path);
return content.toUpperCase();
}
}
// ============= 测试:轻松mock =============
test('DataProcessor should process file content', () => {
// 只mock IReadable接口
const mockReader: IReadable = {
read: jest.fn().mockReturnValue('test content')
};
const processor = new DataProcessor(mockReader);
const result = processor.process('test.txt');
expect(result).toBe('TEST CONTENT');
expect(mockReader.read).toHaveBeenCalledWith('test.txt');
});
核心优势:
-
DataProcessor只依赖IReadable,完全不知道LocalFileHandler或CloudFileHandler的存在 -
测试时只需mock单个接口方法
-
新增文件处理方式只需实现对应接口,无需修改现有代码
三、案例二:订单处理系统(进阶)
❌ 违反SRP:订单类承担所有职责
TypeScript
class Order {
constructor(
public items: OrderItem[],
public customer: Customer,
public status: 'pending' | 'paid' | 'shipped'
) {}
// 职责1:计算总价
calculateTotal(): number {
return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
// 职责2:应用折扣
applyDiscount(discountCode: string): void {
const discount = this.validateDiscount(discountCode);
this.items.forEach(item => {
item.price *= (1 - discount.percentage / 100);
});
}
// 职责3:验证折扣码
private validateDiscount(code: string): Discount {
// 查询数据库验证
return { code, percentage: 10, valid: true };
}
// 职责4:保存订单
async save(): Promise<void> {
await db.insert('orders', this);
}
// 职责5:发送确认邮件
async sendConfirmation(): Promise<void> {
const email = this.buildEmail();
await mailer.send(this.customer.email, email);
}
// 职责6:构建邮件内容
private buildEmail(): string {
return `Order ${this.customer.id} confirmed`;
}
// 职责7:检查库存
async checkInventory(): Promise<boolean> {
for (const item of this.items) {
const stock = await inventory.get(item.productId);
if (stock < item.quantity) return false;
}
return true;
}
}
问题分析:
-
修改邮件模板 → 影响库存检查逻辑
-
更换数据库 → 所有功能都需要测试
-
无法单独重用"计算总价"逻辑
✅ 遵守SRP:接口分离 + 策略模式
TypeScript
// ============= 接口定义:每个职责一个契约 =============
// 职责1:可计算价格
interface IPriceCalculable {
calculateTotal(): number;
applyDiscount(discount: IDiscountStrategy): void;
}
// 职责2:可验证折扣
interface IDiscountValidatable {
validateDiscount(code: string): DiscountInfo;
}
// 职责3:可持久化
interface IPersistable<T> {
save(entity: T): Promise<void>;
findById(id: number): Promise<T | null>;
}
// 职责4:可通知
interface INotifiable {
sendConfirmation(): Promise<void>;
}
// 职责5:可检查库存
interface IInventoryCheckable {
checkInventory(): Promise<boolean>;
}
// 职责6:折扣策略(独立的职责单元)
interface IDiscountStrategy {
apply(items: OrderItem[]): number; // 返回折扣后的总价
}
// ============= 实现类:职责独立 =============
// 实现1:简单折扣策略
class PercentageDiscountStrategy implements IDiscountStrategy {
constructor(private readonly percentage: number) {
if (percentage < 0 || percentage > 100) {
throw new Error('Discount percentage must be between 0 and 100');
}
}
apply(items: OrderItem[]): number {
const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
return total * (1 - this.percentage / 100);
}
}
// 实现2:会员折扣策略
class MemberDiscountStrategy implements IDiscountStrategy {
constructor(private readonly memberLevel: 'gold' | 'silver' | 'bronze') {}
apply(items: OrderItem[]): number {
const discounts = { gold: 0.2, silver: 0.1, bronze: 0.05 };
const rate = discounts[this.memberLevel];
const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
return total * (1 - rate);
}
}
// 实现3:订单实体(只保留数据)
class Order {
constructor(
public readonly items: readonly OrderItem[], // 严格模式:只读保护数据
public readonly customer: Customer,
private _status: 'pending' | 'paid' | 'shipped'
) {}
get status() { return this._status; } // 严格模式:私有状态通过getter暴露
set status(value: 'pending' | 'paid' | 'shipped') {
this._status = value;
}
}
// 实现4:订单价格计算器
class OrderPriceCalculator implements IPriceCalculable {
constructor(private readonly order: Order) {} // 组合优于继承
calculateTotal(): number {
return this.order.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
applyDiscount(strategy: IDiscountStrategy): number {
if (this.order.status !== 'pending') {
throw new Error('Can only apply discount to pending orders');
}
return strategy.apply([...this.order.items]); // 严格模式:复制数组避免副作用
}
}
// 实现5:订单仓库(持久化)
class OrderRepository implements IPersistable<Order> {
constructor(private readonly dbConnection: Database) {} // 依赖注入
async save(order: Order): Promise<void> {
// 严格模式:必须处理可能的null/undefined
if (!order) throw new Error('Order cannot be null');
await this.dbConnection.insert('orders', order);
}
async findById(id: number): Promise<Order | null> {
const data = await this.dbConnection.query('SELECT * FROM orders WHERE id = ?', [id]);
return data.length > 0 ? new Order(data[0].items, data[0].customer, data[0].status) : null;
}
}
// 实现6:订单通知服务
class OrderNotificationService implements INotifiable {
constructor(
private readonly emailService: IEmailService,
private readonly templateEngine: ITemplateEngine
) {}
async sendConfirmation(order: Order): Promise<void> {
const email = this.buildEmail(order);
await this.emailService.send(order.customer.email, email);
}
private buildEmail(order: Order): string {
const template = this.templateEngine.render('order-confirmation', {
orderId: order.customer.id,
total: new OrderPriceCalculator(order).calculateTotal()
});
return template;
}
}
// 实现7:库存服务
class InventoryService implements IInventoryCheckable {
constructor(private readonly inventoryApi: IInventoryApi) {}
async checkInventory(order: Order): Promise<boolean> {
// 严格模式:Promise.all需要类型断言
const results = await Promise.all(
order.items.map(item => this.checkItem(item))
);
return results.every(result => result === true); // 严格模式:必须明确比较
}
private async checkItem(item: OrderItem): Promise<boolean> {
const stock = await this.inventoryApi.getStock(item.productId);
return stock >= item.quantity;
}
}
// ============= 使用方:组合职责 =============
class OrderProcessor {
constructor(
private readonly order: Order,
private readonly calculator: OrderPriceCalculator,
private readonly repository: OrderRepository,
private readonly notifier: OrderNotificationService,
private readonly inventoryService: InventoryService
) {}
async process(discountCode?: string): Promise<void> {
// 1. 检查库存
const inStock = await this.inventoryService.checkInventory(this.order);
if (!inStock) throw new Error('Insufficient inventory');
// 2. 应用折扣
if (discountCode) {
// 可以动态替换折扣策略
const strategy = new PercentageDiscountStrategy(10);
this.calculator.applyDiscount(strategy);
}
// 3. 保存订单
await this.repository.save(this.order);
// 4. 发送通知
await this.notifier.sendConfirmation(this.order);
}
}
// ============= 测试:独立测试每个职责 =============
test('OrderPriceCalculator applies discount correctly', () => {
const order = new Order(
[{ productId: 1, price: 100, quantity: 1 }],
{ id: 1, email: 'test@example.com' },
'pending'
);
const calculator = new OrderPriceCalculator(order);
const strategy = new PercentageDiscountStrategy(10);
const discounted = calculator.applyDiscount(strategy);
expect(discounted).toBe(90);
});
test('InventoryService checks inventory correctly', async () => {
const mockApi: IInventoryApi = {
getStock: jest.fn().mockResolvedValue(5)
};
const service = new InventoryService(mockApi);
const order = new Order(
[{ productId: 1, price: 100, quantity: 3 }],
{} as Customer,
'pending'
);
const result = await service.checkInventory(order);
expect(result).toBe(true);
});
核心优势:
-
每个类都可以独立演化:修改邮件模板不影响库存逻辑
-
高度可测试:可以单独测试
OrderPriceCalculator、InventoryService等 -
灵活组合:可以创建只读订单查看器、只处理折扣的处理器等
四、案例三:用户管理系统(高级)
❌ 违反SRP:上帝类
TypeScript
class UserManager {
// 数据库操作
async createUser(user: User): Promise<void> {
// 验证
if (!this.validateEmail(user.email)) throw new Error('Invalid email');
// 加密密码
user.password = await bcrypt.hash(user.password, 10);
// 保存
await this.db.query('INSERT INTO users ...', user);
// 发送欢迎邮件
await this.sendEmail(user.email, 'Welcome!');
// 记录日志
this.log(`User ${user.email} created`);
// 触发分析事件
analytics.track('user_created', user);
}
// 验证
validateEmail(email: string): boolean {
return email.includes('@');
}
// 认证
async login(email: string, password: string): Promise<string> {
const user = await this.db.query('SELECT * FROM users WHERE email = ?', [email]);
if (!user) throw new Error('User not found');
const valid = await bcrypt.compare(password, user.password);
if (!valid) throw new Error('Invalid password');
return jwt.sign({ userId: user.id }, 'secret');
}
// 发送邮件
async sendEmail(to: string, content: string): Promise<void> {
// 邮件逻辑
}
// 记录日志
log(message: string): void {
console.log(message);
}
// 权限检查
hasPermission(userId: number, permission: string): boolean {
const user = this.db.query('SELECT * FROM users WHERE id = ?', [userId]);
return user.role.includes(permission);
}
// 生成报表
async generateUserReport(): Promise<Report> {
const users = await this.db.query('SELECT * FROM users');
return { total: users.length, data: users };
}
}
✅ 遵守SRP:接口拆分 + 服务组合
TypeScript
// ============= 接口定义:垂直切片职责 =============
// 数据访问职责
interface IUserRepository {
create(user: User): Promise<User>;
findByEmail(email: string): Promise<User | null>;
findById(id: number): Promise<User | null>;
}
// 验证职责
interface IUserValidator {
validate(user: User): ValidationResult;
validateEmail(email: string): boolean;
validatePassword(password: string): boolean;
}
// 认证职责
interface IAuthenticationService {
login(email: string, password: string): Promise<string>;
logout(token: string): Promise<void>;
verifyToken(token: string): Promise<number>; // 返回userId
}
// 密码安全职责
interface IPasswordService {
hash(password: string): Promise<string>;
compare(password: string, hash: string): Promise<boolean>;
}
// 通知职责
interface INotificationService {
sendWelcomeEmail(user: User): Promise<void>;
sendPasswordResetEmail(email: string): Promise<void>;
}
// 权限职责
interface IPermissionService {
hasPermission(userId: number, permission: string): Promise<boolean>;
assignRole(userId: number, role: string): Promise<void>;
}
// 报表职责
interface IUserReportService {
generateReport(): Promise<Report>;
exportReport(format: 'pdf' | 'csv'): Promise<Buffer>;
}
// 审计日志职责
interface IAuditLogger {
logUserAction(userId: number, action: string, details: Record<string, any>): void;
}
// 分析跟踪职责
interface IAnalyticsService {
track(event: string, properties: Record<string, any>): void;
identify(userId: number, traits: Record<string, any>): void;
}
// ============= 实现类:每个类只实现一个职责 =============
// 实现1:用户仓库(仅数据访问)
class UserRepository implements IUserRepository {
constructor(private readonly db: Database) {} // 依赖注入
async create(user: User): Promise<User> {
// 严格模式:必须处理可能的错误
try {
const result = await this.db.query('INSERT INTO users ...', user);
return { ...user, id: result.insertId };
} catch (error) {
throw new Error(`Failed to create user: ${(error as Error).message}`);
}
}
async findByEmail(email: string): Promise<User | null> {
const results = await this.db.query('SELECT * FROM users WHERE email = ?', [email]);
return results[0] || null; // 严格模式:显式返回null
}
async findById(id: number): Promise<User | null> {
const results = await this.db.query('SELECT * FROM users WHERE id = ?', [id]);
return results[0] || null;
}
}
// 实现2:用户验证器(仅验证)
class UserValidator implements IUserValidator {
private readonly emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // 严格模式:readonly
validate(user: User): ValidationResult {
const errors: string[] = [];
if (!this.validateEmail(user.email)) {
errors.push('Invalid email format');
}
if (!this.validatePassword(user.password)) {
errors.push('Password must be at least 8 characters');
}
return {
isValid: errors.length === 0,
errors
};
}
validateEmail(email: string): boolean {
return this.emailRegex.test(email);
}
validatePassword(password: string): boolean {
return password.length >= 8;
}
}
// 实现3:密码服务(仅密码处理)
class BcryptPasswordService implements IPasswordService {
private readonly saltRounds = 10; // 严格模式:配置不可变
async hash(password: string): Promise<string> {
// 严格模式:空值检查
if (!password) throw new Error('Password cannot be empty');
return bcrypt.hash(password, this.saltRounds);
}
async compare(password: string, hash: string): Promise<boolean> {
if (!password || !hash) return false;
return bcrypt.compare(password, hash);
}
}
// 实现4:认证服务(仅认证)
class JwtAuthenticationService implements IAuthenticationService {
constructor(
private readonly userRepo: IUserRepository,
private readonly passwordService: IPasswordService,
private readonly config: { secret: string; expiresIn: string }
) {
// 严格模式:配置校验
if (!config.secret) throw new Error('JWT secret required');
}
async login(email: string, password: string): Promise<string> {
// 严格模式:必须处理所有null情况
const user = await this.userRepo.findByEmail(email);
if (!user) throw new Error('Invalid credentials');
const isValid = await this.passwordService.compare(password, user.password);
if (!isValid) throw new Error('Invalid credentials');
// 严格模式:user.id可能为undefined,需要断言
return jwt.sign({ userId: user.id! }, this.config.secret, {
expiresIn: this.config.expiresIn
});
}
async logout(token: string): Promise<void> {
// 添加到黑名单或清除会话
await this.invalidateToken(token);
}
async verifyToken(token: string): Promise<number> {
try {
const decoded = jwt.verify(token, this.config.secret) as { userId: number };
return decoded.userId;
} catch {
throw new Error('Invalid token');
}
}
private async invalidateToken(token: string): Promise<void> {
// 实际注销逻辑
}
}
// 实现5:通知服务(仅通知)
class EmailNotificationService implements INotificationService {
constructor(
private readonly emailService: IEmailService,
private readonly templateEngine: ITemplateEngine
) {}
async sendWelcomeEmail(user: User): Promise<void> {
const content = this.templateEngine.render('welcome', { name: user.name });
await this.emailService.send(user.email, 'Welcome!', content);
}
async sendPasswordResetEmail(email: string): Promise<void> {
const token = this.generateResetToken(email);
const content = this.templateEngine.render('reset-password', { token });
await this.emailService.send(email, 'Reset Your Password', content);
}
private generateResetToken(email: string): string {
return crypto.randomBytes(32).toString('hex');
}
}
// 实现6:用户注册用例(组合所有职责)
class RegisterUserUseCase {
constructor(
private readonly validator: IUserValidator,
private readonly repository: IUserRepository,
private readonly passwordService: IPasswordService,
private readonly notificationService: INotificationService,
private readonly auditLogger: IAuditLogger,
private readonly analytics: IAnalyticsService
) {}
async execute(userData: UserRegistrationDTO): Promise<Result<User>> {
// 1. 验证
const user = new User(userData);
const validation = this.validator.validate(user);
if (!validation.isValid) {
return { success: false, errors: validation.errors };
}
// 2. 密码哈希
user.password = await this.passwordService.hash(user.password);
// 3. 保存用户
const createdUser = await this.repository.create(user);
// 4. 并行执行:发送邮件 + 日志 + 分析
await Promise.all([
this.notificationService.sendWelcomeEmail(createdUser),
(async () => {
this.auditLogger.logUserAction(createdUser.id!, 'register', { email: createdUser.email });
this.analytics.identify(createdUser.id!, { email: createdUser.email });
})()
]);
return { success: true, data: createdUser };
}
}
// ============= 组合与注入 =============
// 应用启动
const createUserModule = () => {
// 基础设施层
const db = new PostgresqlDatabase();
const emailService = new SendGridEmailService();
const templateEngine = new HandlebarsTemplateEngine();
// 数据层
const userRepo = new UserRepository(db);
// 领域服务
const validator = new UserValidator();
const passwordService = new BcryptPasswordService();
const authService = new JwtAuthenticationService(userRepo, passwordService, config.jwt);
const notifier = new EmailNotificationService(emailService, templateEngine);
const auditLogger = new WinstonAuditLogger();
const analytics = new SegmentAnalyticsService();
// 用例层
const registerUseCase = new RegisterUserUseCase(
validator, userRepo, passwordService, notifier, auditLogger, analytics
);
return { registerUseCase, authService };
};
// API层
app.post('/api/register', async (req, res) => {
const { registerUseCase } = createUserModule();
const result = await registerUseCase.execute(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.errors });
}
res.status(201).json(result.data);
});
严格模式下的类型安全亮点:
-
createdUser.id!断言配合构造函数验证,确保ID存在 -
Result<User>泛型结果模式,强制处理成功/失败两种情况 -
Readonly和readonly防止数据在流程中被意外修改 -
每个服务的依赖通过构造函数注入,类型系统确保完整性
五、总结:接口分离的黄金法则
1. 识别职责边界的技巧
在TypeScript严格模式下,可以通过类型复杂度来识别职责:
-
如果一个接口有超过5个方法:可能职责过大
-
如果实现类需要处理大量不相关的状态:需要拆分
-
如果测试时需要mock太多无关方法:接口需要细化
2. 接口粒度建议
| 接口大小 | 适用场景 | TypeScript严格模式提示 |
|---|---|---|
| 细粒度(1-2方法) | 基础设施服务(邮件、日志) | 易于mock,测试简洁 |
| 中粒度(3-5方法) | 领域服务(订单处理) | 职责聚焦,类型安全 |
| 粗粒度(5+方法) | 仅用于聚合根(谨慎使用) | 需要拆分成小接口 |
3. 依赖注入模式
在严格模式下,推荐构造函数注入 + readonly:
TypeScript
复制
TypeScript
class Service {
constructor(
private readonly repo: IUserRepository, // 不可变,类型安全
private readonly logger: ILogger
) {}
}
4. 测试愉悦度对比
TypeScript
// ❌ 违反SRP:需要mock整个UserManager
const mockManager = {
createUser: jest.fn(),
validateEmail: jest.fn(),
hashPassword: jest.fn(),
sendEmail: jest.fn(),
log: jest.fn(),
// ...还要mock10个不相关的方法
};
// ✅ 遵守SRP:只mock需要的接口
const mockValidator: IUserValidator = {
validate: jest.fn().mockReturnValue({ isValid: true, errors: [] }),
validateEmail: jest.fn().mockReturnValue(true),
validatePassword: jest.fn().mockReturnValue(true)
};
5. 严格模式下的最佳实践
必须启用:
TypeScript
{
"compilerOptions": {
"strict": true,
"strictNullChecks": true,
"strictPropertyInitialization": true,
"noImplicitAny": true,
"noUnusedLocals": true
}
}
代码规范:
-
所有接口属性使用
readonly -
所有服务类属性使用
private readonly -
数组参数使用
ReadonlyArray<T> -
返回
null必须显式声明类型 -
使用
unknown而非any表示不确定类型
六、核心要点回顾
通过接口实现职责分离的三步法:
-
识别职责:找到"改变的原因"(数据库变化、业务规则变化、UI变化等)
-
定义接口:为每个职责创建独立接口(Ixxxable命名约定)
-
组合实现:实现类按需签署接口,用例层组合所需服务
最终效果:
-
可维护:修改邮件服务不影响订单计算
-
可测试:每个接口可独立mock
-
灵活:可替换任何实现而不影响高层
-
类型安全:TypeScript严格模式全程保驾护航
记住:接口是职责的边界,不是实现的约束。拥抱接口分离,让你的代码像乐高积木一样自由组合!