26.项目多也别傻做 - 享元模式(Fly-Weight)

26.1 项目多也别傻做!

  • 时间: 7月9日21点   地点: 小菜大鸟住所的客厅   人物: 小菜, 大鸟

     “小菜, 最近一直在忙些什么呢? 回家就自个开始忙开了.” 大鸟问道.

     “哦, 醉经有朋友介绍我一些小型外包项目, 是给一些私营业主做网站, 我想也不是太难的事情, 对自己也是一个很好的锻炼, 所以最近我都在开发中.”
     “哈, 看来小菜有外快赚了, 又锻炼了自己的技术, 好事啊.”
     “现在不是想象的那么简单, 刚开始是办一个客户做一个产品展示的网站, 我花了一个多星期做好了, 也帮他租用了虚拟空间, 应该说都很顺利.”
     “嗯, 产品展示的网站, 这个应该不难实现.”
     “而后, 他另外的朋友想想做相似的网站, 我想这有何难, 在租用一个空间, 然后把代码复制一份, 然后上传就可以了.”
     “哦, 这好像有点问题, 后来呢.”
     “实际上却是他们的朋友都希望我来提供这样的网站, 但要求就不太一样了, 有人希望是新闻发布形式的, 有人希望是博客形式的, 也有还是原来的产品图片加说明形式的, 而且他们都希望能在费用上大大降低,. 可是每个网站都租用一个空间, 费用降低是不太可能的, 我在想如何办呢?”
     “他们是不是都是类似的商家客户? 要求也就是信息发布, 产品展示, 博客留言, 论坛等功能?”
     “是呀, 要求差别不大, 你说该如何办?” 小菜问道.
     “你的担心是对的, 如果有100家企业来找你去做网站, 你难道去申请100个空间, 建100个数据库, 然后用类似的代码复制100份, 去实现吗?”
     “啊, 那如果有bug或是新的需求改动, 那维护量就太可怕了.”
     “先来看看你的做法, 如果每个网站一个实例, 那么代码应该是这样的.”

     网站类

1
2
3
4
5
6
7
8
9
10
11
12
// 网站
public class WebSite {
private String name = "";
public WebSite(String name) {
this.name = name;
}
public void use() {
System.out.println("网站分类: " + name);
}
}

     客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
WebSite fx = new WebSite("产品展示");
fx.use();
WebSite fy = new WebSite("产品展示");
fy.use();
WebSite fz = new WebSite("产品展示");
fz.use();
WebSite fl = new WebSite("博客");
fl.use();
WebSite fm = new WebSite("博客");
fm.use();
WebSite fn = new WebSite("博客");
fn.use();
}

     “对的, 也就是说如果要做三个产品展示, 三个博客网站, 就需要六个网站类的实例, 而其实他们本质上都是一样的代码, 如果网站增多, 实例也就随着增多, 这对服务器的资源浪费很严重. 小菜, 你说有什么办法解决这个问题.”
     “我不知道, 我想过大家的网站共用一套代码, 但毕竟是不同的网站, 数据都不相同.”
     “我就是希望你说出共享代码这句话, 为什么不可以呢? 比如现在大型的博客网站, 电子商务网站, 里面没一个博客和商家也都是一个小网站, 他们是如何做的?”
     “啊, 我明白了, 利用用户ID 的不同, 来区分不同的用户, 具体数据的模板可以不同, 但是代码核心和数据库可以共享.”
     “小才有开窍了, 项目多也别傻做啊. 你想, 首先你的这些企业客户, 他们需要的网站的相似程度都很高, 而且都不是高访问量的那种网站, 如果分成多个虚拟空间来处理, 相当于相同网站的实例有很多, 这就造成服务器资源的大量浪费, 当然更实际的就是钞票的浪费, 如果整合到一个网站中, 共享核心代码和数据, 那么对于硬盘, CPU, 内存, 数据库空间等服务器资源可以达到共享, 减少服务器资源, 而对于代码, 由于是一份实例, 维护和扩展都更加容易.”
     “是的. 那如何做到共享一份实例呢?”

26.2 享元模式(Fly-Weight)

     “哈, 在弄明白如何共享代码之前, 我们先来看看一个设计模式 — 享元模式.”

