从实际场景来看设计模式2:由自定义类加载器到模板方法模式及桥接模式

问题摘要

问题一自定义类加载器怎样做到只需要覆写findClass方法即可将自身与JVM整体类加载逻辑结合在一起?
问题二:当一个类中组件有多种实现方式又可以通过怎样的方式梳理子类实现从而避免子类出现”爆炸“呢?

ClassLoader类加载器与自定义类加载器

由Java源文件编译出来的字节码class文件是怎样加载到JVM内存中参与运行的呢?答案是文件由ClassLoader类加载器load到内存中经过加载->链接->初始化等步骤转化为Java内存中的类对象从而参与执行的。

我们知道Java的类加载器是有一套等级机制的,通过朔源委托加载机制(也叫双亲委派机制)最终决定由哪个层级的类加载器来进行加载。层级由等级从高到低依次为:BootStrap(引导类加载器)->Extension(拓展类加载器)->System(系统类加载器)->Custom(用户自定义类加载器)

当运行中需要用到一个类而该类未经过加载时,系统将通过该类的类加载器(getClassLoader方法)去加载该类,类加载器不会立即去加载该类,如果它不是最顶层的BootStrap类加载器则需要委托它的上一级类加载器去加载,依次向上委托,直到加载请求到达最顶层的BootStrap类加载器,然后BootStrap尝试进行加载该类,如果加载不到,则交给下一级Extension进行加载,依此类推,如果整条链上的类加载器都未能成功加载,则抛出ClassNotFoudException。

也就是说:如果一个类被加载到JVM,那么加载它的类加载器一定是能加载它的类加载器中等级最高的那个,有什么好处:第一,避免重复加载,父类能加载的,子类就不用再加载一次了;第二,安全因素,如果java.lang.Object可以随意被用户自定义加载器加载的话,那么系统就可以被随意改造了。

什么时候会用到自定义类加载器呢?比如说Tomcat是拥有自己的类加载器的,这样当一个Tomcat中部署了多个项目,而项目中对于同一个类库引用的版本又有所不同,那么不将各自的依赖隔离开是会出现运行异常的,还有一定就是可以很方便的进行热部署:当源码改动时,使用自定义类加载器进行类替换,实现了不重新部署即可对源码热更新。

如何自定义类加载器呢? Java中实现自定义类加载器是很方便的,具体的实现方式为:继承ClassLoader类,覆写findClass方法写好具体的类加载逻辑即可。那么这里引申出摘要中的问题:为什么只需要关注自身的类加载逻辑即可融入到整个类加载体系当中的呢?

答案是:整个ClassLoader体系应用了模板方法模式:

定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

设计模式1:模板方法 Template Method

模板方法设计模式可以理解为:由父类定义组件的流程骨架,将流程中某些具体的实现部分交给子类来实现,与策略模式有所不同,父类中的主干流程是final方法,并不是所有方法都交给子类去覆写实现的。
我们来看一个具体的场景:
设计一个配置加载类,它可以从数据源中加载某个配置的属性,数据源可以是mysql、可以是一个properties文件等等

public abstract class AbsSetting {
   // final方法 不允许覆写
    public final String getSetting(String key) {
        String value = readFromDatabase(key);
        return value;
    }

	// readFromDatabase 因为不确定数据源所以具体实现交给继承Setting的子类
	protected abstract String readFromDatabase(String key);
}

因为从数据库或者通过IO来加载数据是很耗时的操作,所以我们引入下缓存,当然,缓存也不是确定的,可以是Redis、可以是Memcache、或者是HashMap等

public abstract class AbsSetting {
   // final方法 不允许覆写
    public final String getSetting(String key) {
       // 先从缓存读取:
        String value = lookupCache(key);
        if (value == null) {
            // 在缓存中未找到,从数据库读取:
            value = readFromDatabase(key);
            System.out.println("[DEBUG] load from db: " + key + " = " + value);
            // 放入缓存:
            putIntoCache(key, value);
        } else {
            System.out.println("[DEBUG] load from cache: " + key + " = " + value);
        }
        return value;
    }

	// readFromDatabase 因为不确定数据源所以具体实现交给继承Setting的子类
	protected abstract String readFromDatabase(String key);
}

我们发现里边的lookupCache(key)和putIntoCache(key, value)两个方法还没声明的,为了编译通过,声明成抽象的吧,交给子类实现。

