Java编程思想(七)使用组合和继承的场景

2,894 阅读6分钟

banner窄.png

铿然架构  |  作者  /  铿然一叶 这是铿然架构的第 79 篇原创文章

相关阅读:

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.


<--阅过留痕,左边点赞!