七大结构型模式

结构型模式之一:适配器模式

将一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。
(来自《设计模式之禅》)

你要的故事

大家有买过港式的 Apple 产品么?在深圳的同学估计买过,毕竟港式的 Apple 产品基本比国内便宜 500 以上。我手机和平板都是在香港买的,买来后这充电器是没法直接充电的,因为港版的电子产品都是英式的插头,而咱们国内是中式的,所以用上港版电子产品的同学免不了要用上这么一个转换器:将英式的插孔转为中式的插孔,方可插入咱家里的插座充电。这个转换器就是今天想讲的适配器。

没见过的同学可以看看图片熟悉一下,下图右边为港版苹果手机充电器,插头比较大,左边为某品牌转换器,插头为中国家用标准形状。

图片

下图为使用时的图片

图片

在这描述一下这个场景。用港式插头要在国内充电,因为插头和插座大小对不上,所以需要加一个适配器,这个适配器充当插头和插座,它的插头可以插入国内标准的插座,它的插座可以插入港式标准的插头,这样子就可以用港式充电器在国内为手机充电。

下面用适配器模式代码实现这个场景。

首先需要找到被适配的对象是什么?在这里我们的被适配对象是英式充电器。

/**
 * 英式充电器
 */
class BritishCharger {

    public void chargeByBritishStandard(){
        System.out.println("用英式充电器充电");
    }

}

在这个场景的目的是什么?在中国为港式手机充电,因此目的是让英式充电器能够在中国标准的插座充电。

/**
 * 使用中式插座充电
 */
interface Target {

    void chargeByChineseStandard();

}

接下来是这个设计模式的主角:适配器。它需要连接中式插座以及英式充电器,在中间做适配功能。

/**
 * 充电器适配器
 */
class ChargerAdapter implements Target {

    private BritishCharger britishCharger;

    public ChargerAdapter(BritishCharger britishCharger) {
        this.britishCharger = britishCharger;
    }

    @Override
    public void chargeByChineseStandard() {
        System.out.println("使用中英式插头转换器");
        britishCharger.chargeByBritishStandard();
    }
}

上面是适配器模式的一个简单的例子,要学习适配器模式也可以看看 Java 的 IO 实现源码,里面是应用适配器模式的官方很好的代码。

总结

适配器很好的将 2 个无法关联的类结合起来,在中间起桥梁作用。另外新增适配器代码不会影响原来被适配者的正常使用,他们可以一起被使用。在工作中和外部系统对接的时候,大可能外部系统的数据格式和自己系统的数据格式并不相同,这时候就可以利用适配器模式来实现。

结构型模式之二:桥接模式

将抽象和实现解耦,使得两者可以独立地变化。
(来自《设计模式之禅》)

你要的故事

现在手机二分天下,安卓手机和苹果手机目前占有率高居 98.45%,其中安卓手机占有率为 70.21%,苹果手机占有率为 28.24%,如下图所示。

图片

最新手机系统市场份额


(数据从 netmarketshare 来)

因为有这 2 个系统,所以很多软件商都不得不开发 2 个系统的 APP。我们就拿这个案例来讲,目前手机有安卓手机和苹果手机,软件有谷歌浏览器和火狐浏览器,通过手机打开软件这一过程来讲讲桥接模式。

从个人介绍可见,需要抽象化和实现化,然后使用桥接模式将抽象和实现解耦。

抽象化:把一类对象共有的东西抽象到一个类里面,该类作为这类对象的基类。在这里我们可以抽象化的便是手机

实现化:将接口或抽象类的未实现的方法进行实现。在这里我们可以实现化的就是软件

将抽象和实现解耦:有了上面的抽象化和实现化,通过桥接模式来实现解耦。在这里,我们把打开软件 open() 放到软件实现中,而抽象的手机利用模板方法模式定义 openSoftware() 供手机子类去实现,手机子类也是调用软件的 open() 方法,并没有自己实现打开逻辑,也就是解耦了这个打开软件过程。

下面给出案例的代码。

Phone 手机抽象类代码。属性 system 代表系统名称,software 代表要打开的软件,openSoftware() 对外提供打开软件的方法。

