
一个最纯粹的技术分享网站,打造精品技术编程专栏!编程进阶网
设计模式Git项目地址:https://github.com/yangchong211/YCDesignBlog
单一职责原则(SRP)是面向对象设计的重要原则,强调一个类或模块应仅负责完成一个特定的职责或功能。通过将复杂的功能分解为多个粒度小、功能单一的类,可以提高系统的灵活性、可维护性和可扩展性。
本文详细介绍了如何理解单一职责原则,包括方法、接口和类层面的应用,并通过具体例子解释了其优势和判断标准。此外,还探讨了在实际开发中如何平衡类的设计,避免过度拆分导致的复杂性增加。
单一责任原则(Single Responsibility Principle,SRP)是面向对象设计中的一条重要原则。
这个原则的英文描述是这样的:A class or module should have a single responsibility。
如果我们把它翻译成中文,那就是:一个类或者模块只负责完成一个职责(或者功能)。
也就是说,一个模块或类应该只负责一个特定的职责或功能,通过将功能分解到不同的模块或类中,可以使系统更加灵活、可维护和可扩展。
从字面上理解,不难。你一看就感觉懂了,一看就感觉掌握了,但真的用到项目中的时候,你会发现,“看懂”和“会用”是两回事,而“用好”更是难上加难。
从工作经历来看,很多同事因为对这些原则理解得不够透彻,导致在使用的时候过于教条主义,拿原则当真理,生搬硬套,适得其反。
单一原则描述的对象包含两个,一个是类(class),一个是模块(module)。
关于这两个概念,在专栏中,有两种理解方式。一种理解是:把模块看作比类更加抽象的概念,类也可以看作模块。另一种理解是:把模块看作比类更加粗粒度的代码块,模块中包含多个类,多个类组成一个模块。
不管哪种理解方式,单一职责原则在应用到这两个描述对象的时候,道理都是相通的。为了方便你理解,接下来我只从“类”设计的角度,来讲解如何应用这个设计原则。对于“模块”来说,你可以自行引申。
单一职责原则的定义描述非常简单,也不难理解。一个类只负责完成一个职责或者功能。也就是说,不要设计大而全的类,要设计粒度小、功能单一的类。换个角度来讲就是,一个类包含了两个或者两个以上业务不相干的功能,那我们就说它职责不够单一,应该将它拆分成多个功能更加单一、粒度更细的类。
举一个例子来解释一下。比如,一个类里既包含订单的一些操作,又包含用户的一些操作。而订单和用户是两个独立的业务领域模型,我们将两个不相干的功能放到同一个类中,那就违反了单一职责原则。
为了满足单一职责原则,我们需要将这个类拆分成两个粒度更细、功能更加单一的两个类:订单类和用户类。
通常 ,我们做事情都要知道为什么要这么做,才回去做。做的也有底气,那么为什么我们要使用单一职责原则呢?
现在有一个场景,需要修改用户的用户名和密码,就针对这个功能我们可以有多种实现。
先看一下第一种实现方式:
public enum OperateEnum {
UPDATE_USERNAME,
UPDATE_PASSWORD;
}
public interface UserOperate {
void updateUserInfo(OperateEnum type,UserInfo userInfo);
}
public class UserOperateImpl implements UserOperate{
@Override
public void updateUserInfo(OperateEnum type,UserInfo userInfo) {
if (type == OperateEnum。UPDATE_PASSWORD) {
// 修改密码
} else if(type == OperateEnum。UPDATE_USERNAME) {
// 修改用户名
}
}
}然后看一下第二种实现方式:
public interface UserOperate {
void updateUserName(UserInfo userInfo);
void updateUserPassword(UserInfo userInfo);
}
public class UserOperateImpl implements UserOperate {
@Override
public void updateUserName(UserInfo userInfo) {
// 修改用户名逻辑
}
@Override
public void updateUserPassword(UserInfo userInfo) {
// 修改密码逻辑
}
}来看看这两种实现的区别:
由此可见,第二种设计是符合单一职责原则的。这是在方法层面实现单一职责原则。
我们假设一个场景,大家一起做家务,张三扫地,李四买菜。李四买完菜回来还得做饭。这个逻辑怎么实现呢?
先看一下第一种实现方式:
/**
* 做家务
*/
public interface HouseWork {
// 扫地
void sweepFloor();
// 购物
void shopping();
}
public class Zhangsan implements HouseWork{
@Override
public void sweepFloor() {
// 扫地
}
@Override
public void shopping() {
}
}
public class Lisi implements HouseWork{
@Override
public void sweepFloor() {
}
@Override
public void shopping() {
// 购物
}
}首先定义了一个做家务的接口,定义两个方法扫地和买菜。张三扫地,就实现扫地接口。李四买菜,就实现买菜接口。然后李四买完菜回来还要做饭,于是就要在接口类中增加一个方法cooking。张三和李四都重写这个方法,但只有李四有具体实现。
这样设计本身就是不合理的。首先: 张三只扫地,但是他需要重写买菜方法,李四不需要扫地,但是李四也要重写扫地方法。第二: 这也不符合开闭原则。增加一种类型做饭,要修改3个类。这样当逻辑很复杂的时候,很容易引起意外错误。
上面这种设计不符合单一职责原则,修改一个地方,影响了其他不需要修改的地方。
然后看一下第二种实现方式:
/**
* 做家务
*/
public interface Hoursework {
}
public interface Shopping extends Hoursework{
// 购物
void shopping();
}
public interface SweepFloor extends Hoursework{
// 扫地
void sweepFlooring();
}
public class Zhangsan implements SweepFloor{
@Override
public void sweepFlooring() {
// 张三扫地
}
}
public class Lisi implements Shopping{
@Override
public void shopping() {
// 李四购物
}
}上面做家务不是定义成一个接口,而是将扫地和做家务分开了。张三扫地,那么张三就实现扫地的接口。 李四购物,李四就实现购物的接口。 后面李四要增加一个功能做饭。 那么就新增一个做饭接口,这次只需要李四实现做饭接口就可以了。
public interface Cooking extends Hoursework{
void cooking();
}
public class Lisi implements Shopping, Cooking{
@Override
public void shopping() {
// 李四购物
}
@Override
public void cooking() {
// 李四做饭
}
}如上, 我们看到张三没有实现多余的接口, 李四也没有. 而且当新增功能的时候, 只影响了李四, 并没有影响张三.
这就是符合单一职责原则. 一个类只做一件事. 并且他的修改不会带来其他的变化.
从类的层面来讲, 没有办法完全按照单一职责原来来拆分. 换种说法, 类的职责可大可小, 不想接口那样可以很明确的按照单一职责原则拆分. 只要符合逻辑有道理即可.
比如, 我们在网站首页可以注册, 登录, 微信登录.注册登录等操作. 我们通常的做法是:
public interface UserOperate {
void login(UserInfo userInfo);
void register(UserInfo userInfo);
void logout(UserInfo userInfo);
}
public class UserOperateImpl implements UserOperate{
@Override
public void login(UserInfo userInfo) {
// 用户登录
}
@Override
public void register(UserInfo userInfo) {
// 用户注册
}
@Override
public void logout(UserInfo userInfo) {
// 用户登出
}
}那如果按照单一职责原则拆分, 也可以拆分为下面的形式
public interface Register {
void register();
}
public interface Login {
void login();
}
public interface Logout {
void logout();
}
public class RegisterImpl implements Register{
@Override
public void register() {
}
}
public class LoginImpl implements Login{
@Override
public void login() {
// 用户登录
}
}
public class LogoutImpl implements Logout{
@Override
public void logout() {
}
}在真实的软件开发中,对于一个类是否职责单一的判定,是很难拿捏的。我举一个更加贴近实际的例子来给你解释一下。
在一个社交产品中,我们用下面的 UserInfo 类来记录用户的信息。你觉得,UserInfo 类的设计是否满足单一职责原则呢?
public class UserInfo {
private long userId;
private String username;
private String email;
private String telephone;
private long createTime;
private long lastLoginTime;
private String avatarUrl;
private String provinceOfAddress; // 省
private String cityOfAddress; // 市
private String regionOfAddress; // 区
private String detailedAddress; // 详细地址
// 。。。省略其他属性和方法。。。
}对于这个问题,有两种不同的观点。
哪种观点更对呢?实际上,要从中做出选择,我们不能脱离具体的应用场景。
从刚刚这个例子,我们可以总结出,不同的应用场景、不同阶段的需求背景下,对同一个类的职责是否单一的判定,可能都是不一样的。在某种应用场景或者当下的需求背景下,一个类的设计可能已经满足单一职责原则了,但如果换个应用场景或着在未来的某个需求背景下,可能就不满足了,需要继续拆分成粒度更细的类。
有时候一个类的职责是否足够单一,我们并没有一个非常明确的、可以量化的标准,可以说,这是件非常主观、仁者见仁智者见智的事情。
可能会说,这个原则如此含糊不清、模棱两可,到底该如何拿捏才好啊?我这里还有一些小技巧,能够很好地帮你,从侧面上判定一个类的职责是否够单一。下面这几条判断原则,比起很主观地去思考类是否职责单一,要更有指导意义、更具有可执行性:
如何理解单一职责原则(SRP)?
一个类只负责完成一个职责或者功能。不要设计大而全的类,要设计粒度小、功能单一的类。单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。
如何判断类的职责是否足够单一?
类的职责是否设计得越单一越好?
单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、低耦合。
但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。
模块 | 描述 | 备注 |
|---|---|---|
GitHub | 多个YC系列开源项目,包含Android组件库,以及多个案例 | |
博客汇总 | 汇聚Java,Android,C/C++,网络协议,算法,编程总结等 | |
设计模式 | 六大设计原则,23种设计模式,设计模式案例,面向对象思想 | |
Java进阶 | 数据设计和原理,面向对象核心思想,IO,异常,线程和并发,JVM | |
网络协议 | 网络实际案例,网络原理和分层,Https,网络请求,故障排查 | |
计算机原理 | 计算机组成结构,框架,存储器,CPU设计,内存设计,指令编程原理,异常处理机制,IO操作和原理 | |
学习C编程 | C语言入门级别系统全面的学习教程,学习三到四个综合案例 | |
C++编程 | C++语言入门级别系统全面的教学教程,并发编程,核心原理 | |
算法实践 | 专栏,数组,链表,栈,队列,树,哈希,递归,查找,排序等 | |
Android | 基础入门,开源库解读,性能优化,Framework,方案设计 |
23种设计模式
23种设计模式 & 描述 & 核心作用 | 包括 |
|---|---|
创建型模式 提供创建对象用例。能够将软件模块中对象的创建和对象的使用分离 | 工厂模式(Factory Pattern) 抽象工厂模式(Abstract Factory Pattern) 单例模式(Singleton Pattern) 建造者模式(Builder Pattern) 原型模式(Prototype Pattern) |
结构型模式 关注类和对象的组合。描述如何将类或者对象结合在一起形成更大的结构 | 适配器模式(Adapter Pattern) 桥接模式(Bridge Pattern) 过滤器模式(Filter、Criteria Pattern) 组合模式(Composite Pattern) 装饰器模式(Decorator Pattern) 外观模式(Facade Pattern) 享元模式(Flyweight Pattern) 代理模式(Proxy Pattern) |
行为型模式 特别关注对象之间的通信。主要解决的就是“类或对象之间的交互”问题 | 责任链模式(Chain of Responsibility Pattern) 命令模式(Command Pattern) 解释器模式(Interpreter Pattern) 迭代器模式(Iterator Pattern) 中介者模式(Mediator Pattern) 备忘录模式(Memento Pattern) 观察者模式(Observer Pattern) 状态模式(State Pattern) 空对象模式(Null Object Pattern) 策略模式(Strategy Pattern) 模板模式(Template Pattern) 访问者模式(Visitor Pattern) |
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。