zoukankan      html  css  js  c++  java
  • 【设计模式】第十一篇:来一起瞅瞅享元模式

    今天一起来看一个新的设计模式,那就是享元模式,关于此模式,常见的就是 “项目外包”、
    以及 “五子棋” 这样两个例子,我们下面就选择使用 “项目外包” 这个例子引入去讲

    一 故事引入

    (一) 故事背景

    程序员小B,帮助客户 A 做了一个展示一些产品内容的网站,通过 A 的 推荐,客户 B 、客户C 也想要做这样一个网站,但是就是形式有一些变化

    • 有的客户希望是新闻发布形式的
    • 有的客户希望是博客形式的
    • 有的客户希望是公众号形式的等等

    而且他们都希望能够降低一些费用,但是每一个空间部署着一个网站,所以租借空间的费用是固定的,同时程序员小B 并不想从自己的劳动报酬中缩减费用

    (二) 思考解决方案

    (1) 最简单的传统方案

    先说最简单能想到的方案,直接把网站代码复制几份,然后每一个都租借一个空间,然后对代码进行定制修改。注:这里还没考虑优化或者省钱

    我们用一个 WebSite 类来模拟一个网站的模板,所有类型可以通过对 name 赋值然后调用 use 方法进行修改

    public class WebSite {
        private String name = "";
    
        public WebSite(String name) {
            this.name = name;
        }
    
        public void use(){
            System.out.println("当前网站分类: " + name);
        }
    }
    

    如果按照刚才的思路,是这样操作的

    public class Test {
        public static void main(String[] args) {
            WebSite webSite1 = new WebSite("博客");
            webSite1.use();
    
            WebSite webSite2 = new WebSite("博客");
            webSite2.use();
    
            WebSite webSite3 = new WebSite("博客");
            webSite3.use();
    
            WebSite webSite4 = new WebSite("新闻发布");
            webSite4.use();
    
            WebSite webSite5 = new WebSite("公众号");
            webSite5.use();
    
            WebSite webSite6 = new WebSite("公众号");
            webSite6.use();
        }
    }
    

    运行结果:

    当前网站分类: 博客
    当前网站分类: 博客
    当前网站分类: 博客
    当前网站分类: 新闻发布
    当前网站分类: 公众号
    当前网站分类: 公众号

    (2) 存在的问题及改进思路

    • ① 假设虚拟空间在同一台服务器上,做上述内容,需要实例化 6 个 WebSite,而其本质又没有很大的差别,所以对于服务器的资源浪费很大

    • ② 网站结构相似度很高,基本全是重复的代码

    对于这种重复性很高的内容,首先我们要做到将其抽象出来,重复创建实例在设计模式中肯定是不太明智的,我们想要做到多个客户,共享同一个实例。这样不管是代码还是服务器资源利用,都会改善很多

    一个不算特别恰当的例子:例如外卖平台中的一个一个商家店铺,是不是可以理解为平台中的一个小店铺,小网站,其中通过例如店铺 ID 等内容来区分不同店铺,但是其每一家店铺整体的模板和样子是差不多的。

    我们下面要做的就是,将大量相似内容抽象成一个网站模板类,然后把一些特定的内容,通过参数移到实例的外面,调用的时候再指定,这样可以大幅度减少单个实例的数目。

    (3) 享元模式初步改进

    创建一个抽象的 WebSite 类

    public abstract class WebSite {
        public abstract void use();
    }
    

    接下来是具体实现,创建其子类,和前面一样,所有类型可以通过对 type赋值然后调用 use 方法进行修改

    public class ConcreteWebSite extends WebSite {
    
        // 网站发布形式
        private String type = "";
    
        public ConcreteWebSite(String type) {
            this.type = type;
        }
    
        @Override
        public void use() {
            System.out.println("当前网站分类: " + type);
        }
    }
    

    创建一个工厂类,用于创建,返回一个指定的网站实例

    这一个类,首先用一个 HashMap 模拟一种连接池的概念,因为我们既然想要达到不重复创建实例的效果,就需要通过一些逻辑判断,判断 Map 中是否存在这个实例,如果有就直接返回,如果没有就创建一个新的,同样类型 type 是在调用时,显式的指定的。

    后面补充了一个获取网站分类总数的方法,用来测试的时候,看一下是不是没有重复创建实例

    import java.util.HashMap;
    
    /**
     * 网站工厂类,根据需要返回
     */
    public class WebSiteFactory {
        // 模拟一个连接池
        private HashMap<String, ConcreteWebSite> pool = new HashMap<>();
    
        /**
         * 获取网站:根据传入的类型,返回网站,无则创建,有则直接返回
         *
         * @param type
         * @return
         */
        public WebSite getWebSiteCategory(String type) {
            if (!pool.containsKey(type)) {
                // 创建一个网站,放到池种
                pool.put(type, new ConcreteWebSite(type));
            }
            return (WebSite) pool.get(type);
        }
    
        /**
         * 获取网站分类总数
         */
        public int getWebSiteCount() {
            return pool.size();
        }
    
    }
    

    测试一下

    public class Test {
        public static void main(String[] args) {
            // 创建一个工厂
            WebSiteFactory factory = new WebSiteFactory();
    
            // 给客户创建一个博客类型的网站
            WebSite webSite1  = factory.getWebSiteCategory("博客");
            webSite1.use();
    
            // 给客户创建一个博客类型的网站
            WebSite webSite2  = factory.getWebSiteCategory("博客");
            webSite2.use();
    
            // 给客户创建一个博客类型的网站
            WebSite webSite3  = factory.getWebSiteCategory("博客");
            webSite3.use();
    
            // 给客户创建一个新闻发布类型的网站
            WebSite webSite4  = factory.getWebSiteCategory("新闻发布");
            webSite4.use();
    
            // 给客户创建一个公众号类型的网站
            WebSite webSite5  = factory.getWebSiteCategory("公众号");
            webSite5.use();
    
            // 给客户创建一个公众号类型的网站
            WebSite webSite6  = factory.getWebSiteCategory("公众号");
            webSite6.use();
    
            // 查看一下连接池中的实例数
            System.out.println("实例数:" + factory.getWebSiteCount());
        }
    }
    

    运行结果:

    当前网站分类: 博客
    当前网站分类: 博客
    当前网站分类: 博客
    当前网站分类: 新闻发布
    当前网站分类: 公众号
    当前网站分类: 公众号
    实例数:3

    (4) 享元模式再改进-区分内外部状态

    上面的代码,使用工厂代替了直接实例化的方式,工厂中,主要通过一个池的概念,实现了共享对象的目的,但是其实我们会发现,例如创建三个博客类型的网站,但是好像这三个网站就是一模一样的,但是不同的客户,其中博客网站中的数据肯定是不同的,这就是我们还没有区分内部外部的状态

    内部状态:对象共享出来的信息,存储在享元对象内部并且不会随环境改变的共享部分

    外部状态:对象用来标记的一个内容,随环境会改变,不可共享

    打个比方,五子棋只有黑白两色,总不能下多少子,就创建多少个实例吧,所以我们把颜色看做内部状态,有黑白两种颜色。而各个棋子的位置并不相同,当我们落子后这个位置信息才会被传入,所以位置信息就是外部状态

    那么对于“外包网站”的例子中,很显然,不同的客户网站数据就是一个外部状态,下面来修改一下

    首先新增一个 User 类,后面会将其引入作为外部状态

    public class User {
        private String name;
    
        public User(String name) {
            this.name = name;
        }
    
        public String getName() {
            return name;
        }
    }
    

    修改抽象类和子类,通过参数的方式引入 User 这个外部状态

    抽象类

    public abstract class WebSite {
        public abstract void use(User user);
    }
    

    子类

    public class ConcreteWebSite extends WebSite {
    
        // 网站发布形式
        private String type = "";
    
        public ConcreteWebSite(String type) {
            this.type = type;
        }
    
        @Override
        public void use(User user) {
            System.out.println("【网站分类】: " + type + " 【客户】: " + user.getName());
        }
    }
    

    工厂类不变,最后修改测试类

    public class Test {
        public static void main(String[] args) {
            // 创建一个工厂
            WebSiteFactory factory = new WebSiteFactory();
    
            // 给客户创建一个博客类型的网站
            WebSite webSite1  = factory.getWebSiteCategory("博客");
            webSite1.use(new User("客户A"));
    
            // 给客户创建一个博客类型的网站
            WebSite webSite2  = factory.getWebSiteCategory("博客");
            webSite2.use(new User("客户B"));
    
            // 给客户创建一个博客类型的网站
            WebSite webSite3  = factory.getWebSiteCategory("博客");
            webSite3.use(new User("客户C"));
    
            // 给客户创建一个新闻发布类型的网站
            WebSite webSite4  = factory.getWebSiteCategory("新闻发布");
            webSite4.use(new User("客户A"));
    
            // 给客户创建一个公众号类型的网站
            WebSite webSite5  = factory.getWebSiteCategory("公众号");
            webSite5.use(new User("客户A"));
    
            // 给客户创建一个公众号类型的网站
            WebSite webSite6  = factory.getWebSiteCategory("公众号");
            webSite6.use(new User("客户B"));
    
            // 查看一下连接池中的实例数
            System.out.println("实例数:" + factory.getWebSiteCount());
            
        }
    }
    

    运行结果:

    【网站分类】: 博客 【客户】: 客户A
    【网站分类】: 博客 【客户】: 客户B
    【网站分类】: 博客 【客户】: 客户C
    【网站分类】: 新闻发布 【客户】: 客户A
    【网站分类】: 公众号 【客户】: 客户A
    【网站分类】: 公众号 【客户】: 客户B
    实例数:3

    可以看出来,虽然有 6 个客户,但是实际上只有三个实例,同样再增加几十个,也最多只会有三个实例

    二 享元模式概念

    (一) 概念

    定义:享元(Flyweight)模式运用共享技术来有效地支持大量细粒度对象的复用。

    它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。

    享元模式又叫做蝇量模式,所以英文为 Flyweight

    (二) 结构图

    注:方法参数和返回值没细细弄,主要为了说明结构

    • 抽象享元角色(Flyweight):是所有的具体享元类的超类或接口,非享元的外部状态以参数的形式通过方法传入。
    • 具体享元(Concrete Flyweight)角色:实现抽象享元角色中所规定的接口。
    • 非享元(Unsharable Flyweight) 角色:是不共享的外部状态,它以参数的形式注入具体享元的相关方法中,这也意味着,享元模式并不强制共享
    • 享元工厂(Flyweight Factory)角色:负责创建和管理享元角色。
      • 当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象
        • 如果存在则提供给客户
        • 如果不存在的话,则创建一个新的享元对象

    (二) 简述优缺点

    优点:相同对象只需要保存一份,降低了系统中内存的数量,减少了系统内存的压力

    缺点:程序复杂性增大,同时读取享元模式的外部状态会使得运行时间稍微变长

    (三) 应用场景

    享元模式其中也需要一个工厂进行控制,所以就好像是在工厂方法模式的基础上,增加了一个缓存机制,也就是通过一个 “池” 的概念,避免了大量相同的对象创建,大大降低了内存空间的消耗。

    那么应用场景如下:

    • 一个程序使用了大量相似或者相同的对象,且造成了很大的开销的时候
    • 大部分对象,可以根据内部状态分组,且可将不同部分外部化,这样每一个组只需保存一个内部状态。
      • 例如上面的博客,新闻,公众号站形式就是三种组,每个组只需要传入用户数据这个外部状态即可
    • 因为使用享元模式,需要一个保存享元的数据结构(例如上面的 Hashmap)所以请确认实例足够多的时候才值得去使用享元模式。
  • 相关阅读:
    超级简单的分屏控件【自定义PictureBox 继承于UserControl】
    《(学习笔记)两天进步一点点》(3)——应用BindingSource实现数据同步
    《(学习笔记)两天进步一点点》(5)——几个比较小的类,但很实用
    【唠叨两句】如何将一张树型结构的Excel表格中的数据导入到多张数据库表中
    《(学习笔记)两天进步一点点》(2) ——BindingSource基础操作
    微软通用类库——DbHelper
    经典的SQL语句
    ToString 中的常见格式
    【学习笔记】SQL Server 中的批量复制操作 (ADO.NET)
    《(学习笔记)两天进步一点点》(1)——Windows控件DGV
  • 原文地址:https://www.cnblogs.com/ideal-20/p/14301519.html
Copyright © 2011-2022 走看看