title: 从依赖实现到依赖行为
date: 2016-10-22 18:59:11
tags:
- Java
- 设计模式
- Functional
categories: 设计模式
背景
一个报表程序需要初始化报表参数。具体的参数分为三种类型,都实现了Parameter
类型,具体的初始化过程由子类实现。
对报表参数初始化的过程,从对具体实现过程的依赖,依次经历对接口、对数据结构,最终对行为依赖,不断抽象依赖,实现更大程度的复用。
对实现的依赖
public class ParameterCollector {
public void fillParameters(ServletHttpRequest request,
ParameterGraph parameterGraph) {
for (Parameter para : parameterGraph.getParmaeters()) {
if (para instanceof SimpleParameter) {
SimpleParameter simplePara = (SimpleParameter) para;
String[] values = request.getParameterValues(para.getName());
simplePara.setValue(values);
} else {
if (para instanceof ItemParameter) {
ItemParameter itemPara = (ItemParameter) para;
for (Item item : itemPara.getItems()) {
String[] values = request.getParameterValues(item.getName());
item.setValues(values);
}
} else {
TableParameter tablePara = (TableParameter) para;
String[] rows =
request.getParameterValues(tablePara.getRowName());
String[] columns =
request.getParameterValues(tablePara.getColumnName());
String[] dataCells =
request.getParameterValues(tablePara.getDataCellName());
int columnSize = columns.length;
for (int i = 0; i < rows.length; i++) {
for (int j = 0; j < columns.length; j++) {
TableParameterElement element = new TableParameterElement();
element.setRow(rows[i]);
element.setColumn(columns[j]);
element.setDataCell(dataCells[columnSize * i + j]);
tablePara.addElement(element);
}
}
}
}
}
}
}
坏代码的味道:
- 多层次的循环、IF嵌套,如果要在这个层次上加一个期限,那就是大于2。
-
IF/SWITCH instanceof Class
式的样板代码
对接口的依赖
多态替换分支语句,重构:
public class ParameterCollector {
public void fillParameters(ServletHttpRequest request,
ParameterGraph parameterGraph) {
for (Parameter para : parameterGraph.getParmaeters()) {
para.fillParamter(request);
}
}
}
对数据结构的依赖
上面的重构使用多态极大简化了ParameterCollector
,同时也让不同实现的Parameter
更加容易维护。但是在Parameter的方法中传递了request,这使得Parameter类所在的包也必须依赖ServletHttpRequest。这种包之间不必要的耦合可以使用Map保存需要的键值映射关系,弱化这种依赖关系。
进一步重构:
public class ParameterCollector {
public void fillParameters(ServletHttpRequest request,
ParameterGraph parameterGraph) {
Map parmaeters = getParameterMap();
for (Parameter para : parameterGraph.getParmaeters()) {
para.fillParamter(parmaeters);
}
}
}
进一步重构后的代码虽然避免了对ServletHttpRequest的依赖,但是无法避免的对Map进行了依赖。
对类似Map这样具体的数据结构依赖,让人无法清晰知道方法参数含义并不是好的编码风格。
对行为的依赖
在回头看最初的代码,与Servlet API强耦合的是这样一句request.getParameterValues(para.getName())
依赖的到底是什么?站在函数式编程的角度,依赖的其实是一种行为:
string -> request.getParameterValues(string)
在Java语法中为了传递一个方法,必须让方法附属在一个接口上,像这样:
public interface ParamterRequest {
String[] getParameterValues(String string);
}
为了让ServletHttpRequest与ParamterRequest相关联,必须适配器模式,对象的适配器实现像下面这样:
private class ParameterRequestAdapter implements ParamterRequest {
ServletHttpRequest servletHttpRequest;
public ParameterRequestAdapter(ServletHttpRequest servletHttpRequest) {
this.servletHttpRequest = servletHttpRequest;
}
@Override
public String[] getParameterValues(String string) {
return servletHttpRequest.getParameterValues(string);
}
}
使用上面的适配器再进一步重构:
public void fillParameters(ServletHttpRequest request, ParameterGraph parameterGraph) {
for (Parameter para : parameterGraph.getParmaeters()) {
para.fillParamter(new ParameterRequestAdapter(request));
}
}
我们还可以使用匿名内部类:
public void fillParameters(ServletHttpRequest request, ParameterGraph parameterGraph) {
for (Parameter para : parameterGraph.getParmaeters()) {
para.fillParamter(new ParamterRequest() {
@Override
public String[] getParameterValues(String string) {
return request.getParameterValues(string);
}
});
}
}
在Java8我们还可以这样写:
public void fillParameters(ServletHttpRequest request, ParameterGraph parameterGraph) {
for (Parameter para : parameterGraph.getParmaeters()) {
para.fillParamter(string -> request.getParameterValues(string));
}
}
甚至还可以这样:
public void fillParameters(ServletHttpRequest request, ParameterGraph parameterGraph) {
for (Parameter para : parameterGraph.getParmaeters()) {
para.fillParamter(request::getParameterValues);
}
}
回头看一眼原有大篇幅的方法,再看看多次重构过的代码。
最明显的感受fillParameters
方法比以前薄了:
不相关的职责从方法中抽取出来放到单独的类维护了(职责单一)
当有更多参数类型需要添加时fillParameters
不需要做任何的修改,只要添加对应类型就好(开闭原则)
之前对实现类的依赖现在变成了对接口的依赖(依赖倒置)
重构过程中Parameter
从对request
的依赖变为对数据结构(Map)的依赖,避免了Parameter所在模块与Servlet API
之间的耦合。
从对数据结构的依赖最后变成了对接口(行为)的依赖,抽象程度进一步提高,可维护性也更好。
面向对象与函数式编程
面向对象强调对事物的抽象,强调名词
函数式编程强调对行为的抽象,强调动词
例如:
public class People {
private List<Person> persons = new ArrayList<Person>();
public List<Person> findByName(String name) {
List<Person> result = new ArrayList<Person>();
for (Person person : persons) {
if (person.getName().equals(name)) {
result.add(person);
}
}
return result;
}
}
例如我们有一个方法findByName,以后可能还需要更多方法例如findByAge,findByGender,findChildren等等方法,所有的方法其实都只有查找的过滤条件不同,可以抽象出函数式的写法:
public List<Person> findPersons(Predicate<Person> predicate, Person people) {
List<Person> result = new ArrayList<>();
for (Person person : persons) {
if (predicate.test(people)) {
result.add(person);
}
}
return result;
}
Java8可以简写成:
public List<Person> findPersons(Predicate<Person> predicate, Person people) {
return persons.stream().filter(predicate).collect(Collectors.toList());
}