abstract class Phone {

    private String system;
    private Software software;

    public abstract void openSoftware();

    public String getSystem() {
        return system;
    }

    public void setSystem(String system) {
        this.system = system;
    }

    public Software getSoftware() {
        return software;
    }

    public void setSoftware(Software software) {
        this.software = software;
    }

}

AndroidPhone 安卓系统手机代码。

class AndroidPhone extends Phone {

    public AndroidPhone(Software software){
        this.setSystem("Android");
        this.setSoftware(software);
    }

    @Override
    public void openSoftware() {
        this.getSoftware().open(this);
    }
}

IOSPhone IOS 系统手机代码(也就是苹果手机)。

class IOSPhone extends Phone {

    public IOSPhone(Software software) {
        this.setSystem("IOS");
        this.setSoftware(software);
    }

    @Override
    public void openSoftware() {
        this.getSoftware().open(this);
    }
}

Software 软件接口代码。它有一个方法 open(),用于打开该软件。

interface Software {
    void open(Phone phone);
}

Chrome 谷歌浏览器软件代码。

class Chrome implements Software {

    @Override
    public void open(Phone phone) {
        System.out.println("打开 " + phone.getSystem() + " 手机的 Chrome 浏览器");
    }

}

FireFox 火狐浏览器软件代码。

class FireFox implements Software {

    @Override
    public void open(Phone phone) {
        System.out.println("打开 " + phone.getSystem() + " 手机的 Firefox 浏览器");
    }

}

测试代码如下。

public class BridgeTest {

    public static void main(String[] args) {
        Software chrome = new Chrome();
        Software firefox = new FireFox();

        Phone androidPhone = new AndroidPhone(chrome);
        androidPhone.openSoftware();

        androidPhone.setSoftware(firefox);
        androidPhone.openSoftware();

        Phone iosPhone = new IOSPhone(chrome);
        iosPhone.openSoftware();

        iosPhone.setSoftware(firefox);
        iosPhone.openSoftware();
    }

}

打印结果:
打开 Android 手机的 Chrome 浏览器
打开 Android 手机的 Firefox 浏览器
打开 IOS 手机的 Chrome 浏览器
打开 IOS 手机的 Firefox 浏览器

桥接模式代码已经写完。为什么叫桥接模式呢?因为它将打开软件的具体实现放到了软件实现里面,而不是放在了手机,通过聚合方式去调用软件打开的方法,这就像一条桥一样连接手机和软件。

总结

桥接模式利用了聚合的优点去解决继承的缺点,使得抽象和实现进行分离解耦。正由于解耦,使得有更好的扩展性,加手机类型或者加软件都非常容易,也不会破坏原有的代码。

 

结构型模式之三:组合模式

将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。
(来自《设计模式之禅》)

你要的故事

今天咱们再讲讲咱们程序猿的组织架构。技术类的组织架构比较单一,基本上都是这样:经理--->组长--->工程师,如下图所示。

图片

IT组织架构

各个公司的 title 可能不太一样,但是基本是差不多这种架构,按职业发展,从入职到能独立开发需求便为工程师,从独立开发需求到能带小团队开发便为组长,从带小团队开发到能带几个团队一起协作开发便为经理。

假设目前有一家公司,技术部就 4 个人,大熊担任经理,中熊担任组长,小熊1和小熊2担任工程师。下面的代码都围绕这个假设编写。

非组合模式

我们先来一个非正常的实现方案:从组织架构里,有 3 个角色,分别是经理、组长、工程师,那么我们就按角色去实现一番。

Manager 为经理类,经理下有多个组长 leaders。

/**
 * 经理
 */
class Manager {

    private String name;
    private List<Leader> leaders;

    public Manager(String name) {
        this.name = name;
        this.leaders = new LinkedList<>();
    }

    public void add(Leader leader) {
        this.leaders.add(leader);
    }

    public void remove(Leader leader) {
        this.leaders.remove(leader);
    }

    public void display(int index) {
        for (int i = 0; i < index; i++) {
            System.out.print("----");
        }
        System.out.println("经理:" + this.name);
        leaders.forEach(leader -> {
            leader.display(index+1);
        });
    }

}

