设计原则与思想
设计原则与思想
经典的设计原则:SOLID,KISS,YAGNI,DRY,LOD等;
1.单一职责原则
单一职责:Single Responsibility Principle;缩写SRP,这个原则的英文是:A class or module should have a single responsibility.
1.1.如何判断类的职责是否单一?
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; // 详细地址
// ...省略其他属性和方法...
}
在一个社交产品中,我们用下面的UserInfo类来记录用户信息.代码路上,是否满足单一职责原则?两种观点如下:
- UserInfo包含的都是与用户相关的信息,所有属性和方法都隶属于用户这样一个业务模型.满足单一职责原则;
- 地址信息在UserInfo类中,所占的比重比较高,可以继续拆分独立的UserAddress类,拆分之后两个类的职责更加单一;
实际上,要从中做出选择,不能脱离具体的应用场景,在这个产品中,用户地址与其它信息一样,只是单纯作为展示,那UserInfo设计就是合理.但是,如果这个社交产品发展的比较好,之后又在产品中添加了电商模块,用户的地址还会用在电商物流中,我们建议拆分;
我们可以先写一个粗粒度的类,满足业务需求,随着业务的发展,我们就进行拆分成更细粒度的类,这就是持续重构;
1.2.类的职责是否设计越单一越好?
单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、低耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。
2.开闭原则
Open Closed Principle,OCP,英文描述是:software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification.
2.1.如何理解"对扩展开放,修改关闭"?
如下是一段API接口监控告警的代码.AlertRule存储告警规则,可以自由设置.Notification是告警通知类,支持邮件,短信,微信,手机等多种通知渠道.NotificationEmergencyLevel表示通知的紧急程度,包括SEVERE(严重),URGENCY(紧急),不同的紧急程度对应不同的发送渠道.
public class Alert {
private AlertRule rule;
private Notification notification;
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;
if (tps > rule.getMatchedRule(api).getMaxTps()) {
notification.notify(NotificationEmergencyLevel.URGENCY, "...");
}
if (errorCount > rule.getMatchedRule(api).getMaxErrorCount()) {
notification.notify(NotificationEmergencyLevel.SEVERE, "...");
}
}
}
如果我们要添加一个功能,每秒接口超时请求个数,超过某个预先设置的最大阈值,我们也要发出通知.如何改动代码?
- 第一处修改check()函数的入参,添加一个新的统计数据timeoutCount,表示超时接口请求数.
- 第二处是在check()函数中添加新的告警逻辑.
public class Alert {
// ...省略AlertRule/Notification属性和构造函数...
// 改动一:添加参数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, "...");
}
// 改动二:添加接口超时处理逻辑
long timeoutTps = timeoutCount / durationOfSeconds;
if (timeoutTps > rule.getMatchedRule(api).getMaxTimeoutTps()) {
notification.notify(NotificationEmergencyLevel.URGENCY, "...");
}
}
}
这种修改产生如下问题:一方面对接口进行修改,意味着调用这个接口的代码都要做修改,另一方面,修改了check()函数,相应的单元测试都需要修改.
我们重构一下Alert代码:
- 第一部分是将check()函数的多个入参封装成ApiStatInfo类;
- 第二部分是引入handler的概念,将if判断逻辑风扇在各个handler中.
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, "...");
}
}
}
重构后的Alert的使用
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);
}
}
在众多的设计原则,思想,模式中,最常用来提高代码扩展性的方法有:多态,依赖注入,基于接口而非实现编程,以及大部分的设计模式.
如下代码是通过Kafka来发送异步消息.对于这样一个功能的开发,我们要学会将其抽象成与具体消息队列(Kafka)无关的异步消息接口.所有上层系统都依赖这组抽象的接口编程,并且通过依赖注入的方式来调用.当我们要替换新的消息队列的时候,比如将Kafka替换成RocketMQ,可以很方便地拔掉老的消息队列实现,插入新的消息队列实现.
// 这一部分体现了抽象意识
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) {
//...
}
}
3.里式替换原则
Liskov Substitution Principle,缩写为 LSP
If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program.
如下代码,父类Transporter使用org.apache.http库中的HttpClient类来传输网络数据,子类SecurityTransporter集成父类Transporter,增加了额外的功能,支持传输appId和appToken安全认证信息.
public class Transporter {
private HttpClient httpClient;
public Transporter(HttpClient httpClient) {
this.httpClient = httpClient;
}
public Response sendRequest(Request request) {
// ...use httpClient to send request
}
}
public class SecurityTransporter extends Transporter {
private String appId;
private String appToken;
public SecurityTransporter(HttpClient httpClient, String appId, String appToken) {
super(httpClient);
this.appId = appId;
this.appToken = appToken;
}
@Override
public Response sendRequest(Request request) {
if (StringUtils.isNotBlank(appId) && StringUtils.isNotBlank(appToken)) {
request.addPayload("app-id", appId);
request.addPayload("app-token", appToken);
}
return super.sendRequest(request);
}
}
public class Demo {
public void demoFunction(Transporter transporter) {
Reuqest request = new Request();
//...省略设置request中数据值的代码...
Response response = transporter.sendRequest(request);
//...省略其他逻辑...
}
}
// 里式替换原则
Demo demo = new Demo();
demo.demofunction(new SecurityTransporter(/*省略参数*/););
上述代码,子类SecurityTransporter的设计完全符合里式替换原则,可以替换父类出现的任何位置.
我们改造下代码:
// 改造前:
public class SecurityTransporter extends Transporter {
//...省略其他代码..
@Override
public Response sendRequest(Request request) {
if (StringUtils.isNotBlank(appId) && StringUtils.isNotBlank(appToken)) {
request.addPayload("app-id", appId);
request.addPayload("app-token", appToken);
}
return super.sendRequest(request);
}
}
// 改造后:
public class SecurityTransporter extends Transporter {
//...省略其他代码..
@Override
public Response sendRequest(Request request) {
if (StringUtils.isBlank(appId) || StringUtils.isBlank(appToken)) {
throw new NoAuthorizationRuntimeException(...);
}
request.addPayload("app-id", appId);
request.addPayload("app-token", appToken);
return super.sendRequest(request);
}
}
在改造之后的代码中,如果传递进 demoFunction() 函数的是父类 Transporter 对象,那 demoFunction() 函数并不会有异常抛出,但如果传递给 demoFunction() 函数的是子类 SecurityTransporter 对象,那 demoFunction() 有可能会有异常抛出。尽管代码中抛出的是运行时异常(Runtime Exception),我们可以不在代码中显式地捕获处理,但子类替换父类传递进 demoFunction 函数之后,整个程序的逻辑行为有了改变。虽然改造之后的代码仍然可以通过 Java 的多态语法,动态地用子类 SecurityTransporter 来替换父类 Transporter,也并不会导致程序编译或者运行报错。但是,从设计思路上来讲,SecurityTransporter 的设计是不符合里式替换原则的。
虽然从定义描述和代码实现上来看,多态和里式替换有点类似,但它们关注的角度是不一样的。多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。而里式替换是一种设计原则,是用来指导继承关系中子类该如何设计的,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。
4.接口隔离原则
Interface Segregation Principle,缩写为ISP.
Clients should not be forced to depend upon interfaces that they do not use.客户端不应该被强迫依赖它不需要的接口,这里的客户端,可以理解为接口的调用者或使用者.
我们可以把接口理解为:
- 一组API接口集合;
- 单个API接口或函数;
- OOP中的接口概念;
4.1.把"接口"理解为一组API接口集合
微服务用户系统提供了一组与用户相关的API给其他系统使用,比如:注册,登录,获取用户信息等.
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 {
//...
}
后台管理系统要实现删除用户的功能,希望用户系统提供一个删除用户的接口.我们可以在UserService中添加一个deleteUserByCellphone(),这个方法可以解决问题,但是隐藏了一些安全隐患.这就意味着所有使用带UserService的系统,都可以调用这个接口,不加限制被其他业务调用,就有可能导致误删;
当然,最好的解决方案是从架构设计层面,通过接口鉴权的方式来限制接口的调用.我们还可以从代码设计的层面,尽量避免接口被误用.我们参考接口隔离原则,调用者不应该强迫依赖它不需要的接口,将删除接口单独放到另外的接口中.
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 {
// ...省略实现代码...
}
在设计微服务或者类库接口的时候,如果部分接口只被部分调用者调用,那么我们就需要将这部分接口隔离出来,单独给对应的调用者使用,而不是强迫其他调用者也依赖这部分不会被用到的接口.
4.2.把"接口"理解为单个API接口或函数
4.3.把"接口"理解为OOP中的接口概念
假设项目中用到了3个外部系统:Redis,MySQL,Kafka,每个系统都对应一系列配置信息,为了在内存中存储这些配置信息,供项目中的其他模块使用,我们分别设计实现了RedisConfig,MySQLConfig,KafkaConfig;
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配置信息的热更新.我们设计实现了一个ScheduledUpdater类,以固定时间频率来调用RedisConfig,KafkaConfig的update()方法更新配置信息,具体代码实现如下:
public interface Updater {
void update();
}
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();
}
}
5.依赖反转原则
5.1.控制反转(IOC)
我们先暂时别把这个"IOC"与Spring框架的IOC联系在一起.
public class UserServiceTest {
public static boolean doTest() {
// ...
}
public static void main(String[] args) {//这部分逻辑可以放到框架中
if (doTest()) {
System.out.println("Test succeed.");
} else {
System.out.println("Test failed.");
}
}
}
在上面的代码中,所有的流程都由程序员控制,如果我们抽象出下面这样一个框架.
public abstract class TestCase {
public void run() {
if (doTest()) {
System.out.println("Test succeed.");
} else {
System.out.println("Test failed.");
}
}
public abstract boolean doTest();
}
public class JunitApplication {
private static final List<TestCase> testCases = new ArrayList<>();
public static void register(TestCase testCase) {
testCases.add(testCase);
}
public static final void main(String[] args) {
for (TestCase case: testCases) {
case.run();
}
}
把这个简化版本的测试框架引入到工程中,我们只需要在框架预留的扩展点,也就是TestCase类中的doTest()抽象函数中,填充具体的测试代码就可以实现之前的功能了.
public class UserServiceTest extends TestCase {
@Override
public boolean doTest() {
// ...
}
}
// 注册操作还可以通过配置的方式来实现,不需要程序员显示调用register()
JunitApplication.register(new UserServiceTest();
这里的控制指的是对程序执行流程的控制,而反转指的是在没有使用框架之前,程序员自己控制整个程序的执行.在使用框架之后,整个程序的执行流程可以通过框架来控制.流程控制权从程序员反转到了框架.
5.1.依赖注入(DI)
不通过new()的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数,函数参数等方式传递(注入)给类使用.
// 非依赖注入实现方式
public class Notification {
private MessageSender messageSender;
public Notification() {
this.messageSender = new MessageSender(); //此处有点像hardcode
}
public void sendMessage(String cellphone, String message) {
//...省略校验逻辑等...
this.messageSender.send(cellphone, message);
}
}
public class MessageSender {
public void send(String cellphone, String message) {
//....
}
}
// 使用Notification
Notification notification = new Notification();
// 依赖注入的实现方式
public class Notification {
private MessageSender messageSender;
// 通过构造函数将messageSender传递进来
public Notification(MessageSender messageSender) {
this.messageSender = messageSender;
}
public void sendMessage(String cellphone, String message) {
//...省略校验逻辑等...
this.messageSender.send(cellphone, message);
}
}
//使用Notification
MessageSender messageSender = new MessageSender();
Notification notification = new Notification(messageSender);
我们还可以把MessageSender定义成接口,基于接口而非实现编程,改造上面的代码
public class Notification {
private MessageSender messageSender;
public Notification(MessageSender messageSender) {
this.messageSender = messageSender;
}
public void sendMessage(String cellphone, String message) {
this.messageSender.send(cellphone, message);
}
}
public interface MessageSender {
void send(String cellphone, String message);
}
// 短信发送类
public class SmsSender implements MessageSender {
@Override
public void send(String cellphone, String message) {
//....
}
}
// 站内信发送类
public class InboxSender implements MessageSender {
@Override
public void send(String cellphone, String message) {
//....
}
}
//使用Notification
MessageSender messageSender = new SmsSender();
Notification notification = new Notification(messageSender);
5.3.依赖注入框架(DI Framework)
Spring框架的控制反转主要通过依赖注入来实现;
5.4.依赖反转原则(DIP)
高层模块不要依赖低层模块.高层模块和低层模块应该通过抽象来相互依赖.除此之外,抽象不要依赖具体实现细节,具体实现细节依赖抽象;
Tomcat 是运行 Java Web 应用程序的容器。我们编写的 Web 应用程序代码只需要部署在 Tomcat 容器下,便可以被 Tomcat 容器调用执行。按照之前的划分原则,Tomcat 就是高层模块,我们编写的 Web 应用程序代码就是低层模块。Tomcat 和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个“抽象”,也就是 Servlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节,而 Tomcat 容器和应用程序依赖 Servlet 规范.
本文深入探讨了软件设计中的五大核心原则:单一职责原则、开闭原则、里式替换原则、接口隔离原则及依赖反转原则,并通过具体示例阐述了各原则的实际应用。
2104

被折叠的 条评论
为什么被折叠?



