单一职责(SRP)
-
如何理解单一职责原则(SRP)?
单一职责原则的英文是 Single Responsibility Principle,缩写为 SRP。这个原则的英文描述是这样的:A class or module should have a single responsibility。如果我们把它翻译成中文,那就是:一个类或者模块只负责完成一个职责(或者功能)。
注意,这个原则描述的对象包含两个,一个是类(class),一个是模块(module)。关于这两个概念,有两种理解方式。一种理解是:把模块看作比类更加抽象的概念,类也可以看作模块。另一种理解是:把模块看作比类更加粗粒度的代码块,模块中包含多个类,多个类组成一个模块,不管哪种理解道理是想通的,下面以类作为分析对象,模块自行引申即可。
一个类只负责完成一个职责或者功能。不要设计大而全的类,要设计粒度小、功能单一的类。单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。
如何判断类的职责是否足够单一?
不同的应用场景、不同阶段的需求背景、不同的业务层面,对同一个类的职责是否单一,可能会有不同的判定结果。所以我们可以先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候,我们就可以将这个粗粒度的类,拆分成几个更细粒度的类(持续重构)。
实际上,一些侧面的判断指标更具有指导意义和可执行性,比如,出现下面这些情况就有可能说明这类的设计不满足单一职责原则:
- 类中的代码行数、函数或者属性过多;
- 类依赖的其他类过多,或者依赖类的其他类过多;
- 私有方法过多;
- 比较难给类起一个合适的名字;
- 类中大量的方法都是集中操作类中的某几个属性。
/**
* UserInfo类
*
* 该类是否满足单一职责?
*
* 分析问题要结合实际的应用场景:如果在这个社交产品中,用户的地址信息跟其他信息一样,只是单纯地用来展示,那 UserInfo 现在的设计就是合理的。
* 但是,如果这个社交产品发展得比较好,之后又在产品中添加了电商的模块,用户的地址信息还会用在电商物流中,那我们最好将地址信息从 UserInfo 中拆分出来,独立成用户物流信息(或者叫地址信息、收货信息等)。
*
*/
@Getter
@Setter
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; // 详细地址
}
-
类的职责是否设计得越单一越好?
单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、低耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。
/**
* Serialization类
*
* 拆分过度问题:以序列化为例经过拆分之后,Serializer 类和 Deserializer 类的职责更加单一了,
* 但也随之带来了新的问题。如果我们修改了协议的格式,数据标识从“UEUEUE”改为“DFDFDF”,或者序列
* 化方式从 JSON 改为了 XML,那 Serializer 类和 Deserializer 类都需要做相应的修改,代码的
* 内聚性显然没有原来 Serialization 高了。而且,如果我们仅仅对 Serializer 类做了协议修改,而
* 忘记了修改 Deserializer 类的代码,那就会导致序列化、反序列化不匹配,程序运行出错,也就是说,
* 拆分之后,代码的可维护性变差了。
*
*
*/
public class Serialization {
private static final String IDENTIFIER_STRING = "UEUEUE;";
public String serialze(Map<String, String> object) {
StringBuilder textBuilder = new StringBuilder(IDENTIFIER_STRING);
textBuilder.append(JSON.toJSONString(object));
return textBuilder.toString();
}
public Map<String, String> deserialize(String text){
if(!text.startsWith(IDENTIFIER_STRING)){
return Collections.emptyMap();
}
text = text.substring(IDENTIFIER_STRING.length());
return JSON.parseObject(text,new TypeReference<HashMap<String,String>>(){});
}
}
public class Serializer {
private static final String IDENTIFIER_STRING = "UEUEUE;";
public String serialze(Map<String, String> object) {
StringBuilder textBuilder = new StringBuilder(IDENTIFIER_STRING);
textBuilder.append(JSON.toJSONString(object));
return textBuilder.toString();
}
}
public class Deserializer {
private static final String IDENTIFIER_STRING = "UEUEUE;";
public Map<String, String> deserialize(String text){
if(!text.startsWith(IDENTIFIER_STRING)){
return Collections.emptyMap();
}
text = text.substring(IDENTIFIER_STRING.length());
return JSON.parseObject(text,new TypeReference<HashMap<String,String>>(){});
}
}
接口隔离原则(ISP)
-
如何理解“接口隔离原则”?
接口隔离原则的英文翻译是“ Interface Segregation Principle”,缩写为 ISP。Robert Martin 在 SOLID 原则中是这样定义它的:“Clients should not be forced to depend upon interfaces that they do not use。”直译成中文的话就是:客户端不应该被强迫依赖它不需要的接口。其中的“客户端”,可以理解为接口的调用者或者使用者。
理解“接口隔离原则”的重点是理解其中的“接口”二字。这里有三种不同的理解。
- 如果把“接口”理解为一组接口集合,可以是某个微服务的接口,也可以是某个类库的接口等。如果部分接口只被部分调用者使用,我们就需要将这部分接口隔离出来,单独给这部分调用者使用,而不强迫其他调用者也依赖这部分不会被用到的接口。
/** * UserService接口 * * 场景:用户系统提供了一组跟用户相关的 API 给其他系统使用,比如:注册、登录、获取用户信息等。现在, * 我们的后台管理系统要实现删除用户的功能,希望用户系统提供一个删除用户的接口。这个时候我们该如何来做呢? * * 分析:方案一:在 UserService 中新添加一个 deleteUserByCellphone() 或 deleteUserById() 接口就可以了。 * 这个方法可以解决问题,但是也隐藏了一些安全隐患,删除用户是一个非常慎重的操作,我们只希望通过后台管理系统来执行, * 所以这个接口只限于给后台管理系统使用,如果在没有鉴权的情况下,加限制地被其他业务系统调用,就有可能导致误删用户。 * * 方案二:在没有鉴权情况下可以从代码层面规避上述风险,具体可以参照接口隔离原则,调用者不应该强迫依赖它不需要的接口, * 将删除接口单独放到另外一个接口 RestrictedUserService 中,然后将 RestrictedUserService 只打包提供给后台 * 管理系统来使用。 * */ public interface UserService { boolean register(String cellphone, String password); boolean login(String cellphone, String password); UserInfo getUserInfoById(long id); UserInfo getUserInfoByCellphone(String cellphone); } public interface RestrictedUserService { boolean deleteUserByCellphone(String cellphone); boolean deleteUserById(long id); } public class BackgroundUserServiceImpl implements UserService, RestrictedUserService { @Override public boolean deleteUserByCellphone(String cellphone) { return false; } @Override public boolean deleteUserById(long id) { return false; } @Override public boolean register(String cellphone, String password) { return false; } @Override public boolean login(String cellphone, String password) { return false; } @Override public UserInfo getUserInfoById(long id) { return null; } @Override public UserInfo getUserInfoByCellphone(String cellphone) { return null; } }
- 如果把“接口”理解为单个 API 接口或函数,函数的设计要功能单一,不要将多个不同的功能逻辑在一个函数中实现。部分调用者只需要函数中的部分功能,那我们就需要把函数拆分成粒度更细的多个函数,让调用者只依赖它需要的那个细粒度函数。
/** * Statistics类 * * 接口设计分析:count() 函数的功能不够单一,包含很多不同的统计功能,比如,求最大值、最小值、平均值等 * 场景一:如果在项目中,对每个统计需求,Statistics 定义的那几个统计信息都有涉及,那 count() * 函数的设计就是合理的。 * * 场景二:如果每个统计需求只涉及 Statistics 罗列的统计信息中一部分,比如,有的只需要用到 max、 * min、average 这三类统计信息,在这个应用场景下,count() 函数的设计就有点不合理了,这种场景下 * 需要将其拆分成粒度更细的多个统计函数。 * * 总结:ISP提供了一种判断接口是否职责单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只 * 使用部分接口或接口的部分功能,那接口的设计就不够职责单一。 * */ @Getter public class Statistics { private Long max; private Long min; private Long average; private Long sum; private Long percentile99; private Long percentile999; /** * 场景一下合理 */ public Statistics count(Collection<Long> dataSet) { Statistics statistics = new Statistics(); //求最大值 statistics.setMax(2L); // 最小值 statistics.setMin(0L); // 平均值 statistics.setAverage(1L); return statistics; } /** * 场景二下合理 * * @param dataSet * @return */ public Long max(Collection<Long> dataSet) { return 2L; } public Long min(Collection<Long> dataSet) { return 0L; } public Long average(Collection<Long> dataSet) { return 1L; } public void setMax(Long max) { this.max = max; } public void setMin(Long min) { this.min = min; } public void setAverage(Long average) { this.average = average; } }
- 如果把“接口”理解为 面向对象编程(OOP) 中的接口,也可以理解为面向对象编程语言中的接口语法。那接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数。
/** * Application类 * * 背景:假设我们的项目中用到了三个外部系统:Redis、MySQL、Kafka。每个系统都对应一系列配置信息, * 比如地址、端口、访问超时时间等。为了在内存中存储这些配置信息,供项目中的其他模块来使用,我 * 们分别设计实现了三个 Configuration 类:RedisConfig、MysqlConfig、KafkaConfig * * 需求: * 1.希望支持 Redis 和 Kafka 配置信息的热更新。所谓“热更新(hot update)”就是,如果在配 * 置中心中更改了配置信息,我们希望在不用重启系统的情况下,能将最新的配置信息加载到内存中(也就 * 是 RedisConfig、KafkaConfig 类中)。 * * 2.监控功能需求。通过命令行来查看 Zookeeper 中的配置信息是比较麻烦的。所以,我们希望能有一 * 种更加方便的配置信息查看方式。我们可以在项目中开发一个内嵌的 SimpleHttpServer,输出项目的 * 配置信息到一个固定的 HTTP 地址,比如:http://127.0.0.1:2389/config 。我们只需要在浏览 * 器中输入这个地址,就可以显示出系统的配置信息。不过,出于某些原因,我们只想暴露 MySQL 和 Redis * 的配置信息。 * */ public class Application { private static ConfigSource configSource = new ZookeerConfigSource(); private static final RedisConfig redisConfig = new RedisConfig(configSource); private static final KafkaConfig kafkaConfig = new KafkaConfig(configSource); private static final MysqlConfig mySqlConfig = new MysqlConfig(configSource); public static void main(String[] args) { ScheduledUpdater redisConfigUpdater = new ScheduledUpdater(redisConfig,300,300); redisConfigUpdater.run(); ScheduledUpdater kafkaConfigUpdater = new ScheduledUpdater(kafkaConfig,60,60); kafkaConfigUpdater.run(); SimpleHttpServer simpleHttpServer = new SimpleHttpServer("127.0.0.1",2389); simpleHttpServer.addViewer("/config",redisConfig); simpleHttpServer.addViewer("/config",mySqlConfig); } } /** * Updater热更新接口 */ public interface Updater { /** * 热部署,从configSource加载配置到address/timeout/maxTotal */ void update(); } /** * Viewer监控接口 */ public interface Viewer { /** * 监控-输出文本信息 */ String outputInPlainText(); /** * 监控-输出监控项 */ Map<String,String> output(); } //接口处理类 public class ScheduledUpdater { private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); private long initialDelayInSeconds; private long periodInSeconds; private Updater updater; public ScheduledUpdater(Updater updater, long initialDelayInSeconds, long periodInSeconds) { this.initialDelayInSeconds = initialDelayInSeconds; this.periodInSeconds = periodInSeconds; this.updater = updater; } public void run(){ executor.scheduleAtFixedRate(new Runnable() { @Override public void run() { updater.update(); } },this.initialDelayInSeconds,this.periodInSeconds, TimeUnit.SECONDS); } } public class SimpleHttpServer { private String host; private int port; private Map<String, List<Viewer>> viewerMap = new HashMap<>(); public SimpleHttpServer(String host, int port) { this.host = host; this.port = port; } public void addViewer(String urlDirectory, Viewer viewer) { if (!viewerMap.containsKey(urlDirectory)) { viewerMap.put(urlDirectory, new ArrayList<Viewer>()); } viewerMap.get(urlDirectory).add(viewer); } public void run(){ // 输出项目的配置信息到一个固定的 HTTP 地址 // 比如:http://127.0.0.1:2389/config 。 // 我们只需要在浏览器中输入这个地址,就可以显示出系统的配置信息。 } } //config配置类 @Getter public abstract class AbstractConfig { /** * 配置中心(比如zookeeper) */ protected ConfigSource configSource; protected String address; protected int timeout; protected int maxTotal; } public class RedisConfig extends AbstractConfig implements Updater, Viewer { public RedisConfig(ConfigSource configSource) { super(); super.configSource = configSource; } /** * 热部署,从configSource加载配置到address/timeout/maxTotal */ @Override public void update() { super.address = configSource.getAddress(); super.timeout = configSource.getTimeout(); super.maxTotal = configSource.getMaxTotal(); } /** * 监控-输出文本信息 */ @Override public String outputInPlainText() { return JSON.toJSONString(this); } /** * 监控-输出监控项 */ @Override public Map<String, String> output() { return JSON.parseObject(this.outputInPlainText(), new TypeReference<HashMap<String, String>>(){}); } } public class MysqlConfig extends AbstractConfig implements Viewer { public MysqlConfig(ConfigSource configSource) { super(); super.configSource = configSource; } @Override public String outputInPlainText() { return JSON.toJSONString(this); } @Override public Map<String, String> output() { return JSON.parseObject(this.outputInPlainText(), new TypeReference<HashMap<String, String>>(){}); } } public class KafkaConfig extends AbstractConfig implements Updater { public KafkaConfig(ConfigSource configSource) { super(); super.configSource = configSource; } @Override public void update() { super.address = configSource.getAddress(); super.timeout = configSource.getTimeout(); super.maxTotal = configSource.getMaxTotal(); } }
-
接口隔离原则与单一职责原则的区别
单一职责原则针对的是模块、类、接口的设计。接口隔离原则相对于单一职责原则,一方面更侧重于接口的设计,另一方面它的思考角度也是不同的。接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。