Leader 为组长类,组长下有多个工程师 engineers。

/**
 * 组长
 */
class Leader {

    private String name;
    private List<Engineer> engineers;

    public Leader(String name) {
        this.name = name;
        this.engineers = new LinkedList<>();
    }

    public void add(Engineer engineer) {
        this.engineers.add(engineer);
    }

    public void remove(Engineer engineer) {
        this.engineers.remove(engineer);
    }

    public void display(int index) {
        for (int i = 0; i < index; i++) {
            System.out.print("----");
        }
        System.out.println("组长:" + this.name);
        engineers.forEach(engineer -> {
            engineer.display(index + 1);
        });
    }
}

Engineer 为工程师类,工程师没有下属。

/**
 * 工程师
 */
class Engineer {

    private String name;

    public Engineer(String name) {
        this.name = name;
    }

    public void display(int index) {
        for (int i = 0; i < index; i++) {
            System.out.print("----");
        }
        System.out.println("工程师:" + this.name);
    }

}

测试代码

public class NoCompositeTest {

    public static void main(String[] args) {
        Manager manager = new Manager("大熊");
        Leader leader = new Leader("中熊");
        Engineer engineer1= new Engineer("小熊1");
        Engineer engineer2 = new Engineer("小熊2");

        manager.add(leader);
        leader.add(engineer1);
        leader.add(engineer2);

        manager.display(0);
    }

}

打印结果:
经理:大熊
----组长:中熊
--------工程师:小熊1
--------工程师:小熊2

这份代码看完之后,有什么想法?是不是感觉代码有点冗余?经理和组长的代码几乎一致,而工程师类和经理类、组长类也有共同点,唯一的区别就是工程师没有下属,因此没有对下属的增删操作方法。

安全模式

通过上面一层思考,这 3 个角色有相通性,我们可以抽象出一个 Employee2 类,把 3 个角色共同的特性放到 Employee2 类中,经理和组长合并共用一个类,因为在这个例子里,这 2 个角色完全一样的。下面看代码。

Employee2 抽象类,它有这 3 个角色共有的特性,名称设置获取以及显示数据。

abstract class Employee2 {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public abstract void display(int index);

}

Leader2 领导类,把上面的经理类和组长类都合并到这个领导类,因为他们都是领导层。

class Leader2 extends Employee2 {

    private List<Employee2> employees;

    public Leader2(String name) {
        this.setName(name);
        this.employees = new ArrayList<>();
    }

    public void add(Employee2 employee) {
        this.employees.add(employee);
    }

    public void remove(Employee2 employee) {
        this.employees.remove(employee);
    }

    @Override
    public void display(int index) {
        for(int i = 0; i < index; i++) {
            System.out.print("----");
        }
        System.out.println("领导:" + this.getName());
        this.employees.forEach(employee -> {
            employee.display(index + 1);
        });
    }
}

Engineer2 工程师类,工程师类比较简单,因为名称设置获取在抽象类 Employee2 有了,所以就只需实现显示数据的功能。

class Engineer2 extends Employee2 {

    public Engineer2(String name) {
        this.setName(name);
    }

    @Override
    public void display(int index) {
        for(int i = 0; i < index; i++) {
            System.out.print("----");
        }
        System.out.println("工程师:" + this.getName());
    }
}

测试代码

public class CompositeTest {

    public static void main(String[] args) {
        // 安全模式
        Leader2 leader1 = new Leader2("大熊");
        Leader2 leader2 = new Leader2("中熊");
        Engineer2 engineer1 = new Engineer2("小熊1");
        Engineer2 engineer2 = new Engineer2("小熊2");

        leader1.add(leader2);
        leader2.add(engineer1);
        leader2.add(engineer2);

        leader1.display(0);
    }

}

打印结果:
领导:大熊
----领导:中熊
--------工程师:小熊1
--------工程师:小熊2

