第二部分:理论四
第二部分:理论四
理论四
如何理解“接口隔离原则”?
- SOLID 中的英文字母“I”,接口隔离原则,英文翻译是“ Interface Segregation Principle”,缩写为 ISP。
- 客户端不应该强迫依赖它不需要的接口。
如何理解“接口”二字?
把“接口”理解为一组 API 接口集合
- 比如:微服务用户系统提供了一组跟用户相关的 API 给其他系统使用,UserService。
- 现在需要在用户系统中提供一个删除用户接口,可以在 UserService 中添加 deleteUserById()。
- 但是带来了安全隐患,所有使用到 UserService 的系统,都可以调用这个接口,我们只希望通过后台管理系统来执行。
- 可以将删除接口单独放到另外一个接口 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 class UserServiceImpl implements UserService {
//...
}
将删除接口单独放到另外一个接口 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 UserServiceImpl implements UserService, RestrictedUserService {
// ... 省略实现代码...
}
把“接口”理解为单个 API 接口或函数
- 比如,包含统计功能的count()函数。
- 那么,求最大值、最小值、平均值等功能就不应该在count()函数中,应该封装成独立的函数max()、min()、average()等。
- 设计的时候,如果每个统计需求都要用到那几个计算,放在count()函数中也算合理。
- 但是,如果不同的统计需求只用到部分计算,还是不同的部分组合,把不同的计算放在不同的函数中,每次统计挑选需要的计算函数,不用每次都过一遍所有的计算,提升系统的性能。
- 接口隔离原则跟单一职责原则有点类似,单一职责原则针对的是模块、类、接口的设计。而接口隔离原则更侧重于接口的设计,思考的角度是通过调用者如何使用接口来间接地判定接口是否职责单一。
代码示例:count() 函数的功能不够单一,应该把 count() 函数拆成几个更小粒度的函数
public class Statistics {
private Long max;
private Long min;
private Long average;
private Long sum;
private Long percentile99;
private Long percentile999;
//... 省略 constructor/getter/setter 等方法...
}
public Statistics count(Collection<Long> dataSet) {
Statistics statistics = new Statistics();
//... 省略计算逻辑...
return statistics;
}
拆分之后的代码:
public Long max(Collection<Long> dataSet) { //... }
public Long min(Collection<Long> dataSet) { //... }
public Long average(Colletion<Long> dataSet) { //... }
// ... 省略其他统计函数...
把“接口”理解为 OOP 中的接口概念
接口隔离示例:
- 比如,我们的项目中用到了三个外部系统:Redis、MySQL、Kafka,对应一系列配置信息 RedisConfig、MysqlConfig、KafkaConfig 类。
- 增加 Redis 和 Kafka 配置信息需要热更新的需求:
- 更新接口 Updater 中有一个热更新方法 Updater()
- RedisConfig、KafkaConfig 实现接口 Updater 中的 Updater()
- ScheduledUpdater 类中调用 RedisConfig、KafkaConfig 的 Updater() 方法,以实现更新配置
- 增加 MySQL 和 Redis 监控功能,需要通过HTTP地址访问:
- 展示接口 Viewer 中有一个展示方法 output()
- MysqlConfig、RedisConfig 实现接口Viewer中的 output()
- SimpleHttpServer 类中的 addViewers() 方法,实例化的时候可以添加参数MysqlConfig、RedisConfig
- 上面两个例子中的 Updater 和 Viewer 接口功能单一,ScheduledUpdater 只依赖 Updater,SimpleHttpServer 只依赖 Viewer,都不需要被强迫依赖不需要的接口。
RedisConfig 的代码实现:
public class RedisConfig {
private ConfigSource configSource; // 配置中心(比如 zookeeper)
private String address;
private int timeout;
private int maxTotal;
// 省略其他配置: maxWaitMillis,maxIdle,minIdle...
public RedisConfig(ConfigSource configSource) {
this.configSource = configSource;
}
public String getAddress() {
return this.address;
}
//... 省略其他 get()、init() 方法...
public void update() {
// 从 configSource 加载配置到 address/timeout/maxTotal...
}
}
public class KafkaConfig {
//... 省略...
}
public class MysqlConfig {
//... 省略...
}
为了支持 Redis 和 Kafka 配置信息的热更新,并不对 MySQL 的配置信息进行热更新,设计实现了一个 ScheduledUpdater 类,以固定时间频率(periodInSeconds)来调用 RedisConfig、KafkaConfig 的 update() 方法更新配置信息:
public interface Updater {
void update();
}
public class RedisConfig implements Updater {
//... 省略其他属性和方法...
@Override
public void update() {
//...
}
}
public class KafkaConfig implements Updater {
//... 省略其他属性和方法...
@Override
public void update() {
//...
}
}
public class MysqlConfig {
//... 省略其他属性和方法...
}
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
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.updater = updater;
this.initialDelayInSeconds = initialDelayInSeconds;
this.periodInSeconds = periodInSeconds;
}
public void run() {
executor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
updater.update();
}
}, this.initialDelayInSeconds, this.periodInSeconds, TimeUnit.SECONDS);
}
}
public class Application {
ConfigSource configSource = new ZookeeperConfigSource(/* 省略参数 */);
public static final RedisConfig redisConfig = new RedisConfig(configSource);
public static final KafkaConfig kafkaConfig = new KakfaConfig(configSource);
public static final MySqlConfig mysqlConfig = new MysqlConfig(configSource);
public static void main(String[] args) {
ScheduledUpdater redisConfigUpdater = new ScheduledUpdater(redisConfig,
redisConfigUpdater.run());
ScheduledUpdater kafkaConfigUpdater = new ScheduledUpdater(kafkaConfig,
kafkaConfigUpdater.run());
}
}
为了方便查看 Zookeeper 中的配置信息,可以开发一个内嵌的 SimpleHttpServer,但只想暴露 MySQL和 Redis 的配置信息,不想暴露 Kafka 的配置信息,实现代码:
public interface Updater {
void update();
}
public interface Viewer {
String outputInPlainText();
Map<String, String> output();
}
public class RedisConfig implements Updater, Viewer {
//... 省略其他属性和方法...
@Override
public void update() {
//...
}
@Override
public String outputInPlainText() {
//...
}
@Override
public Map<String, String> output() {
//...
}
}
public class KafkaConfig implements Updater {
//... 省略其他属性和方法...
@Override
public void update() {
//...
}
}
public class MysqlConfig implements Viewer {
//... 省略其他属性和方法...
@Override
public String outputInPlainText() {
//...
}
@Override
public Map<String, String> output() {
//...
}
}
public class SimpleHttpServer {
private String host;
private int port;
private Map<String, List<Viewer>> viewers = new HashMap<>();
public SimpleHttpServer(String host, int port) {
//...
}
public void addViewers(String urlDirectory, Viewer viewer) {
if (!viewers.containsKey(urlDirectory)) {
viewers.put(urlDirectory, new ArrayList<Viewer>());
}
this.viewers.get(urlDirectory).add(viewer);
}
public void run() {
//...
}
}
public class Application {
ConfigSource configSource = new ZookeeperConfigSource();
public static final RedisConfig redisConfig = new RedisConfig(configSource)
public static final KafkaConfig kafkaConfig = new KakfaConfig(configSource)
public 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);
redisConfigUpdater.run();
SimpleHttpServer simpleHttpServer = new SimpleHttpServer(“127.0.0.1”, 8080);
simpleHttpServer.addViewer("/config", redisConfig);
simpleHttpServer.addViewer("/config", mysqlConfig);
simpleHttpServer.run();
}
}
至此,热更新和监控的需求就都实现了,我们设计了两个功能非常单一的接口:Updater 和 Viewer。ScheduledUpdater 只依赖 Updater 这个跟热更新相关的接口,不需要被强迫去依赖不需要的 Viewer 接口,满足接口隔离原则。同理,SimpleHttpServer 只依赖跟查看信息相关的 Viewer 接口,不依赖不需要的 Updater 接口,也满足接口隔离原则。
非接口隔离示例:
- 还是上文的需求,设计一个大而全的 Config 接口:
- 接口 Config 中有热更新方法 Updater(),展示方法 output()
- RedisConfig、MysqlConfig、KafkaConfig 实现接口 Config 中的所有方法和属性
- ScheduledUpdater 类和 SimpleHttpServer 类中使用全部的配置文件
代码实现:
public interface Config {
void update();
String outputInPlainText();
Map<String, String> output();
}
public class RedisConfig implements Config {
//... 需要实现 Config 的三个接口 update/outputIn.../output
}
public class KafkaConfig implements Config {
//... 需要实现 Config 的三个接口 update/outputIn.../output
}
public class MysqlConfig implements Config {
//... 需要实现 Config 的三个接口 update/outputIn.../output
}
public class ScheduledUpdater {
//... 省略其他属性和方法..
private Config config;
public ScheduleUpdater(Config config, long initialDelayInSeconds, long periodInSeconds) {
this.config = config;
//...
}
//...
}
public class SimpleHttpServer {
private String host;
private int port;
private Map<String, List<Config>> viewers = new HashMap<>();
public SimpleHttpServer(String host, int port) {
//...
}
public void addViewer(String urlDirectory, Config config) {
if (!viewers.containsKey(urlDirectory)) {
viewers.put(urlDirectory, new ArrayList<Config>());
}
viewers.get(urlDirectory).add(config);
}
public void run() {
//...
}
}
第一种设计思路更加灵活、易扩展、易复用。因为 Updater、Viewer 职责更加单一,单一就意味了通用、复用性好。比如:现在又有一个新的需求,开发一个Metrics 性能统计模块,并且希望将 Metrics 也通过 SimpleHttpServer 显示在网页上,以方便查看。
尽管 Metrics 跟 RedisConfig 等没有任何关系,但仍然可以让 Metrics 类实现非常通用的 Viewer 接口,复用 SimpleHttpServer 的代码实现。具体的代码如下所示:
public class ApiMetrics implements Viewer {
//...
}
public class DbMetrics implements Viewer {
//...
}
public class Application {
ConfigSource configSource = new ZookeeperConfigSource();
public static final RedisConfig redisConfig = new RedisConfig(configSource);
public static final KafkaConfig kafkaConfig = new KakfaConfig(configSource);
public static final MySqlConfig mySqlConfig = new MySqlConfig(configSource);
public static final ApiMetrics apiMetrics = new ApiMetrics();
public static final DbMetrics dbMetrics = new DbMetrics();
public static void main(String[] args) {
SimpleHttpServer simpleHttpServer = new SimpleHttpServer(“127.0.0.1”, 8080);
simpleHttpServer.addViewer("/config", redisConfig);
simpleHttpServer.addViewer("/config", mySqlConfig);
simpleHttpServer.addViewer("/metrics", apiMetrics);
simpleHttpServer.addViewer("/metrics", dbMetrics);
simpleHttpServer.run();
}
}
总结
- 首先,第一种设计思路更加灵活、易扩展、易复用。
- Updater 和 Viewer 接口功能单一,可以很方便复用
- 如果我们增加统计模块,也想展示在网页上,就可以复用 SimpleHttpServer,调用 addViewers() 方法,和配置类没有任何关系
- 其次,第二种设计思路在代码实现上做了一些无用功。
- KafkaConfig 只需要实现 Updater(),不需要实现 output()
- MysqlConfig 只需要实现 output(), 不需要实现 Updater()
- 而第二种设计中,RedisConfig、MysqlConfig、KafkaConfig 需要实现 Config 的所有方法。
- 另外,如果往 Config 中添加一个新的方法,那么所有实现类RedisConfig、MysqlConfig、KafkaConfig 都要改一遍。