// 从缓存读取
protected abstract String lookupCache(String key);
// 添加key-value到缓存
protected abstract void putIntoCache(String key, String value);

好了,骨架我们已经写好了,现在我们写子类,数据源用properties文件吧,缓存用hashmap,这样实现是最简单的,java原生类库就支持
只需要继承Setting类,覆写三个读写相关的方法即可

public class LocalSetting extends AbsSetting {
    private Map<String, String> cache = new HashMap<>();
	@Override
    protected String lookupCache(String key) {
        return cache.get(key);
    }
	@Override
    protected void putIntoCache(String key, String value) {
        cache.put(key, value);
    }
    @Override
    protected String readFromDatabase(String key) throws IOException {
        String f = "setting.properties";
        Properties props = new Properties();
        props.load(new java.io.FileInputStream(f));
        return props.getProperty(key);
    }
}

测试代码

class Test {
    public static void main(String[] args) throws IOException {
        AbsSetting setting = new LocalSetting ();
        setting.getResource("testKey");
    }
}       

还有改进空间吗

从模板方法的实现中,我们看到了顶层设计中如何避免核心逻辑被修改(final修饰)以及如何让子类更简单的去实现需要实现的逻辑。

小结:模板方法是一种高层定义骨架,底层实现细节的设计模式,适用于流程固定,但某些步骤不确定或可替换的情况。

此时我们应该发现上述实现中一个新的问题:如果数据源有10多种,缓存有10多种,那么学过数学排列组合的你一定想到,如果仅使用继承去实现所有数据源与缓存的自由组合,需要定义10 * 10种子类,那再继续扩充呢,子类就轻易的“爆炸”了

这是一个典型的问题,为了优雅的适应这种变化,可以考虑一下桥接设计模式

设计模式2:桥接模式 Bridge

将抽象部分与它的实现部分相分离,使它们都可以独立的变化

咋一看挺玄乎,不过我们先看下面这段话:

持有高层接口不但代码更灵活,而且把各种接口组合起来也更容易。一旦持有某个具体的子类类型,要想做一些改动就非常困难。

好像还是玄乎,,,其实简单来说,上一小结留给我们的问题是:一个子类只能选数据源中的一种与缓存中的一种来实现,这样持有具体实现的强耦合结构,不利于拓展,想要其他组合只能另起炉灶–重新再定义一个。

