相关阅读:
JAVA编程思想(一)通过依赖注入增加扩展性
JAVA编程思想(二)如何面向接口编程
JAVA编程思想(三)去掉别扭的if,自注册策略模式优雅满足开闭原则
JAVA编程思想(四)Builder模式经典范式以及和工厂模式如何选?
Java编程思想(五)事件通知模式解耦过程
Java编程思想(六)事件通知模式解耦过程
JAVA基础(一)简单、透彻理解内部类和静态内部类
JAVA基础(二)内存优化-使用Java引用做缓存
JAVA基础(三)ClassLoader实现热加载
JAVA基础(四)枚举(enum)和常量定义,工厂类使用对比
JAVA基础(五)函数式接口-复用,解耦之利刃
HikariPool源码(二)设计思想借鉴
【极客源码】JetCache源码(一)开篇
【极客源码】JetCache源码(二)顶层视图
人在职场(一)IT大厂生存法则
1. 继承(extends)和实现(implements)的目的
实现(implements)的目的是为了面向接口编程。
继承(extends)的目的是为了获得能力。
第1点组合做不到,第2点组合和继承都可以做到,那么何时使用组合,何时使用继承呢?
2. 继承和实现的使用场景
在决定何时使用组合,何时使用继承前,先看下使用继承和实现的各种场景。
2.1. 只有实现类,子类方法签名和接口一致
子类实现接口,子类的方法签名和接口完全一致,不多也不少,此场景不涉及是使用继承还是组合。
2.2. 子类方法签名和接口不一致,可遵循面向接口编程
子类实现了接口的一个方法,增加了两个不在接口中的方法,这种场景的一个例子是实现java的Closeable,使得资源可以自动释放。
例子:
import java.io.Closeable;
/**
* @ClassName CloseDemo
* @Description
* @Author 铿然一叶
* @Date 2020/6/13 19:31
* @Version 1.0
* 掘金:https://juejin.im/user/3544481219739870
**/
public class CloseDemo implements Closeable {
public static void main(String[] args) {
// 实现了Closeable接口,在try中初始化,try结束后会自动执行close方法
try (CloseDemo closeDemo = new CloseDemo()) {
closeDemo.doSomeThing();
}
}
public CloseDemo() {
// 模拟获取资源
System.out.println("get resource.");
}
@Override
public void close() {
// 模拟释放资源
System.out.println("release resource.");
}
public void doSomeThing() {
System.out.println("do some thing.");
}
}
输出:
get resource.
do some thing.
release resource.
此场景使用了一个标识接口,在特定的情况下会自动执行该接口的方法,此时面向接口编程原则依然有效,这种场景下也不涉及使用组合还是继承。
2.3. 有抽象类,子类方法签名和接口一致
接口和子类之间有一个抽象类,抽象类实现了公用的方法operation1,子类继承抽象类,自动获得此能力。
抽象类增加了一个方法operation4,此方法不对外暴露,只给子类使用,对外暴露的接口没有变化。
最终两个子类的方法签名和接口一致,方法不多也不少,遵循面向接口编程原则。
此场景适合使用继承。
2.4. 子类方法签名和抽象类名不一致,无法遵循面向接口编程原则
子类的方法签名和抽象类不一致,此时无法优雅的面向接口编程,当接口参数为抽象类时,需在方法内部判断具体是那个类型,根据不同的类型来处理,这个识别类型的过程是隐式的,无法根据暴露的方法知道要做这个判断,在这种场景下不适合使用继承。
2.4.1. 例子:由于继承导致的类型判断和强转
/**
* @ClassName DefaultCacheMonitor
* @Description 这是一个缓存事件监控类,用于统计命中率
* @Author 铿然一叶
* @Date 2020/6/10 23:44
* @Version 1.0
* 掘金:https://juejin.im/user/3544481219739870
**/
public class DefaultCacheMonitor {
// 方法参数面向接口,但是内部要根据实例类型做强转,并且从方法签名上看不出来要做强转,只有写代码的人自己知道,使用继承的反例。
public synchronized void afterOperation(CacheEvent event) {
if (event instanceof CacheGetEvent) {
CacheGetEvent e = (CacheGetEvent) event;
afterGet(e.getMillis(), e.getKey(), e.getResult());
} else if (event instanceof CachePutEvent) {
CachePutEvent e = (CachePutEvent) event;
afterPut(e.getMillis(), e.getKey(), e.getValue(), e.getResult());
} else if (event instanceof CacheLoadEvent) {
CacheLoadEvent e = (CacheLoadEvent) event;
afterLoad(e.getMillis(), e.getKey(), e.getLoadedValue(), e.isSuccess());
}
}
private void afterGet(long millis, Object key, CacheGetResult result) {
// do some thing
}
private void afterPut(long millis, Object key, Object value, CacheResult result) {
// do some thing
}
private void afterLoad(long millis, Object key, Object loadedValue, boolean success) {
// do some thing
}
}
2.4.2. 将不合适的继承关系优化为使用组合
接着上例,我们将继承优化为组合:
优化过程:
1.将不同事件的差异部分抽象为DetailInfo,并通过泛型变量来记录不同的DetailInfo,将继承改为组合。
2.定义CacheEventType枚举类,用于显示区分不同的事件类型。
3.根据不同事件类型定义不同的Detail类。
这样对于不同事件类型的处理是显示的,不再需要做类型强转。示例代码如下:
2.4.2.1. CacheEvent.java
public class CacheEvent<T> {
private CacheEventType cacheEventType;
private Cache cache;
private T detailInfo;
public CacheEvent(Cache cache, CacheEventType cacheEventType, T detailInfo) {
this.cache = cache;
this.cacheEventType = cacheEventType;
this.detailInfo = detailInfo;
}
public Cache getCache() {
return cache;
}
public CacheEventType getCacheEventType() {
return cacheEventType;
}
public T getDetailInfo() {
return detailInfo;
}
}
2.4.2.2. CacheEventType.java
public enum CacheEventType {
GetEvent, LoadEvent, PutEvent;
}
2.4.2.3. GetEventDetail.java
public class GetEventDetail {
private long millis;
private Object key;
private CacheGetResult result;
public GetEventDetail(long millis, Object key, CacheGetResult result) {
this.millis = millis;
this.key = key;
this.result = result;
}
public long getMillis() {
return millis;
}
public Object getKey() {
return key;
}
public CacheGetResult getResult() {
return result;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("GetEventDetail {")
.append("millis=").append(millis)
.append(", key=").append(key)
.append(" }");
return builder.toString();
}
}
2.4.2.4. LoadEventDetail.java
public class LoadEventDetail {
private final long millis;
private final Object key;
private final Object loadedValue;
private final boolean success;
public LoadEventDetail(long millis, Object key, Object loadedValue, boolean success) {
this.millis = millis;
this.key = key;
this.loadedValue = loadedValue;
this.success = success;
}
public long getMillis() {
return millis;
}
public Object getKey() {
return key;
}
public Object getLoadedValue() {
return loadedValue;
}
public boolean isSuccess() {
return success;
}
}
2.4.2.5. PutEventDetail.java
public class PutEventDetail {
private long millis;
private Object key;
private Object value;
private CacheResult result;
public PutEventDetail(long millis, Object key, Object value, CacheResult result) {
this.millis = millis;
this.key = key;
this.value = value;
this.result = result;
}
public long getMillis() {
return millis;
}
public Object getKey() {
return key;
}
public CacheResult getResult() {
return result;
}
public Object getValue() {
return value;
}
}
2.4.2.6. DefaultCacheMonitor.java
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
public class DefaultCacheMonitor {
// 存储消费不同事件的消费者,避免使用别扭的if判断
private Map<CacheEventType, Consumer> consumerMap = new ConcurrentHashMap<>();
public DefaultCacheMonitor() {
registerConsumer();
}
public synchronized void afterOperation(CacheEvent event) {
// 根据事件类型获取对应的事件消费者
Consumer consumer = consumerMap.get(event.getCacheEventType());
if (consumer != null) {
consumer.accept(event.getDetailInfo());
}
}
// 根据事件类型注册不同的事件消费者,避免使用if
private void registerConsumer() {
// 函数接口利用泛型避免类示例强转
consumerMap.put(CacheEventType.GetEvent, new Consumer<GetEventDetail>() {
@Override
public void accept(GetEventDetail o) {
System.out.println("consume GetEventDetail");
System.out.println(o.toString());
}
});
consumerMap.put(CacheEventType.LoadEvent, new Consumer<LoadEventDetail>() {
@Override
public void accept(LoadEventDetail o) {
System.out.println("consume LoadEventDetail");
System.out.println(o.toString());
}
});
consumerMap.put(CacheEventType.PutEvent, new Consumer<PutEventDetail>() {
@Override
public void accept(PutEventDetail o) {
System.out.println("consume PutEventDetail");
System.out.println(o.toString());
}
});
}
}
2.4.2.7. EntryDemo.java
public class EntryDemo {
public static void main(String[] args) {
DefaultCacheMonitor monitor = new DefaultCacheMonitor();
CacheEvent<GetEventDetail> cacheEvent = new CacheEvent<>(new Cache(),
CacheEventType.GetEvent,
new GetEventDetail(1001, "apple", new CacheGetResult()));
monitor.afterOperation(cacheEvent);
}
}
demo输出结果:
consume GetEventDetail
GetEventDetail {millis=1001, key=apple }
3. 总结
至此,我们可以整理出哪些场景适合使用组合,哪些场景适合使用继承。
1. 使用继承时也要遵循面向接口编程原则,如果打破了此原则,就要考虑使用组合。
2. 继承通常出现在抽象类中,如果不是,优先使用组合。
3. 使用泛型、函数式接口,策略模式可以将不好的使用继承代码,优化为使用组合。
end.