看下运行结果和上面是一致的,这份代码比第一份代码有更好的封装性,也更符合面向对象的编程方式,经理和组长被合并成 Leader2,也就是咱们今天讲的组合模式,Leader2 为组合对象。上面讲的是安全模式,安全模式指的是抽象类 Employee2 只提供了 3 个角色中共有的特性,安全是相对透明模式所说的,因为这里领导类 Leader2 和工程师类 Engineer2 都只提供了自己能提供的方法,Engineer2 不会有多余的方法,而透明模式则不是。下面讲讲透明模式。

透明模式

透明模式把组合对象(即领导类)使用的方法放到抽象类中,而因为工程师没有下属,则不具体实现对应的方法。代码如下。

Employee3 抽象类,将组合对象的属性 employees 和 方法 add()、 remove() 都放到这个类里面。

abstract class Employee3 {

    private String name;
    private List<Employee3> employees;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<Employee3> getEmployees() {
        return employees;
    }

    public void setEmployees(List<Employee3> employees) {
        this.employees = employees;
    }

    public abstract void add(Employee3 employee);

    public abstract void remove(Employee3 employee);

    public abstract void display(int index);

}

Leader3 领导类,具体实现 Employee3 提供的所有方法。

class Leader3 extends Employee3 {

    public Leader3(String name) {
        this.setName(name);
        this.setEmployees(new ArrayList<>());
    }

    @Override
    public void add(Employee3 employee) {
        this.getEmployees().add(employee);
    }

    @Override
    public void remove(Employee3 employee) {
        this.getEmployees().remove(employee);
    }

    @Override
    public void display(int index) {
        for(int i = 0; i < index; i++) {
            System.out.print("----");
        }
        System.out.println("领导:" + this.getName());
        this.getEmployees().forEach(employee -> {
            employee.display(index + 1);
        });
    }
}

Engineer3 工程师类,只具体实现 Employee3 中的 display() 方法,add() 和 remove() 方法不是工程师具备的,所以留空,不做具体实现。

class Engineer3 extends Employee3 {

    public Engineer3(String name) {
        this.setName(name);
    }

    @Override
    public void add(Employee3 employee) {
        // 没有下属
    }

    @Override
    public void remove(Employee3 employee) {
        // 没有下属
    }

    @Override
    public void display(int index) {
        for(int i = 0; i < index; i++) {
            System.out.print("----");
        }
        System.out.println("工程师:" + this.getName());
    }
}

测试代码:

public class CompositeTest {

    public static void main(String[] args) {
        // 透明模式
        Leader3 leader3 = new Leader3("大熊");
        Leader3 leader31 = new Leader3("中熊");
        Engineer3 engineer31 = new Engineer3("小熊1");
        Engineer3 engineer32 = new Engineer3("小熊2");

        leader3.add(leader31);
        leader31.add(engineer31);
        leader31.add(engineer32);

        leader3.display(0);

    }

打印结果:
领导:大熊
----领导:中熊
--------工程师:小熊1
--------工程师:小熊2
}

安全模式把 3 个角色的共同点抽象到 Employee2 中,透明模式则把 3 个角色中的领导者(组合对象)的内容抽象到 Employee3 中。透明模式有些不好的地方在于工程师也有领导者的下属对象和相应的方法,其实工程师并没有这些功能。安全模式把领导者和工程师分开,每个对象都只提供自己具有的功能,这样子在使用的时候也就更安全。

总结

我们根据 IT 组织架构,从简单的每个角色对应一个类的实现,再到抽象出每个角色共同的功能、组合领导类的安全模式,接着再到抽象起来领导类(组合)所有功能的透明模式,分析了组合模式的完整过程,也讲了安全模式和透明模式的差异。组合模式让对象更加有层次,将对象的划分更加清晰,特别是树形结构的层次,利用组合模式会更加简化。

结构型模式之四:装饰模式 

动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式相比生成子类更为灵活。
(来自《设计模式之禅》)

你要的故事

夏天到了,吃货们期待的各种各样冷冻零食就要大面积面向市场,什么冰淇淋、雪糕、冰棍等等。今天的装饰模式不讲这些都受欢迎的零食,讲讲那乌黑滴龟苓膏。不知道大伙们喜不喜欢吃龟苓膏,我是挺喜欢的,不喜欢的人很多都闲它苦,应该没有人愿意在没加任何糖类的情况下吃龟苓膏。很多糖水店会提供几种龟苓膏,比如蜂蜜龟苓膏、牛奶龟苓膏。下面我们空想出一个场景来。

