1. Single principle
- Too many lines of code, functions or attributes in the class will affect the readability and maintainability of the code, so we need to consider splitting the class;
- Classes depend on too many other classes, or depend on too many other classes, which does not conform to the design idea of high cohesion and low coupling, so we need to consider splitting classes;
- If there are too many private methods, we should consider whether we can separate the private methods into new classes and set them as public methods for more classes to use, so as to improve the reusability of the code;
- It is difficult to give a suitable name to a class, and it is difficult to summarize it with a business term, or it can only be named with some general words such as Manager and Context, which indicates that the definition of class responsibilities may not be clear enough;
- A large number of methods in the class operate on certain attributes in the class. For example, in the UserInfo example, if half of the methods operate on address information, you can consider splitting these attributes and corresponding methods.
2. Opening and closing principle
Software entities (modules, classes, methods, etc.) should be "open to extension and closed to modification", that is, adding a new function should be to extend the code (add modules, classes, methods, etc.) based on the existing code, rather than modify the existing code (modify modules, classes, methods, etc.).
With the following Take API interface monitoring alarm code as an example:
public class Alert { private AlertRule rule; //Storage alarm rules private Notification notification; //Alarm notification, which supports email, SMS, wechat, mobile phone and other notification channels public Alert(AlertRule rule, Notification notification) { this.rule = rule; this.notification = notification; } public void check(String api, long requestCount, long errorCount, long durationOfSeconds) { long tps = requestCount / durationOfSeconds; // TPS exceeds a preset maximum if (tps > rule.getMatchedRule(api).getMaxTps()) { notification.notify(NotificationEmergencyLevel.URGENCY, "..."); } // The number of request errors is greater than a maximum allowed value if (errorCount > rule.getMatchedRule(api).getMaxErrorCount()) { notification.notify(NotificationEmergencyLevel.SEVERE, "..."); } } }
Now, if we need to add a function, when the number of interface timeout requests per second exceeds a preset maximum threshold, we also need to trigger an alarm to send a notification. At this time, there are two changes: the first is to modify the input parameters of the check() function and add a new statistical data timeoutCount, which represents the number of timeout interface requests; The second is to add new alarm logic to the check() function.
public class Alert { // ... omit the AlertRule/Notification property and constructor // Change 1: add the parameter timeoutCount public void check(String api, long requestCount, long errorCount, long timeoutCount, 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, "..."); } // Change 2: add interface timeout processing logic long timeoutTps = timeoutCount / durationOfSeconds; if (timeoutTps > rule.getMatchedRule(api).getMaxTimeoutTps()) { notification.notify(NotificationEmergencyLevel.URGENCY, "..."); } } }
We have modified the interface, which means that the code calling the interface must be modified accordingly. On the other hand, the check() function is modified, and the corresponding unit tests need to be modified, which obviously does not comply with the opening and closing principle. We can refactor Alert differently:
- Encapsulate multiple input parameters of the check() function into the ApiStatInfo class;
- The concept of handler is introduced, and the if judgment logic is dispersed in each handler
Refactored Code:
public abstract class AlterHander { private AlertRule rule; //Storage alarm rules private Notification notification; //Alarm notification, which supports email, SMS, wechat, mobile phone and other notification channels public AlterHander(AlertRule rule, Notification notification) { this.rule = rule; this.notification = notification; } public abstract void check(ApiStatInfo apiStatInfo); } public class ErrorAlertHandler extends AlterHander { 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 TpsAlertHandler extends AlterHander { 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 ApiStatInfo { private String api; private long requestCount; private long errorCount; private long durationOfSeconds; } 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); } } }
After new requirements, we can add parameters and class inheritance in ApiStatInfo Alterhandler, using the reconstructed Alert:
public class ApplicationContext { private AlertRule alertRule; private Notification notification; private Alert alert; public void initializeBeans() { alertRule = new AlertRule(/*.Omit parameters*/); //Omit some initialization code notification = new Notification(/*.Omit parameters*/); // Omit some initialization code alert = new Alert(); alert.addAlertHandler(new TpsAlertHandler(alertRule, notification)); alert.addAlertHandler(new ErrorAlertHandler(alertRule, notification)); } public Alert getAlert() { return alert; } // Hungry Han style single case private static final ApplicationContext instance = new ApplicationContext(); private ApplicationContext() { initializeBeans(); } public static ApplicationContext getInstance() { return instance; } public static void main(String[] args) { ApiStatInfo apiStatInfo = new ApiStatInfo(); // ... omit the code to set the apiStatInfo data value ApplicationContext.getInstance().getAlert().check(apiStatInfo); } }
3. Richter substitution principle
The subclass object (object of subtype/derived class) can replace any place where the parent object (object of base/parent class) appears in the program, and ensure that the logical behavior of the original program is unchanged and the correctness is not damaged.
Several examples of violation of Richter's substitution principle:
- The subclass violates the function declared by the parent class to be implemented. The sortOrdersByAmount() order sorting function provided in the parent class sorts the orders according to the amount from small to large. After overriding the sortOrdersByAmount() order sorting function, the subclass sorts the orders according to the creation date. The design of that subclass violates the principle of internal substitution.
- The subclass violates the parent class's conventions on input, output and exception. In the parent class, a function Convention: return null when running error; When the acquired data is empty, an empty collection is returned. After the subclass overloads the function, the implementation changes, an exception is returned when running an error, and null is returned when the data is not obtained. The design of that subclass violates the principle of internal substitution. In the parent class, a function stipulates that the input data can be any integer, but when the subclass is implemented, only the input data is allowed to be positive integers, and negative numbers are thrown. In other words, the verification of the input data by the subclass is more strict than that of the parent class, and the design of the subclass violates the internal substitution principle. In the parent class, a function convention will only throw ArgumentNullException exception. In the design and implementation of that subclass, only ArgumentNullException exception is allowed. The throwing of any other exception will cause the subclass to violate the internal replacement principle.
- The subclass violates any special instructions listed in the parent class's comments. The comment of the withraw() withdrawal function defined in the parent class reads: "the user's withdrawal amount shall not exceed the account balance...". After rewriting the withraw() function, the subclass realizes the overdraft withdrawal function for the VIP account, that is, the withdrawal amount can be greater than the account balance, The design of this subclass also does not conform to the principle of Li substitution.
4. Interface isolation principle:
The client should not be forced to rely on interfaces it does not need. The "client" can be understood as the caller or user of the interface.
Suppose three external systems are used in our project: Redis, MySQL and Kafka. Each system corresponds to a series of Configuration information, such as address, port, access timeout, etc. In order to store these Configuration information in memory for use by other modules in the project, we have designed and implemented three Configuration classes: RedisConfig, MysqlConfig and KafkaConfig.
public class RedisConfig { private ConfigSource configSource; //Configuration center (such as zookeeper) private String address; private int timeout; private int maxTotal; //Omit other configurations: maxWaitMillis,maxIdle,minIdle public RedisConfig(ConfigSource configSource) { this.configSource = configSource; } public String getAddress() { return this.address; } //... omit other get(), init() methods public void update() { //Load configuration from configSource to address/timeout/maxTotal } } public class KafkaConfig { //... omit...} public class MysqlConfig { //... omit...}
The requirement is to support the hot update of Redis and Kafka configuration information, rather than the hot update of MySQL configuration information. In order to achieve such a functional requirement, we designed and implemented a scheduleupdater class to call the update() methods of RedisConfig and KafkaConfig to update the configuration information at a fixed time and frequency (periodInSeconds)
public interface Updater { void update(); } public class RedisConfig implemets Updater { //... omit other properties and methods @Override public void update() { //... } } public class KafkaConfig implements Updater { //... omit other properties and methods @Override public void update() { //... } } public class MysqlConfig { //... omit other properties and methods...} 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(/ellipsis parameter /); 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); kafkaConfigUpdater.run(); } }
New monitoring function requirements:
It is troublesome to view the configuration information in Zookeeper through the command line. We hope to have a more convenient way to view the configuration information. Develop an embedded SimpleHttpServer in the project and output the project configuration information to a fixed HTTP address, such as: http://127.0.0.1:2389/config</a> . We only need to enter this address in the browser to display the system configuration information. We only expose the configuration information of MySQL and Redis, not Kafka
public interface Updater { void update(); } public interface Viewer { String outputInPlainText(); Map<String, String> output(); } public class RedisConfig implemets Updater, Viewer { //... omit other properties and methods @Override public void update() { //... } @Override public String outputInPlainText() { //... } @Override public Map<String, String> output() { //...} } public class KafkaConfig implements Updater { //... omit other properties and methods @Override public void update() { //... } } public class MysqlConfig implements Viewer { //... omit other properties and methods @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(); } }
We have designed two interfaces with very single function: Updater and Viewer. The scheduleupdater only relies on the Updater, which is an interface related to hot update. It does not need to be forced to rely on the unnecessary Viewer interface, meeting the interface isolation principle. Similarly, SimpleHttpServer only relies on the Viewer interface related to viewing information, does not rely on the unnecessary Updater interface, and also meets the interface isolation principle.
If we do not follow the principle of interface isolation, instead of designing two small interfaces, Updater and Viewer, we design a large and comprehensive Config interface, let RedisConfig, KafkaConfig and MysqlConfig implement this Config interface, and replace the Updater originally passed to scheduleupdater and Viewer passed to SimpleHttpServer with Config
public interface Config { void update(); String outputInPlainText(); Map<String, String> output(); } public class RedisConfig implements Config { //... need to implement three interfaces of Config update/outputIn.../output } public class KafkaConfig implements Config { //... need to implement three interfaces of Config update/outputIn.../output } public class MysqlConfig implements Config { //... need to implement three interfaces of Config update/outputIn.../output } public class ScheduledUpdater { //... omit other properties and methods 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() { //... } }
Obviously, the first design idea is more flexible, easy to expand and reuse. Because the responsibilities of Updater and Viewer are more single, single means good universality and reusability. For example, we now have a new requirement to develop a Metrics performance statistics module, and hope to display Metrics on the web page through SimpleHttpServer for easy viewing. At this time, although Metrics has nothing to do with RedisConfig, we can still make the Metrics class implement a very general Viewer interface and reuse the code implementation of 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", 2389); simpleHttpServer.addViewer("/config", redisConfig); simpleHttpServer.addViewer("/config", mySqlConfig); simpleHttpServer.addViewer("/metrics", apiMetrics); simpleHttpServer.addViewer("/metrics", dbMetrics); simpleHttpServer.run(); } }
Secondly, the second design idea does some useless work in code implementation. Because the Config interface contains two types of unrelated interfaces, one is update(), the other is output() and outputInPlainText(). Theoretically, KafkaConfig only needs to implement the update () interface, not the interface related to output (). Similarly, mysqlconfig only needs to implement the output () interface and the update () interface. However, the second design idea requires RedisConfig, KafkaConfig and MySQL Config to implement all interface functions (update, output and outputInPlainText) of Config at the same time. In addition, if we want to add a new interface to Config, all the implementation classes will have to be changed. On the contrary, if our interface granularity is relatively small, there are fewer classes involved in changes.
5.DRY principle
Don't Repeat Yourself means that duplicate code is not written during programming
public class UserService { private UserRepo userRepo;//Through dependency injection or IOC framework injection public User login(String email, String password) { boolean existed = userRepo.checkIfUserExisted(email, password); if (!existed) { // ... throw AuthenticationFailureException... } User user = userRepo.getUserByEmail(email); return user; } } public class UserRepo { public boolean checkIfUserExisted(String email, String password) { if (!EmailValidation.validate(email)) { // ... throw InvalidEmailException... } if (!PasswordValidation.validate(password)) { // ... throw InvalidPasswordException... } //...query db to check if email&password exists... } public User getUserByEmail(String email) { if (!EmailValidation.validate(email)) { // ... throw InvalidEmailException... } // ...query db to get user by email... } }