享元模式(FlyWeight), 运用共享技术有效的支持大量细颗粒度的对象.



     Flyweight 类, 它是所有享元类的超类或接口, 通过这个接口, Flyweight 可以接受并作用于外部状态.

1
2
3
public abstract class Flyweight {
public abstract void operation(int extrinsicState);
}

     ConcreteFlyweight 是继承 Flyweight超类 或实现 Flyweight 接口, 并为内部状态增加存储空间.

1
2
3
4
5
6
public class ConcreteFlyweight extends Flyweight {
@Override
public void operation(int extrinsicState) {
System.out.println("具体 Flyweight: " + extrinsicState);
}
}

     UnsharedConcreteFlyweight 指那些不需要共享的 Flyweight 子类. 因为 Flyweight 共享成为可能, 但是他不强制共享.

1
2
3
4
5
6
public class UnsharedConcreteFlyweight extends Flyweight {
@Override
public void operation(int extrinsicState) {
System.out.println("不共享的具体 Flyweight: " + extrinsicState);
}
}

     FlyweightFactory 享元工厂, 用来创建并管理 Flyweight 对象. 主要是确保合理的共享 Flyweight 对象, 当一个用户请求 Flyweight 时, FlyweightFactory 提供一个已创建的实例, 或者创建一个(如果不存在的话)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class FlyweightFactory {
private Hashtable<String, Flyweight> flyweights = new Hashtable<>();
// 初始化工厂时, 先生成三个实例
public FlyweightFactory() {
flyweights.put("X", new ConcreteFlyweight());
flyweights.put("Y", new ConcreteFlyweight());
flyweights.put("Z", new ConcreteFlyweight());
}
public Flyweight getFlyweight(String key) {
return flyweights.get(key);
}
}

     客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
int extrinsicState = 22;
FlyweightFactory f = new FlyweightFactory();
Flyweight fx = f.getFlyweight("X");
fx.operation(--extrinsicState);
Flyweight fy = f.getFlyweight("Y");
fy.operation(--extrinsicState);
Flyweight fz = f.getFlyweight("Z");
fz.operation(--extrinsicState);
Flyweight uf = new UnsharedConcreteFlyweight();
uf.operation(--extrinsicState);
}

     结果表示

1
2
3
4
具体 Flyweight: 21
具体 Flyweight: 20
具体 Flyweight: 19
不共享的具体 Flyweight: 18

     “大鸟, 有个问提,” 小菜问道, “FlyweightFactory 是根据客户需求返回早已生成好的对象, 但一定要事先生成对象实例吗?”
     “问得好, 实际上是不需要的, 完全可以初始化时什么也不做, 到需要时, 再去判断对象是否为null 再来决定是否初始化.”
     “还有个问题, 为什么要有 UnsharedConcreteFlyweight 存在呢?”
     “这是因为尽管我们大部分时间都需要共享对象来降低内存的损耗, 但个别时候也是可能不需要共享的, 那么此时 UnsharedConcreteFlyweight 就有存在的必要了, 他可以解决那些不需要共享对象的问题.”

26.3 网站共享代码

     “好了, 你试着参照这个样例来改写一下帮别人做网站的代码.” 大鸟说到.
     “哦, 好的, 那这样的话, 网站应该有一个抽象类和具体网站类才可以, 然后通过网站工厂来生成对象, 我马上去写.”
     半小时后, 小菜的第二版代码.
     网站抽象类

1
2
3
public abstract class WebSite {
public abstract void use();
}

     具体网站类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ConcreteWebsite extends WebSite {
private String name;
public ConcreteWebsite(String name) {
this.name = name;
}
@Override
public void use() {
System.out.println("网站分类: " + name);
}
}

     网站工厂类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class WebsiteFactory {
private Hashtable<String, WebSite> flyweights = new Hashtable<>();
// 获得网站分类
public WebSite getWebsiteCategory(String key) {
if (!flyweights.contains(key)) {
flyweights.put(key, new ConcreteWebsite(key));
}
return flyweights.get(key);
}
// 获得网站分类总数
public int getWebSiteCount() {
return flyweights.size();
}
}

     客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) {
WebsiteFactory f = new WebsiteFactory();
WebSite fx = f.getWebsiteCategory("产品展示");
fx.use();
WebSite fy = f.getWebsiteCategory("产品展示");
fy.use();
WebSite fz = f.getWebsiteCategory("产品展示");
fz.use();
WebSite fl = f.getWebsiteCategory("博客");
fl.use();
WebSite fm = f.getWebsiteCategory("博客");
fm.use();
WebSite fn = f.getWebsiteCategory("博客");
fn.use();
}

     结果显示