天气到了 30℃,小明和小红加班到了 10 点,一起下班,路过一家糖水店,小红萌生了吃糖水解解热的想法,小明最近涨薪,就提出要请小红吃糖水,他们进去糖水店,小红想着好久没吃龟苓膏了,就想吃吃,怀念一下童年那段在农村夜晚吃龟苓膏的时光。糖水店里面有 3 种龟苓膏,一种是普通龟苓膏(这老板居然提供不加任何糖分的,过分了),一种是蜂蜜龟苓膏,另外一种是牛奶龟苓膏,小明点了一份蜂蜜龟苓膏,小红想加蜂蜜和牛奶,就咨询了老板娘,能否同时加蜂蜜和牛奶,老板娘用那东北腔爽快地回复小红:行。小明和小红就等龟苓膏上桌。。。脑洞到这。

我们来把这个故事套入到装饰模式里去。上面故事里老板卖 3 种龟苓膏,而都不满足小红的需求,小红想要的是蜂蜜牛奶龟苓膏,如果用继承来实现龟苓膏,那就无法满足小红的要求了,因为继承直接固定了龟苓膏的做法,加什么就是什么,要蜂蜜牛奶龟苓膏,那就需要另外一个龟苓膏类代表蜂蜜牛奶龟苓膏;而用装饰模式则不同,下面看看装饰模式的实现代码。

龟苓膏抽象类,该类定义了制作龟苓膏的抽象方法。

/**
 * 龟苓膏
 */
abstract class HerbalJelly {

    /**
     * 制作龟苓膏方法
     */
    public abstract void process();

}

老板提供的最基本的龟苓膏,这种龟苓膏不加任何料,就是那苦苦的龟苓膏,我们称它为普通龟苓膏。

/**
 * 普通龟苓膏
 */
class CommonHerbalJelly extends HerbalJelly {

    @Override
    public void process() {
        System.out.println("盛一碗龟苓膏");
    }

}

另外 2 种龟苓膏:蜂蜜龟苓膏和牛奶龟苓膏,不是用继承实现,而是用装饰器实现,我们可以发现这 2 种龟苓膏都是基于上面普通龟苓膏添加不同的糖类食品制作而成。下面实现一个抽象类充当装饰器。

/**
 * 龟苓膏装饰器
 */
abstract class Decorator extends HerbalJelly {

    private HerbalJelly herbalJelly;

    public Decorator(HerbalJelly herbalJelly) {
        this.herbalJelly = herbalJelly;
    }

    @Override
    public void process() {
        this.herbalJelly.process();
    }
}

接下来就根据上面的龟苓膏装饰器来实现蜂蜜龟苓膏和牛奶龟苓膏。

/**
 * 蜂蜜龟苓膏
 */
class HoneyHerbalJelly extends Decorator{

    public HoneyHerbalJelly(HerbalJelly herbalJelly) {
        super(herbalJelly);
    }

    @Override
    public void process() {
        super.process();
        System.out.println("加蜂蜜");
    }
}

/**
 * 牛奶龟苓膏
 */
class MilkHerbalJelly extends Decorator{

    public MilkHerbalJelly(HerbalJelly herbalJelly) {
        super(herbalJelly);
    }

    @Override
    public void process() {
        super.process();
        System.out.println("加牛奶");
    }
}

下面提供我们的测试代码,还记得上面说的,小明要了一碗蜂蜜龟苓膏,小红则要了一碗蜂蜜牛奶龟苓膏。

public class DecoratorTest {

    public static void main(String[] args) {
        CommonHerbalJelly commonHerbalJelly = new CommonHerbalJelly();
        HoneyHerbalJelly honeyHerbalJelly = new HoneyHerbalJelly(commonHerbalJelly);
        // 小明的蜂蜜龟苓膏
        honeyHerbalJelly.process();

        MilkHerbalJelly milkHerbalJelly = new MilkHerbalJelly(honeyHerbalJelly);
        // 小红的蜂蜜牛奶龟苓膏
        milkHerbalJelly.process();
    }

}