好的,持有抽象是吧,那缓存用抽象的,因为缓存主要设计两个主要功能:缓存键值的存和取,这样我们可以定义一个接口(抽象类和接口如何选择?形容词用接口,名词用抽象类。接口代表实现类公有的能力,抽象类是子类的模板,与普通类的区别仅仅在于有没有抽象方法。

/**
 * 定义缓存的抽象接口 为了将实现延迟到客户端
 */
interface CacheSource {
    /**
     * 设置缓存值
     *
     * @param key
     * @param value
     * @return
     */
    String setKeyValue(String key, String value);

    /**
     * 获取缓存值
     *
     * @param key
     * @return
     */
    String getValue(String key);
}

好了,缓存抽象出来了,我们再AbsSetting中持有它的类型

public abstract class AbsSetting {

	// 这里就是持有抽象接口了  可以灵活切换实现类
    private CacheSource cacheSource;

    public Setting2(CacheSource cacheSource) {
        this.cacheSource = cacheSource;
    }
   // 下边的代码先省略 与模板方法中的AbsSetting相同,后文会给出完整实例代码
}

其实现在我们已经写好了桥接模式的一部分,现在我们给出一个具体的缓存实现类型吧,简单起见就用HashMap。

/**
 * 缓存实现1-使用HashMap作为缓存池
 */
class LocalCache implements CacheSource {
    private Map<String, String> cache = new HashMap<>(1024);

    public LocalCache() {
        super();
    }

    @Override
    public String setKeyValue(String key, String value) {
        return cache.put(key, value);
    }

    @Override
    public String getValue(String key) {
        return cache.get(key);
    }
}

然后我们给出一个AbsSetting的子类,Mysql作为数据源,这样一个数据源中的缓存可以通过实现类的切换来避免重复定义子类,我们的10 * 10自由组合已经变成 10(10种数据源) + 10(10种缓存实现)了,不过不着急写代码

为了再一次拥抱变化,通常我们将数据源再次进行抽象,即AbsSetting与 XXXSetting之间再加一层AbsDataBase ,在这个中间层的抽象类中,我们可以对之后所有数据源的共有改变定义到次层,再一次降低了耦合的概率

当然,XXXSetting也不再直接继承AbsSetting了,而是继承抽象公共层AbsDataBase

/**
 * 数据源之上的抽象类 灵活拓展其他共用方法  比如获取下数据源类型、强制关闭数据源连接、实现一个连接池等操作
 * 所有具体数据源实例继承此类
 */
abstract class AbsDataBase extends Setting2 {
    private CacheSource cacheSource;

    public AbsDataBase(CacheSource cacheSource) {
        super(cacheSource);
        this.cacheSource = cacheSource;
    }

    @Override
    protected String putIntoCache(String key, String value) {
        return cacheSource.setKeyValue(key, value);
    }

    @Override
    protected String lookupCache(String key) {
        return cacheSource.getValue(key);
    }
}

这样,我们的数据源子类就可以这样写了

/**
 * 具体数据源实现类-mysql数据源
 * 之后其他任意数据源只需要继承AbsDataBase,覆写readFromDatabase即可
 */
class MysqlSetting extends AbsDataBase {

    public MysqlSetting(CacheSource cacheSource) {
        super(cacheSource);
    }

    @Override
    protected String readFromDatabase(String key) throws IOException {
        // 这里写从数据库获取的逻辑即可
        return "find in db";
    }
}

基本上,桥接模式已经写进去了,之后数据源的增加,只需要继承AbsDataSource即可;缓存类型的增加,只需要实现CacheSource接口即可。数据源与缓存实现在两个维度上独立的变化,相互之间无任何影响,不会对原有代码进行改动

重温里氏替换原则

良好的代码设计应该具备里氏替换原则:对拓展开放,对修改关闭。

总结

  • 模板方法可以让父类与子类各司其职:父类负责骨架,子类填充实现。
  • 模板方法在java中有广泛应用:ClassLoader;在集合类中,AbstractList和AbstractQueuedSynchronizer都定义了很多通用操作,子类只需要实现某些必要方法。
  • 桥接模式通过持有组件的高层抽象接口,使组件间独立的进行变化,很好的解决了子类爆炸问题。
  • 通过学习设计模式,可以很好的了解面向对象的封装、继承与多态,通过合理的运用,将臃肿的代码结构变成可维护、具有良好拓展性的优雅的代码。

附模板方法模式与桥接模式实例的完整代码

因为是先写好的代码,而后作总结的,所以代码中类和方法的命名会有出入

模板方法源码

package com.designparttern.template;

/**
 * 设计模式-行为型模式-模板方法
 * 模板方法的核心思想是:父类定义骨架,子类实现某些细节。
 * java中有许多应用  例如类加载器:当用户需要自定义加载器时,只需要继承ClassLoader类重写findClass方法 实现自定义的类加载策略即可
 */

import java.io.*;
import java.util.HashMap;
import java.util.Map;

/**
 * 获取配置抽象类
 */
public abstract class Setting {

    /**
     * 主要对外提供的方法  通过key来获取配置value
     * 代码逻辑为先从缓存中获取  如果获取不到则从database中获取 至于使用什么缓存  使用什么database 暂时未指定  提供需子类实现的抽象方法
     * 子类覆写时 需要考虑缓存操作get/set 及 数据库操作 get 的具体实现
     * getResource定义为final方法  不允许子类覆写修改核心业务逻辑
     *
     * @param key
     * @return
     */
    public final String getResource(String key) throws IOException {
        String val = lookupCache(key);
        if (val == null) {
            String value = readFromDatabase(key);
            System.out.println("[DEBUG] load from db: " + key + " = " + value);
            putIntoCache(key, value);
            return value;
        }
        System.out.println("[DEBUG] load from cache: " + key + " = " + val);
        return val;
    }

    protected abstract String readFromDatabase(String key) throws IOException;

    protected abstract String putIntoCache(String key, String value);

    protected abstract String lookupCache(String key);
}

/**
 * 伪代码  仅作为示例
 * 子类1 使用Map作为缓存  使用mysql作为database
 */
class LocalCacheMysqlSetting extends Setting {

    private Map<String, String> cache = new HashMap<>(1024);

    @Override
    protected String readFromDatabase(String key) {
        // 假装我在mysql中通过sql获取了数据
        return "read for Mysql or null";
    }

    @Override
    protected String putIntoCache(String key, String value) {
        Object put = cache.put(key, value);
        // key or null
        return put.toString();
    }

    @Override
    protected String lookupCache(String key) {
        return cache.get(key);
    }
}

/**
 * 伪代码 仅作为示例
 * 子类2  使用redis做缓存  使用文件作为数据源 通过io流读取
 */
/*class RedisCacheFileSetting extends Setting {

    private RedisClient client = RedisClient.create("redis://localhost:6379");

    @Override
    protected String readFromDatabase(String key) throws IOException {
        String f = "setting.properties";
        Properties props = new Properties();
        props.load(new java.io.FileInputStream(f));
        return props.getProperty(key);
    }

    @Override
    protected String putIntoCache(String key, String value) {
        try (StatefulRedisConnection<String, String> connection = client.connect()) {
            RedisCommands<String, String> commands = connection.sync();
            return commands.get(key);
        }
    }

    @Override
    protected String lookupCache(String key) {
        try (StatefulRedisConnection<String, String> connection = client.connect()) {
            RedisCommands<String, String> commands = connection.sync();
            return commands.set(key, value);
        }
    }
}*/

class Test {
    public static void main(String[] args) throws IOException {
        Setting setting = new LocalCacheMysqlSetting();
        setting.getResource("testKey");

        /**
         * 思考一个问题  假设缓存有map redis memcache mongodb 等类型  数据源有mysql oracle 等类型  当缓存与数据源随意组合时  可产生m * n  8种不同组合得Setting实现
         * 如果有更多的缓存类型及数据源  那么就会带来子类爆炸问题
         * 想要在缓存和数据源两种维度各自拥有不同的拓展  那么可以使用另一种设计模式来实现--桥接设计模式
         */
    }
}

加入桥接模式的源码

package com.designparttern.template;

/**
 * 因为Setting的实现中 缓存及数据源都具有不同的类型 当我们想要得到任意组合的配置源时  需要为自由组合的缓存及数据源都提供一个实现类
 * 为了避免子类爆炸,本例Setting2尝试使用桥接模式来解耦配置源的实现
 */

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * 获取配置抽象类
 * 将核心逻辑getResource实现 具体与数据库和缓存的交互交由子类实现
 * 持有缓存实例的接口 对于任意缓存的添加和实现都只需要实现CacheSource接口即可 不再需要继续组合数据源创建子类
 * 数据源通过再次定义一个AbsDataBase抽象类  可以在其中定义其他公用的功能
 * 每一个数据源实例都只需要继承AbsDataBase,然后覆写readFromDatabase 从数据源获取配置即可,很好的实现了对拓展开放
 * 客户端选择具体数据源在其中传入相应的缓存实现,调用getResource即可
 */
public abstract class Setting2 {

    private CacheSource cacheSource;

    public Setting2(CacheSource cacheSource) {
        this.cacheSource = cacheSource;
    }

    /**
     * 主要对外提供的方法  通过key来获取配置value
     * 代码逻辑为先从缓存中获取  如果获取不到则从database中获取 至于使用什么缓存  使用什么database 暂时未指定  提供需子类实现的抽象方法
     * 子类覆写时 需要考虑缓存操作get/set 及 数据库操作 get 的具体实现
     * getResource定义为final方法  不允许子类覆写修改核心业务逻辑
     * @param key
     * @return
     */
    public final String getResource(String key) throws IOException {
        String val = lookupCache(key);
        if (val == null) {
            String value = readFromDatabase(key);
            System.out.println("[DEBUG] load from db: " + key + " = " + value);
            putIntoCache(key, value);
            return value;
        }
        System.out.println("[DEBUG] load from cache: " + key + " = " + val);
        return val;
    }

    protected abstract String readFromDatabase(String key) throws IOException;

    protected abstract String putIntoCache(String key, String value);

    protected abstract String lookupCache(String key);
}

/**
 * 数据源之上的抽象类 灵活拓展其他共用方法  比如获取下数据源类型、强制关闭数据源连接、实现一个连接池等操作
 * 所有具体数据源实例继承此类
 */
abstract class AbsDataBase extends Setting2 {
    private CacheSource cacheSource;

    public AbsDataBase(CacheSource cacheSource) {
        super(cacheSource);
        this.cacheSource = cacheSource;
    }

    @Override
    protected String putIntoCache(String key, String value) {
        return cacheSource.setKeyValue(key, value);
    }

    @Override
    protected String lookupCache(String key) {
        return cacheSource.getValue(key);
    }
}

/**
 * 具体数据源实现类-mysql数据源
 * 之后其他任意数据源只需要继承AbsDataBase,覆写readFromDatabase即可
 */
class MysqlSetting extends AbsDataBase {

    public MysqlSetting(CacheSource cacheSource) {
        super(cacheSource);
    }

    @Override
    protected String readFromDatabase(String key) throws IOException {
        // 这里写从数据库获取的逻辑即可
        return "find in db";
    }
}

/**
 * 定义缓存的抽象接口 为了将实现延迟到客户端
 */
interface CacheSource {
    /**
     * 设置缓存值
     *
     * @param key
     * @param value
     * @return
     */
    String setKeyValue(String key, String value);

    /**
     * 获取缓存值
     *
     * @param key
     * @return
     */
    String getValue(String key);
}

/**
 * 缓存实现1-使用HashMap作为缓存池
 */
class LocalCache implements CacheSource {
    private Map<String, String> cache = new HashMap<>(1024);

    public LocalCache() {
        super();
    }

    @Override
    public String setKeyValue(String key, String value) {
        return cache.put(key, value);
    }

    @Override
    public String getValue(String key) {
        return cache.get(key);
    }
}


class Test2 {
    public static void main(String[] args) throws IOException {
        /**
         * 思考一个问题  假设缓存有map redis memcache mongodb 等类型  数据源有mysql oracle 等类型  当缓存与数据源随意组合时  可产生m * n  8种不同组合得Setting实现
         * 如果有更多的缓存类型及数据源  那么就会带来子类爆炸问题
         * 想要在缓存和数据源两种维度各自拥有不同的拓展  那么可以使用另一种设计模式来实现--桥接设计模式
         *
         * 解答:通过以上实现将缓存抽离出接口 具体的数据源实现 具体的缓存实现将在不同方向上灵活拓展
         */
        Setting2 setting = new MysqlSetting(new LocalCache());
        setting.getResource("testKey");
    }
}

桥接模式源码

package com.designparttern.bridge;

/**
 * 桥接模式
 * 通过抽象将一个类中可能出现自由组合的组件进行管理,避免子类爆炸
 * 加入汽车品牌有很多种 奔驰 宝马 奥迪  引擎也有多种 电动 燃油  混合  如果将上述品牌及引擎自由组合在一起 通过继承方式实现 需要定义三个抽象类 至少九个不同子类
 * 如果再增加一个品牌和一个引擎 则整个汽车结构将变得异常臃肿难以维护
 * 桥接模式通过持有引擎抽象类  并将汽车抽象化  在客户端实现时再实现具体的汽车品牌及传入具体的引擎,将整个实现方式变得灵活
 */

/**
 * 顶层抽象类 汽车类  持有引擎接口  并且子类通过实现该接口灵活实现不同品牌
 */
public abstract class Car {
    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

    public abstract void drive();
}

/**
 * 为了使引擎可以独立拓展  通过抽象接口  定义共有行为
 * 汽车类持有顶层接口  可以灵活切换具体实现
 */
interface Engine {
    void start();
}

/**
 * 定义抽象修正类用来添加一些公共的额外的操作
 */
abstract class RefindCar extends Car {
    private Engine engine;

    public RefindCar(Engine engine) {
        super(engine);
        this.engine = engine;
    }

    @Override
    public void drive() {
        engine.start();
        System.out.println("Drive " + getBrand() + " car");
    }

    /**
     * 获取汽车品牌
     *
     * @return
     */
    public abstract String getBrand();
}

/**
 * 针对每一种汽车 继承自抽象修正类
 * 汽车品牌可以任意独立拓展  bossCar tinyCar bigCar 。。。
 */
class BossCar extends RefindCar {
    public BossCar(Engine engine) {
        super(engine);
    }

    @Override
    public String getBrand() {
        return "BossCar";
    }
}

/**
 * 针对每一种引擎都可以通过继承Engine来拓展
 * 引擎可以有任意多种  HyBridEngine  FuelOilEngine  ElectricEngine 。。。
 */
class HyBridEngine implements Engine {

    @Override
    public void start() {
        System.out.println("HyBridEngine start...");
    }
}

class TestDemo {
    public static void main(String[] args) {
        Car car = new BossCar(new HyBridEngine());
        car.drive();
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hongmin.shm

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值