1
2
3
4
5
6
网站分类: 产品展示
网站分类: 产品展示
网站分类: 产品展示
网站分类: 博客
网站分类: 博客
网站分类: 博客

     “这样写基本上算是实现了享元模式共享对象的目的, 也就是说, 不管几个网站, 只要是产品展示, 都是一样的, 只要是 ‘博客’ 都是相通的, 但是这样也是有问题的, 你给企业建网站不是一家企业, 他们的数据会有不同, 所以他们至少应该有不同的账号, 你怎么办.”
     “啊, 对的, 我这样写没有体现对象间的不同, 只是体现了他们的共享部分.”

26.4 内部状态与外部状态

     “在享元对象内部, 并且不会随着环境改变而改变的共享部分, 可以称为是享元对象的内部状态, 而随着环境改变而改变的, 不可以共享的状态就是外部状态了. 事实上, 享元模式可以避免大量的非常相似的开销. 在程序设计中, 有时需要生成大量细颗粒度的类实例来表示数据. 如果发现这几个实例除了几个参数外基本全是相同的, 有时候就能大幅度较少需要实例化的类的数量. 如果能把这些参数移到类实例外面, 在方法调用时将他们传递进来, 就可以通过共享大幅度减少实例的数目. 也就是说, 享元模式 Flyweight 执行时所需要的状态是是有内部的也可能是有外部的, 内部状态存储于 ConcreteFlyweight 对象之中, 而外部状态则应当考虑有客户端对象存储或计算, 当调用 Flyweight 对象操作时, 将该状态传递给他.”
     “那你的意思是, 账号是外部状态, 应该有专门的类来处理.”
     “来, 你试试看.”
     大约20分钟后, 小才写出第三版代码.



     用户类, 用于网站的客户账号, 是 ‘网站类’ 的外部状态.

1
2
3
4
5
6
7
8
9
10
11
public class User {
private String name;
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
}

     网站抽象类.

1
2
3
4
public abstract class WebSite {
// '使用' 方法需要传递 '用户' 对象
public abstract void use(User user);
}

     具体网站类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ConcreteWebsite extends WebSite {
private String name;
public ConcreteWebsite(String name) {
this.name = name;
}
@Override
public void use(User user) {
// 实现 'user' 方法
System.out.println("网站分类: " + name + "用户: " + user.getName());
}
}

     网站工厂类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 网站工厂
public class WebsiteFactory {
private Hashtable<String, WebSite> flyweights = new Hashtable<>();
// 获得网站分类
public WebSite getWebsiteCategory(String key) {
if (!flyweights.contains(key)) {
flyweights.put(key, new ConcreteWebsite(key));
}
return flyweights.get(key);
}
// 获得网站分类总数
public int getWebSiteCount() {
return flyweights.size();
}
}

     客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) {
WebsiteFactory f = new WebsiteFactory();
WebSite fx = f.getWebsiteCategory("产品展示");
fx.use(new User("小菜"));
WebSite fy = f.getWebsiteCategory("产品展示");
fy.use(new User("大鸟"));
WebSite fz = f.getWebsiteCategory("产品展示");
fz.use(new User("娇娇"));
WebSite fl = f.getWebsiteCategory("博客");
fl.use(new User("老顽童"));
WebSite fm = f.getWebsiteCategory("博客");
fm.use(new User("桃谷六仙"));
WebSite fn = f.getWebsiteCategory("博客");
fn.use(new User("南海鳄神"));
System.out.println("得到网站分类总数: " + f.getWebSiteCount());
}

     结果显示, 尽管有六个不同的网站, 但实际上只有两个网站的实例.

1
2
3
4
5
6
7
网站分类: 产品展示用户: 小菜
网站分类: 产品展示用户: 大鸟
网站分类: 产品展示用户: 娇娇
网站分类: 博客用户: 老顽童
网站分类: 博客用户: 桃谷六仙
网站分类: 博客用户: 南海鳄神
得到网站分类总数: 2

     “哈, 写得非常好, 这样可以协调内部与外部状态了. 由于用了享元模式, 哪怕你接受的1000个网站的需求, 只要要求相同或类似, 你的实际开发代码也就是分类那几种, 对于服务器来说, 占用的资源, CPU, 硬盘都是非常少的, 这确实是一种很好的方式.”