打印结果:
盛一碗龟苓膏
加蜂蜜
盛一碗龟苓膏
加蜂蜜
加牛奶

我们看到,小明的龟苓膏只加蜂蜜,小红的龟苓膏加了蜂蜜和牛奶,这样就很简单的满足了小红的要求。

总结

装饰模式在一些类与类之间有叠加效应(也就是给一个类增加附加功能)的场景中非常好用,它可以说是继承的替代品,有更好的扩展性,也比较灵活。在 Java JDK 源码中也大面积用到了装饰模式,比如:java.io.BufferedInputStream(InputStream)。学完基础知识后,可以看看源码中是怎么实现的,巩固知识。

结构型模式之五:外观模式

要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。
(来自《设计模式之禅》)

你要的故事

作为开发同学,我们平时打交道最多的就是需求同学和测试同学,公司小的时候,什么事情都全靠吼,工作也直接一对一,一个需求下来,需求同学先跟开发同学一起跟进这个需求,需求开发完成了,需求同学和测试同学沟通了需求的测试要点,测试同学就开测。这个过程中需求一直跟到上线。我们用代码来描述这个过程。

开发同学,负责开发需求。

/**
 * 开发同学
 */
class Developer {

    public void develop(String name) {
        System.out.println("开发需求:" + name);
    }

}

测试同学,负责测试需求。

/**
 * 测试同学
 */
class Tester {

    public void test(String name) {
        System.out.println("测试需求:" + name);
    }

}

需求同学,负责提需求,也负责跟进需求的开发、测试,直到上线。

/**
 * 需求同学
 */
class Demander {

    private Developer developer = new Developer();
    private Tester tester = new Tester();

    public void demand(String name) {
        System.out.println("提需求:" + name);
        developer.develop(name);
        tester.test(name);
    }

}

测试代码。

public class FacadeTest {

    public static void main(String[] args) {
        Demander demander = new Demander();
        demander.demand("开发一个跟淘宝一样的系统");
    }
}

打印结果:
提需求:开发一个跟淘宝一样的系统
开发需求:开发一个跟淘宝一样的系统
测试需求:开发一个跟淘宝一样的系统

公司小的时候,这样干没啥问题,咱关注的是业务的迭代速度和沟通成本,大家都是在一块办公,随时吼一声完事。当公司发展到一定程度,比如有 100 来人,其中需求 10 人、开发 70 人、测试 20 人,那就没法靠吼来沟通了,需要有一个比较规范化的沟通机制。一般会这样子引进,开发会把一些沟通能力较强、把控开发流程能力较好的同学升职为组长,负责保证一个需求的正常开发,他们会直接面对需求同学,直接沟通需求的开发要点,然后组长安排开发同学和测试同学跟进这个需求直到上线,也就是把需求同学以前的工作分配到开发组长,让他把控整个流程,这样就不会使得开发同学、测试同学、需求同学之间互相频繁沟通影响效率。这样子我们看看代码实现。

多了一个技术组长的类,负责跟进整个需求的开发测试过程。

/**
 * 技术组长
 */
class Leader {

    private Developer developer = new Developer();
    private Tester tester = new Tester();

    public void processDemand(String name) {
        developer.develop(name);
        tester.test(name);
    }

}

需求同学就不用直接和开发同学、测试同学沟通了,就跟技术组长对接就好。

/**
 * 需求同学
 */
class Demander2 {

    public Leader leader = new Leader();
    public void demand(String name) {
        System.out.println("提需求:" + name);
        leader.processDemand(name);
    }

}

测试代码。

public class FacadeTest {

    public static void main(String[] args) {
        Demander2 demander2 = new Demander2();
        demander2.demand("开发一个跟微信一样的系统");
    }

}

打印结果:
提需求:开发一个跟微信一样的系统
开发需求:开发一个跟微信一样的系统
测试需求:开发一个跟微信一样的系统

