借助这个页面上的各种帖子和cmet,已经产生了一个解决方案,我觉得这对我的场景是正确的。
下面显示了解决方案的迭代更改以满足 SOLID 原则。
要求
为了生成 Web 服务的响应,键 + 对象对被添加到响应对象中。有许多不同的键 + 对象对需要添加,每个可能需要独特的处理将数据从源转换为响应中所需的格式。
由此可以清楚地看出,虽然不同的键/值对在将源数据转换为目标响应对象时可能具有不同的处理要求,但它们都有一个共同的目标,即向响应对象添加对象。
因此,在方案迭代1中产生了如下界面:
解决方案迭代 1
ResponseObjectProvider<T, S> {
void addObject(T targetObject, S sourceObject, String targetKey);
}
任何需要向响应中添加对象的开发人员现在都可以使用符合其要求的现有实现来执行此操作,或者在给定新场景的情况下添加新实现
这很好,因为我们有一个通用接口,作为添加响应对象的这种常见做法的合同
但是,一种情况要求目标对象应取自源对象,并给出特定的键“标识符”。
这里有几个选项,第一个是添加一个已有接口的实现,如下:
public class GetIdentifierResponseObjectProvider<T extends Map, S extends Map> implements ResponseObjectProvider<T, S> {
public void addObject(final T targetObject, final S sourceObject, final String targetKey) {
targetObject.put(targetKey, sourceObject.get("identifier"));
}
}
这可行,但是其他源对象键(“startDate”、“endDate”等...)可能需要此方案,因此应使此实现更通用,以便在此方案中重用。
另外,其他实现可能需要更多的上下文信息来执行 addObject 操作......所以应该添加一个新的泛型类型来满足这一点
解决方案迭代 2
ResponseObjectProvider<T, S, U> {
void addObject(T targetObject, S sourceObject, String targetKey);
void setParams(U params);
U getParams();
}
该接口同时满足两种使用场景;需要额外参数来执行 addObject 操作的实现和不需要的实现
但是,考虑到后者的使用场景,不需要额外参数的实现将打破 SOLID 接口隔离原则,因为这些实现将覆盖 getParams 和 setParams 方法但不实现它们。例如:
public class GetObjectBySourceKeyResponseObjectProvider<T extends Map, S extends Map, U extends String> implements ResponseObjectProvider<T, S, U> {
public void addObject(final T targetObject, final S sourceObject, final String targetKey) {
targetObject.put(targetKey, sourceObject.get(U));
}
public void setParams(U params) {
//unimplemented method
}
U getParams() {
//unimplemented method
}
}
解决方案迭代 3
为了解决接口隔离问题,getParams 和 setParams 接口方法被移到了一个新的接口中:
public interface ParametersProvider<T> {
void setParams(T params);
T getParams();
}
需要参数的实现现在可以实现ParametersProvider接口:
public class GetObjectBySourceKeyResponseObjectProvider<T extends Map, S extends Map, U extends String> implements ResponseObjectProvider<T, S>, ParametersProvider<U>
private String params;
public void setParams(U params) {
this.params = params;
}
public U getParams() {
return this.params;
}
public void addObject(final T targetObject, final S sourceObject, final String targetKey) {
targetObject.put(targetKey, sourceObject.get(params));
}
}
这解决了接口隔离问题,但又导致了两个问题......如果调用客户端想要对接口进行编程,即:
ResponseObjectProvider responseObjectProvider = new GetObjectBySourceKeyResponseObjectProvider<>();
那么 addObject 方法将可用于实例,但不能使用 ParametersProvider 接口的 getParams 和 setParams 方法...调用这些方法需要强制转换,并且为了安全起见,还应该执行 instanceof 检查:
if(responseObjectProvider instanceof ParametersProvider) {
((ParametersProvider)responseObjectProvider).setParams("identifier");
}
这不仅是不可取的,它还破坏了 Liskov 替换原则 - “如果 S 是 T 的子类型,那么程序中 T 类型的对象可以被 S 类型的对象替换,而不会改变任何可取的该程序的属性"
即如果我们用不实现 ParametersProvider 的实现替换了同样实现 ParametersProvider 的 ResponseObjectProvider 实现,那么这可能会改变程序的一些理想属性......此外,客户端需要知道正在使用哪个实现调用正确的方法
另一个问题是调用客户端的用法。如果调用客户端想要使用实现这两个接口的实例多次执行 addObject,则需要在 addObject 之前调用 setParams 方法...如果调用时不小心,这可能会导致可避免的错误。
解决方案迭代 4 - 最终解决方案
解决方案迭代 3 生成的接口解决了所有当前已知的使用要求,泛型为使用不同类型的实现提供了一些灵活性。但是,此解决方案打破了 Liskov 替换原则,并且调用客户端的 setParams 用法并不明显
解决方案是有两个独立的接口,ParameterisedResponseObjectProvider 和 ResponseObjectProvider。
这允许客户端对接口进行编程,并根据添加到响应中的对象是否需要额外的参数来选择适当的接口
新接口最初是作为 ResponseObjectProvider 的扩展实现的:
public interface ParameterisedResponseObjectProvider<T,S,U> extends ResponseObjectProvider<T, S> {
void setParams(U params);
U getParams();
}
但是,这仍然存在使用问题,调用客户端首先需要在调用 addObject 之前调用 setParams,这也会降低代码的可读性。
所以最终的解决方案有两个独立的接口,定义如下:
public interface ResponseObjectProvider<T, S> {
void addObject(T targetObject, S sourceObject, String targetKey);
}
public interface ParameterisedResponseObjectProvider<T,S,U> {
void addObject(T targetObject, S sourceObject, String targetKey, U params);
}
该解决方案解决了对接口隔离和 Liskov 替换原则的违反,同时也改进了调用客户端的使用,提高了代码的可读性。
这确实意味着客户需要了解不同的接口,但由于合同不同,这似乎是一个合理的决定,尤其是在考虑到解决方案已避免的所有问题时。