26.5 享元模式应用

     “大鸟, 你通过这个例子讲解享元模式我虽然理解了, 但在现实中什么时候才应该考虑使用享元模式呢?”
     “就知道你会问这样的问题, 如果一个应用程序使用了大量的对象, 而大量的这些对象造成了存储开销时就应该考虑使用; 还有就是对象的大多数状态可以外部状态, 如果删除对象的外部状态, 那么就可以用相对较少的共享对象取代很多组对象, 此时可以考虑使用享元模式.
     “在实际应用中, 享元模式可以达到什么样的效果呢?”
     “因为用了享元模式, 所以有了共享对象, 实例总数就大大减少了, 如果共享对象越多, 那存储细节就越多, 节约量随着共享状态的增多而增大.”
     “能具体一些吗, 有些什么情况用到享元模式的.”
     “哈, 实际在 JAVA 中, 字符串 String 就是用到了 Flyweight 模式. 举个例子吧. objB.equal(Object objA) 方法是用来确定objA 与 objB 是否相同的实例, 返回值为 bool 值.”

1
2
3
String titleA = "大话设计模式";
String titleB = "大话设计模式";
System.out.println(titleA.equals(titleB));

     “啊, 返回值竟然是 true, 这两个字符串是相同的实例.”
     “试想一下, 如果每次创建字符串时, 都需要创建一个新的字符串的话, 内存的开销会很大. 所以如果第一次创建了字符串 titleA, 下次再创建相同的字符串 titleB 只是把他的引用指向 ‘大话设计模式’, 这样就实现了 ‘大话设计模式’ 在内存中的共享.”
     “哦, 原来我一直在用享元模式呀, 之前我都不知道. 还有没有其他现实中的应用呢?”
     “虽说享元模式更多的时候是一种低层的设计模式, 但现实中也有应用. 比如游戏开发中, 像围棋, 五子棋, 跳棋等, 他们都有大量的棋子对象, 你分析一下, 他们的内部状态和外部状态都是什么?”
     “围棋和五子棋只有黑白两色, 跳棋颜色略多一下, 但是也不太变化, 所以颜色应该是棋子的内部状态, 而各个棋子之间的差别主要是位置不同, 所以方位坐标应该是棋子的外部状态.”
     “对的, 像围棋, 一盘棋理论上可以有361个空位可以放棋子, 那如果用常规的面向对象的方式编程, 每盘棋都可能有二三百个棋子对象产生, 一台服务器就很难支持更多的玩家玩围棋游戏了, 毕竟内存空间还是有限的. 如果用享元模式来处理棋子, 那么棋子可以减少到只有两个实例, 结果—你应该明白.”
     “太了不起了, 这确实分非常好的解决了对象开销的问题.”
     “在某些情况下, 对象的数量可能会太多, 从而导致运行时的资源与性能损耗. 那么我们如何去避免大量细颗粒度的对象, 同时又不影响客户端程序, 是一个值得思考的问题, 享元模式, 可以运用共享技术有效的支持大量细颗粒度的对象. 不过, 你也别高兴得太早, 使用享元模式需要维护一个记录了系统所有已使用的享元的列表, 而这本身需要耗费资源, 另外享元模式使得系统更加复杂. 为了是对象可以共享, 需要将一些状态外部化, 这时的程序逻辑复杂化. 应当在有足够多的对象实例可供共享时, 在考虑使用享元模式.”
     “哦, 明白了, 我给人家做网站, 如果只是两三个人的博客网站, 我没有必要考虑太多. 但是如果要开发一个可供多人注册的博客网站, 那么用共享代码的方式是一个非常不错的选择.”
     “小菜, 说了这么多, 你网站赚到了钱是不是应该报答我一下呀.”
     “哈, 如果完成开发后客户非常满意, 我一定…我一定….”
     “一定什么? 怎么这么不爽快…”
     “我一定送你一个博客账号….”
     “啊!!!!”

~感谢捧场,您的支持将鼓励我继续创作~