这个就是我们的外观模式,我们的技术组长就是外观模式的象征,他专门对外提供接收需求服务,然后安排需求给开发同学和测试同学,保证完成。

总结

外观模式通过一个对外统一的接口,隐藏了内部的具体实现,使得外部系统可以更加简单的访问,也减少了外部系统对内部系统的依赖,从上面的例子讲,如果开发同学开发一半生病短时间无法来上班,交接给其他同学,由组长内部安排解决,需求同学并不需要知道。外观模式在微服务交互之间经常使用。

结构型模式之六:享元模式

使用共享对象可有效地支持大量的细粒度的对象。
(来自《设计模式之禅》)

你要的故事

还记得笔袋么?可能有人已经忘记了,在写这篇文章之前其实我也忘了,从初中开始就再也没用过笔袋。拿笔袋来讲享元模式再适合不过了。笔袋放各种各样的笔,今天我们不讲别的,就讲蜡笔。前段时间在逛公园的时候,看到一位老师在画画,画的就是蜡笔画,第一次看到真正的蜡笔画,真的很震撼,原来蜡笔也可以把景色画得那么美。当时偷偷拍了一张,看下图。

图片

我们就拿这幅画来说,里面画了草、树、路、山、天空等等。如果没有用享元模式,我们可能这样子实现。

蜡笔接口。

interface ICrayon {

    void draw(String place);

}

蜡笔。

/**
 * 蜡笔
 */
class Crayon implements ICrayon {

    private String color;

    public Crayon(String color) {
        System.out.println("---新建【" + color + "】蜡笔" );
        this.color = color;
    }

    @Override
    public void draw(String place) {
        System.out.println("用" + this.color + "蜡笔画" + place);
    }
}

测试代码。这幅画是小明和小红一起画,小明画了草和路,小红画了树和蓝天。

public class NoFlyweightTest {

    public static void main(String[] args) {
        drawByXiaoMing();
        drawByXiaoHong();
    }

    public static void drawByXiaoMing() {
        ICrayon greenCrayon = new Crayon("绿色");
        greenCrayon.draw("草");

        ICrayon grayCrayon = new Crayon("灰色");
        grayCrayon.draw("路");
    }

    public static void drawByXiaoHong() {
        ICrayon blueCrayon = new Crayon("蓝色");
        blueCrayon.draw("蓝天");

        ICrayon greenCrayon = new Crayon("绿色");
        greenCrayon.draw("树");
    }

}

打印结果:
---新建【绿色】蜡笔
用绿色蜡笔画草
---新建【灰色】蜡笔
用灰色蜡笔画路
---新建【蓝色】蜡笔
用蓝色蜡笔画蓝天
---新建【绿色】蜡笔
用绿色蜡笔画树

我们发现小明和小红都用了绿色蜡笔,而这里新建了 2 次绿色蜡笔,也就是在整个作画过程中,小明和小红并不是共用一套蜡笔,而是各自用一套蜡笔,在现实中是没什么问题的,但是在软件开发中,如果这种情况出现,其实相当于资源浪费,因为每个蜡笔都会占用内存,可以共用的我们尽量共用,节省一些内存空间,特别是出现很多这种可以共享却没有共享的对象时候。下面我们引入享元模式。享元模式实现方法相当于我们蜡笔都放在了笔袋,小明和小红用完就放到笔袋里面,每一种颜色的蜡笔只有一根,也就是他们共用一套蜡笔。代码如下所示。

笔袋代码。我们用了 Map 作为容器,如果容器里面没有想要颜色的蜡笔,则创建新的蜡笔,并存到容器里。

/**
 * 笔袋
 */
class CrayonFactory {

    private static Map<String, ICrayon> data = new HashMap<>();

    public static ICrayon getCrayon(String color) {
        if (data.containsKey(color)) {
            return data.get(color);
        }
        ICrayon crayon = new Crayon(color);
        data.put(color, crayon);
        return crayon;
    }

}

测试代码。

public class FlyweightTest {

    public static void main(String[] args) {
        drawByXiaoMing();
        drawByXiaoHong();
    }

