SOLID原则:由 5 个设计原则组成的,它们分别是:单一职责原则、开闭原则、里式替换原则、接口隔离原则和依赖反转原则,依次对应 SOLID 中的 S、O、L、I、D 这 5 个英文字母,SRP单一职责原则 Single Responsibility Principle; KISS保持简单 Keep It Simple and Stupid; YAGNI不需要原则 You Ain’t Gonna Need It ; DRY 不要重复原则 Don’t Repeat Yourself ; LOD 迪米特法则 Law of Demeter。
单一职责原则的英文是 Single Responsibility Principle,缩写为 SRP。这个原则的英文描述是这样的:A class or module should have a single responsibility。如果我们把它翻译成中文,那就是:一个类或者模块只负责完成一个职责(或者功能)。
开闭原则的英文全称是 Open Closed Principle,简写为 OCP。它的英文描述是:software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification。我们把它翻译成中文就是:软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭”。
public void check(String api, long requestCount, long errorCount, long durationOfSeconds) { long tps = requestCount / durationOfSeconds; if (tps > rule.getMatchedRule(api).getMaxTps()) { notification.notify(NotificationEmergencyLevel.URGENCY, "..."); } if (errorCount > rule.getMatchedRule(api).getMaxErrorCount()) { notification.notify(NotificationEmergencyLevel.SEVERE, "..."); } } }
public class Alert { private List<AlertHandler> alertHandlers = new ArrayList<>(); public void addAlertHandler(AlertHandler alertHandler) { this.alertHandlers.add(alertHandler); }
public void check(ApiStatInfo apiStatInfo) { for (AlertHandler handler : alertHandlers) { handler.check(apiStatInfo); } } }
public class ApiStatInfo {//省略constructor/getter/setter方法 private String api; private long requestCount; private long errorCount; private long durationOfSeconds; }
public abstract class AlertHandler { protected AlertRule rule; protected Notification notification; public AlertHandler(AlertRule rule, Notification notification) { this.rule = rule; this.notification = notification; } public abstract void check(ApiStatInfo apiStatInfo); }
public class TpsAlertHandler extends AlertHandler { public TpsAlertHandler(AlertRule rule, Notification notification) { super(rule, notification); }
@Override public void check(ApiStatInfo apiStatInfo) { long tps = apiStatInfo.getRequestCount()/ apiStatInfo.getDurationOfSeconds(); if (tps > rule.getMatchedRule(apiStatInfo.getApi()).getMaxTps()) { notification.notify(NotificationEmergencyLevel.URGENCY, "..."); } } }
public class ErrorAlertHandler extends AlertHandler { public ErrorAlertHandler(AlertRule rule, Notification notification){ super(rule, notification); }
@Override public void check(ApiStatInfo apiStatInfo) { if (apiStatInfo.getErrorCount() > rule.getMatchedRule(apiStatInfo.getApi()).getMaxErrorCount()) { notification.notify(NotificationEmergencyLevel.SEVERE, "..."); } } }
public class ApplicationContext { private AlertRule alertRule; private Notification notification; private Alert alert; public void initializeBeans() { alertRule = new AlertRule(/*.省略参数.*/); //省略一些初始化代码 notification = new Notification(/*.省略参数.*/); //省略一些初始化代码 alert = new Alert(); alert.addAlertHandler(new TpsAlertHandler(alertRule, notification)); alert.addAlertHandler(new ErrorAlertHandler(alertRule, notification)); } public Alert getAlert() { return alert; }
// 饿汉式单例 private static final ApplicationContext instance = new ApplicationContext(); private ApplicationContext() { initializeBeans(); } public static ApplicationContext getInstance() { return instance; } }
public class Demo { public static void main(String[] args) { ApiStatInfo apiStatInfo = new ApiStatInfo(); // ...省略设置apiStatInfo数据值的代码 ApplicationContext.getInstance().getAlert().check(apiStatInfo); } }
如果这时需要添加其他规则警告,只需按如下进行改动
public class Alert { // 代码未改动... } public class ApiStatInfo {//省略constructor/getter/setter方法 private String api; private long requestCount; private long errorCount; private long durationOfSeconds; private long timeoutCount; // 改动一:添加新字段 } public abstract class AlertHandler { //代码未改动... } public class TpsAlertHandler extends AlertHandler {//代码未改动...} public class ErrorAlertHandler extends AlertHandler {//代码未改动...} // 改动二:添加新的handler public class TimeoutAlertHandler extends AlertHandler {//省略代码...}
public class ApplicationContext { private AlertRule alertRule; private Notification notification; private Alert alert; public void initializeBeans() { alertRule = new AlertRule(/*.省略参数.*/); //省略一些初始化代码 notification = new Notification(/*.省略参数.*/); //省略一些初始化代码 alert = new Alert(); alert.addAlertHandler(new TpsAlertHandler(alertRule, notification)); alert.addAlertHandler(new ErrorAlertHandler(alertRule, notification)); // 改动三:注册handler alert.addAlertHandler(new TimeoutAlertHandler(alertRule, notification)); } //...省略其他未改动代码... }
public class Demo { public static void main(String[] args) { ApiStatInfo apiStatInfo = new ApiStatInfo(); // ...省略apiStatInfo的set字段代码 apiStatInfo.setTimeoutCount(289); // 改动四:设置tiemoutCount值 ApplicationContext.getInstance().getAlert().check(apiStatInfo); }
// 这一部分体现了抽象意识 public interface MessageQueue { //... } public class KafkaMessageQueue implements MessageQueue { //... } public class RocketMQMessageQueue implements MessageQueue {//...}
public interface MessageFromatter { //... } public class JsonMessageFromatter implements MessageFromatter {//...} public class ProtoBufMessageFromatter implements MessageFromatter {//...}
public class Demo { private MessageQueue msgQueue; // 基于接口而非实现编程 public Demo(MessageQueue msgQueue) { // 依赖注入 this.msgQueue = msgQueue; } // msgFormatter:多态、依赖注入 public void sendNotification(Notification notification, MessageFormatter msgFormatter) { //... } }
If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program。 Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it。
中文描述:子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。
里式替换原则的理解
里式替换原则是用来指导,继承关系中子类该如何设计的一个原则。理解里式替换原则,最核心的就是理解“design by contract,按照协议来设计”这几个字。父类定义了函数的“约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数原有的“约定”。这里的约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。
public class UserServiceImpl implements UserService, RestrictedUserService { // ...省略实现代码... }
把“接口”理解为单个 API 接口或函数
如果把“接口”理解为单个 API 接口或函数,部分调用者只需要函数中的部分功能,那我们就需要把函数拆分成粒度更细的多个函数,让调用者只依赖它需要的那个细粒度函数。
比如一下代码 count() 函数功能不够单一,包含了很多不同的统计功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
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) { //... } // ...省略其他统计函数...
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 配置信息的热更新。所谓“热更新(hot update)”就是,如果在配置中心中更改了配置信息,我们希望在不用重启系统的情况下,能将最新的配置信息加载到内存中(也就是 RedisConfig、KafkaConfig 类中)。但是,因为某些原因,我们并不希望对 MySQL 的配置信息进行热更新。
public class RedisConfig implemets Updater { //...省略其他属性和方法... @Override public void update() { //... } }
public class KafkaConfig implements Updater { //...省略其他属性和方法... @Override public void update() { //... } }
public class MysqlConfig { //...省略其他属性和方法... }
public class ScheduledUpdater { private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();; private long initialDelayInSeconds; private long periodInSeconds; private Updater updater;
public ScheduleUpdater(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, 300, 300); redisConfigUpdater.run(); ScheduledUpdater kafkaConfigUpdater = new ScheduledUpdater(kafkaConfig, 60, 60); redisConfigUpdater.run(); } }
接着,有一个新的监控需求,期望输出项目的配置信息到一个固定的 HTTP 地址,比如:http://127.0.0.1:2389/config 。我们只需要在浏览器中输入这个地址,就可以显示出系统的配置信息。不过,出于某些原因,我们只想暴露 MySQL 和 Redis 的配置信息,不想暴露 Kafka 的配置信息。
public interface Viewer { String outputInPlainText(); Map<String, String> output(); }
public class RedisConfig implemets 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”, 2389); simpleHttpServer.addViewer("/config", redisConfig); simpleHttpServer.addViewer("/config", mysqlConfig); simpleHttpServer.run(); } }
现成的依赖注入框架有很多,比如 Google Guice、Java Spring、Pico Container、Butterfly Container 等。不过,如果你熟悉 Java Spring 框架,你可能会说,Spring 框架自己声称是控制反转容器(Inversion Of Control Container)。
依赖反转原则(DIP)
High-level modules shouldn’t depend on low-level modules. Both modules should depend on abstractions. In addition, abstractions shouldn’t depend on details. Details depend on abstractions.
Each unit should have only limited knowledge about other units: only units “closely” related to the current unit. Or: Each unit should only talk to its friends; Don’t talk to strangers.
每个模块(unit)只应该了解那些与它关系密切的模块(units: only units “closely” related to the current unit)的有限知识(knowledge)。或者说,每个模块只和自己的朋友“说话”(talk),不和陌生人“说话”(talk)。
//应用场景:统计下面两个接口(注册和登录)的响应时间和访问次数 public class UserController { private Metrics metrics = new Metrics(); public UserController() { metrics.startRepeatedReport(60, TimeUnit.SECONDS); }
public void register(UserVo user) { long startTimestamp = System.currentTimeMillis(); metrics.recordTimestamp("regsiter", startTimestamp); //... long respTime = System.currentTimeMillis() - startTimestamp; metrics.recordResponseTime("register", respTime); }
public UserVo login(String telephone, String password) { long startTimestamp = System.currentTimeMillis(); metrics.recordTimestamp("login", startTimestamp); //... long respTime = System.currentTimeMillis() - startTimestamp; metrics.recordResponseTime("login", respTime); } }
基于最新的原型进一步考虑
图可以非常直观地体现设计思想,并且能有效地帮助我们释放更多的脑空间,来思考其他细节问题。
把整个框架分为四个模块:数据采集、存储、聚合统计、显示。每个模块负责的工作简单罗列如下。
数据采集:负责打点采集原始数据,包括记录每次接口请求的响应时间和请求时间。数据采集过程要高度容错,不能影响到接口本身的可用性。除此之外,因为这部分功能是暴露给框架的使用者的,所以在设计数据采集 API 的时候,我们也要尽量考虑其易用性。
大致地识别出几个核心的类之后,我的习惯性做法是,先在 IDE 中创建好这几个类,然后开始试着定义它们的属性和方法。在设计类、类与类之间交互的时候,我会不断地用之前学过的设计原则和思想来审视设计是否合理,比如,是否满足单一职责原则、开闭原则、依赖注入、KISS 原则、DRY 原则、迪米特法则,是否符合基于接口而非实现编程思想,代码是否高内聚、低耦合,是否可以抽象出可复用代码等等。
public interface MetricsStorage { void saveRequestInfo(RequestInfo requestInfo);
List<RequestInfo> getRequestInfos(String apiName, long startTimeInMillis, long endTimeInMillis);
Map<String, List<RequestInfo>> getRequestInfos(long startTimeInMillis, long endTimeInMillis); }
public class RedisMetricsStorage implements MetricsStorage { //...省略属性和构造函数等... @Override public void saveRequestInfo(RequestInfo requestInfo) { //... }
@Override public List<RequestInfo> getRequestInfos(String apiName, long startTimestamp, long endTimestamp) { //... }
@Override public Map<String, List<RequestInfo>> getRequestInfos(long startTimestamp, long endTimestamp) { //... } }
public class Demo { public static void main(String[] args) { MetricsStorage storage = new RedisMetricsStorage(); ConsoleReporter consoleReporter = new ConsoleReporter(storage); consoleReporter.startRepeatedReport(60, 60);
EmailReporter emailReporter = new EmailReporter(storage); emailReporter.addToAddress("wangzheng@xzg.com"); emailReporter.startDailyReport();