架构设计 - 如何解除依赖
架构设计中常会遇到依赖的问题,依赖如果设计不好,往往会导致紧耦合、难以测试、扩展以及维护升级问题,下面通过几个具体的例子来讨论具体采用什么样的方式解决依赖问题。
1. 示例1
public class Account{
private Date deactivatedAt;
private boolean active = true;
public void deactivate(){
this.active = false;
this.deactivatedAt = new Date();
// ...
}
public Date getDeactivatedAt() {
return deactivatedAt;
}
public boolean isActive() {
return active;
}
}
问题: 难以测试/扩展
解决办法:依赖注入
将所依赖对象注入待测类中:
1. 不在待测业务类中使用new构建
• 其他业务对象
• 第三方依赖对象
2. 不直接使用非常量全局变量、静态类、静态方法
3. 控制传入的对象行为 -> 可分离依赖,可测
重构步骤:
1.抽取Method Object
2.抽取成员变量量
3.构造函数传参
4.抽取接口
2. SOLID原则:
• Single Responsibility Principle 单一职责原则
• Open Closed Principle 开闭原则
• Liskov Substitution Principle 里里氏替换原则
• Interface Segregation Principle 接口隔离原则
• Dependency Inversion Principle 依赖反转原则
2.1 单一职责 (SRP):
一个类应该只为一个原因被改动
什么是职责? 职责是不同用户群对改变的需求
示例2:
职责混合导致紧耦合
• Employee的用户与所有功能耦合
• 如果一个功能改变,所有功能需要重新编译和部署
更好的设计:
2.2 开闭原则 (OCP):
任何软件实体应可扩展而不不可修改
如果不不想用触摸板……
示例3: 以告警服务为例:
public class AlertService {
private final int threshold;
public AlertService(int threshold) {
this.threshold = threshold;
}
void process(Event event) {
int result = doSomeWork(event);
if (result > threshold) {
sendMail(event);
}
}
告警服务 – 新增短信通知功能
public class AlertService {
private final int threshold;
public AlertService(int threshold) {
this.threshold = threshold;
}
void process(Event event, String actionType) {
int result = doSomeWork(event);
if (result > threshold) {
if ("email".equals(actionType)) {
sendMail(event);
} else if ("text".equals(actionType)) {
sendText(event);
}
}
}
告警服务 – 新增电话/微信/QQ通知功能:
public class AlertService {
private final int threshold;
public AlertService(int threshold) {
this.threshold = threshold;
}
void process(Event event, String actionType) {
int result = doSomeWork(event);
if (result > threshold) {
if ("email".equals(actionType)) {
return sendMail(event);
} else if ("text".equals(actionType)) {
return sendText(event);
} else if ("phoneCall".equals(actionType)) {
return call(event);
} else if ("weixin".equals(actionType)) {
return sendWeixinMessage(event);
} else if ("qq".equals(actionType)) {
return sendQQMessage(event);
}
}
问题在哪?
难理解,难维护,难扩展,每次加入新功能,这个类都需要修改。
电脑如何解决这种问题的?
更好的设计:
• AlertService 可扩展⽽而⽆无需修改
• 未来AlertAction的扩展对AlertService透明
2.3 依赖反转原则 (DIP):
高层逻辑不应依赖于底层实现细节
示例4:例如一个分层架构:
紧耦合导致改动困难:
电脑如何解决这种问题的?
更更好的设计:
• 全部依赖于接口
• 实现细节对调用方透明
• Controller/Service 可无修改复用
松耦合让改动简单:
3. 示例5:
public class LoanCalculator {
// …
public void calculate(int loan, int months){
if(loan <= 0 || months <= 0){
JOptionPane.showMessageDialog(null, "Kwota pozyczki oraz liczba miesiecy musi byc wieksza od zera.", "Nieprawidlowe dane",JOptionPane.ERROR_MESSAGE);
return;
}
int rate = (int)Math.ceil(loan/months);
int[] rates = new int[months + 1];
int[] remainingLoan = new int[months + 1];
remainingLoan[0] = loan;
rates[0] = 0;
for(int i=1; i<months; i++){
rates[i] = rate + (int)Math.ceil(remainingLoan[i-1] * percent / 12);
remainingLoan[i] = remainingLoan[i-1] - rate;
}
rates[months] = remainingLoan[months-1];
remainingLoan[months] = 0;
DefaultCategoryDataset dataset = new DefaultCategoryDataset();
for (int i=0; i <= months; i++) {
Integer m = Integer.valueOf(i);
dataset.setValue(remainingLoan[i], "Rata malejąca", m);
}
JFreeChart chart = ChartFactory.createBarChart3D("Kredyt", "Miesiące", "Pozostało do spłaty", dataset, PlotOrientation.VERTICAL, true, true,false);
ChartFrame frame=new ChartFrame("Kredyt",chart);
frame.setVisible(true);
frame.setSize(800, 600);
}
}
问题:
- 难以测试/扩展
- 方法太长
- 可读性低
- 违反原则 SRP/OCP/DIP
解决办法:将画图和贷款计算分离
重构步骤:
1.选择一:组合
- 1.抽取Method
- 2.Delegate
- 3.抽取Constructor parameter
- 4.抽取Interface
2.选择二:继承
- 1.抽取方法
- 2.抽取Superclass
哪一种更好?
组合 > 继承 (违反SRP)
---------OVER-----------