    public static void drawByXiaoMing() {
        ICrayon greenCrayon = CrayonFactory.getCrayon("绿色");
        greenCrayon.draw("草");

        ICrayon grayCrayon = CrayonFactory.getCrayon("灰色");
        grayCrayon.draw("路");
    }

    public static void drawByXiaoHong() {
        ICrayon blueCrayon = CrayonFactory.getCrayon("蓝色");
        blueCrayon.draw("蓝天");

        ICrayon greenCrayon = CrayonFactory.getCrayon("绿色");
        greenCrayon.draw("树");
    }
}

打印结果:
---新建【绿色】蜡笔
用绿色蜡笔画草
---新建【灰色】蜡笔
用灰色蜡笔画路
---新建【蓝色】蜡笔
用蓝色蜡笔画蓝天
用绿色蜡笔画树

利用享元模式实现的结果,小红画树所用到的绿色蜡笔跟小明画草的绿色蜡笔一样,小红用到时并没有新建绿色蜡笔。

总结

是不是有一种,原来这就是享元模式的感觉?平时开发过程中经常见到这种因为很多重复的对象,所以利用享元模式来实现的场景。享元模式合理提高了对象的复用性,减少了程序的内存占用,还有一个提高性能的地方就是减少了对象创建的过程。好了,收下这个简单的设计模式。欢迎关注公众号,一起学习进步。

 

结构型模式之七:代理模式

为其他对象提供一种代理以控制对这个对象的访问。
(来自《设计模式之禅》)

你要的故事

咱们从事 IT 行业,随时都可能上网查东西,如果网络速度慢或者网络访问受限制,那是相当的折磨,忍无可忍。而咱在国内网络比较特殊,有个墙围着,俗称防火长城。今天讲到代理模式,就来讲讲这道墙。这墙是这么实现的,我们上网,正常的网络是世界各地的网站我们都能访问,而加上这道墙,相当于在我们上网的时候做了一层代理,这一层代理把禁用的网站给过滤掉,使得我们没法访问被禁用的网站。下面通过代码来讲解。

定义一个互联网接口,里面有一个访问网站的通用方法 access

/**
 * 互联网
 */
interface Internet {

    String access(String domain);

}

定义世界范围内的网络类,可以访问任何存在的网站。

/**
 * 世界网络
 */
class WorldNetwork implements Internet {

    @Override
    public String access(String domain) {
        System.out.println("访问网站:" + domain);
        return domain + "网站内容";
    }

}

定义中国的网络类,就是代理类,实现墙的功能。disable 对象存储了在国内禁止访问的网站,用户在访问网站时(也就是调用 access 访问)先判断网站是不是在禁用的网站集合里面,如果是则禁用,如果不是则继续访问。

/**
 * 中国网络(就是代理)
 */
class ChinnessNetwork implements Internet {

    private Set<String> disable;

    private Internet internet;

    public ChinnessNetwork(Internet internet) {
        this.internet = internet;
        this.disable = new HashSet<>();
        this.disable.add("www.google.com");
        this.disable.add("www.facebook.com");
    }

    @Override
    public String access(String domain) {
        if (disable.contains(domain)) {
            System.out.println("禁止访问该网站:" + domain);
            return "禁止访问该网站:" + domain;
        }
        return internet.access(domain);
    }
}

测试代码,ChinnessNetwork 作为代理类,WorldNetwork 作为被代理类。

public class ProxyTest {

    public static void main(String[] args) {
        WorldNetwork worldNetwork = new WorldNetwork();
        ChinnessNetwork chinnessNetwork = new ChinnessNetwork(worldNetwork);
        chinnessNetwork.access("www.google.com");
        chinnessNetwork.access("www.baidu.com");
    }

}

打印结果:
禁止访问该网站:www.google.com
访问网站:www.baidu.com

看到结果,万能的 google 被屏蔽在外,我们只能默默用 baidu。

总结

代理模式简单说就是在原来对象的功能基础上加上额外的功能,在工作开发中这个很好用,比如我们要统计系统中各方法执行的时间,就可以用代理模式来实现。开源框架中也用得很多,比如 Spring 的 AOP 等等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值