经典伴读系列文章,想写的不是读书笔记,也不是读后感,自己的理解加上实际项目中运用,让大家4,5天读懂这本书
预备知识:
GOF中23种设计模式从用途上分为三类,第二类是结构型模式,描述的是如何组合类和对象以获得更大的结构。
将一个类的接口转换成客户希望的另外一个接口。 使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
什么是适配?就是消除差异。如在已有的订单服务中,添加多平台订单来源(淘宝,京东),而淘宝,京东的订单结构肯定和已有的订单结构不同,这时候需要使用适配器模式。
//已有系统订单
public static class MyOrder {
private String orderId; //订单编号
private Date createTime; //创建时间
...
//淘宝订单,字段意义相同,名称不同,需要适配
public static class TBOrder {
private String oid; //订单编号
private Date ct; //创建时间
...
//已有系统订单服务
interface MyOrderService {
void saveOrder(MyOrder myOrder); //保存订单
}
public static class MyOrderServiceImpl implements MyOrderService{
@Override
public void saveOrder(MyOrder myOrder) {
System.out.println("保存订单" + myOrder);
}
}
//淘宝订单服务
interface TaobaoOrderService {
void saveTBOrder(TBOrder tbOrder); //保存订单
}
//通过继承的方式适配
public static class OrderServiceAdapter
extends MyOrderServiceImpl implements TaobaoOrderService {
@Override
public void saveTBOrder(TBOrder tbOrder) {
MyOrder myOrder = new MyOrder();
myOrder.setOrderId(tbOrder.getOid());
myOrder.setCreateTime(tbOrder.getCt());
saveOrder(myOrder);
}
}
public static void main(String[] args) {
//测试订单
TBOrder tbOrder = new TBOrder();
tbOrder.setOid("1001");
tbOrder.setCt(new Date());
TaobaoOrderService taobaoOrderService = new OrderServiceAdapter();
taobaoOrderService.saveTBOrder(tbOrder);
}
注意:对照类图,我们已有的订单服务MyOrderServiceImpl称为Adaptee,需要适配的目标接口TaobaoOrderService称为Target,中间的OrderServiceAdapter就是Adapter。
//通过继承的方式适配
public static class OrderServiceAdapter
extends MyOrderServiceImpl implements TaobaoOrderService
适配器模式除了通过继承实现,还可以使用对象组合的方式,前者称为类适配器,后者称为对象适配器。java只能继承一个类,因此少用继承,多用对象组合。如一个Adapter对应多个Adaptee的情况。
//通过对象组合的方式适配
public static class OrderServiceAdapter2 implements TaobaoOrderService {
private MyOrderService myOrderService;
public OrderServiceAdapter2() {
this.myOrderService = new MyOrderServiceImpl(); //实际项目中由Spring注入
}
@Override
public void saveTBOrder(TBOrder tbOrder) {
MyOrder myOrder = new MyOrder();
myOrder.setOrderId(tbOrder.getOid());
myOrder.setCreateTime(tbOrder.getCt());
myOrderService.saveOrder(myOrder);
}
}
JDK中最常见的适配器是InputStreamReader,OutputStreamWriter,将字节流适配为字符流,它们的适配目标Target都不是接口,而是类Reader和Writer,因此只能通过对象组合实现适配。
将抽象部分与它的实现部分分离,使它们都可以独立地变化
Bridge桥接模式,平时业务中很少能见到,因为这不只是一种模式,无法直接套用,而是一种设计系统的思路和规则。
先来看下GOF中桥接的例子(有修改,保留原意),开发一个C端的GUI的界面库,包含两种窗口,带标题的窗口TitleWindow,和不带标题的窗口NoTitleWindow,每种窗口在Windows X(简称X系统)和 Linux(简称L系统)中都可以正常使用。要设计一套这样的界面库,初版设计可能是这样:
我们发现如果要开发10种窗口,那么至少需要20种类(每一种窗口都要匹配两个系统),这时就需要重新设计了,首要任务就是把和系统直接相关的代码(画线、画文字)抽离出来独立成实现类WindowImp。接着将画窗口操作根据系统实现类WindowImp中已有的方法进一步拆分,如画标题和画矩形框,封装到Window层。要求WIndow子类对WindowImp无感,也就是窗口子类根本不知道需要系统匹配这件事。
//依赖系统的具体实现,可以是接口也可以是抽象类
interface WindowImp {
void drawLineByDev();
void drawTextByDev();
}
public static class XWindowImpl implements WindowImp {
@Override
public void drawLineByDev() {
System.out.println("WindowX系统画直线");
}
@Override
public void drawTextByDev() {
System.out.println("WindowX系统画文字");
}
}
public static class LWindowImpl implements WindowImp {
@Override
public void drawLineByDev() {
System.out.println("Linux系统画直线");
}
@Override
public void drawTextByDev() {
System.out.println("Linux系统画文字");
}
}
//将所有和WindowImpl的操作封装在Window层
//Window子类不能感知WindowImp
public static class Window {
private WindowImp imp;
public Window(WindowImp imp) {
this.imp = imp;
}
public void drawText() {
imp.drawTextByDev();
}
public void drawRect() {
//一个矩形由四条线组成
imp.drawLineByDev();
imp.drawLineByDev();
imp.drawLineByDev();
imp.drawLineByDev();
}
}
public static class TitleWindow extends Window {
public TitleWindow(WindowImp imp) {
super(imp);
}
public void drawWindow() {
drawText(); //画标题
drawRect();
}
}
public static class NoTitleWindow extends Window{
public NoTitleWindow(WindowImp imp) {
super(imp);
}
public void drawWindow() {
drawRect();
}
}
public static void main(String[] args) {
//调用方决定使用哪个个系统的API
TitleWindow titleWindow = new TitleWindow(new XWindowImpl());
titleWindow.drawWindow();
NoTitleWindow noTitleWindow = new NoTitleWindow(new LWindowImpl());
noTitleWindow.drawWindow();
}
将具体的系统实现从应用代码中分离,这才是GOF初衷,减少所需类的数量只是附带效果。很明显桥接模式对场景要求比较严苛,开发人员需要有较高的抽象能力,更多的应该出现在框架代码中。
将对象组合成树形结构以表示“部分-整体”的层次结构。Composite使得用户对单个对象和组合对象的使用具有一致性。
组合模式并不是“多用组合少用继承”中的组合,更像是android中的View和ViewGroup的关系,一个布局中可以放入一个文本,也可以继续放一个子布局,一直往下,就是一个树形结构,当我们需要渲染这棵树时,只需要将根节点渲染即可。这类明显带有递归色彩的场景,为了不必区分子节点类型,可以使用组合模式。
实际web项目中,组合模式最常见的场景是菜单展示。菜单本身是一个树状结构,节点类型是目录或叶子(带链接)。但在数据库中却是二维表形式存储,取出则是列表,此时需要使用递归构造树,并使用组合模式渲染树。(这里渲染的结果是xml格式,也可以是json等)
(1)按照组合模式构建树节点DTO
public static class MenuItem {
protected int id;
protected String name;
protected int pid;
public MenuItem(int id, String name, int pid) {
this.id = id;
this.name = name;
this.pid = pid;
}
//渲染
public String print() {
return "";
}
}
//叶子节点
public static class MenuLeaf extends MenuItem{
private String url;
public MenuLeaf(int id, String name, int pid, String url) {
super(id, name, pid);
this.url = url;
}
@Override
public String print() {
StringBuilder builder = new StringBuilder();
builder.append("" );
builder.append("" ).append(id).append("");
builder.append("" ).append(name).append("");
builder.append("" ).append(pid).append("");
builder.append("" ).append(url).append("");
builder.append("");
return builder.toString();
}
}
//目录节点
public static class MenuDirectory extends MenuItem {
private List<MenuItem> children = new ArrayList<>();
public MenuDirectory(int id, String name, int pid) {
super(id, name, pid);
}
public void addItem(MenuItem menuItem) {
children.add(menuItem);
}
public void removeItem(MenuItem menuItem) {
children.remove(menuItem);
}
@Override
public String print() {
StringBuilder builder = new StringBuilder();
builder.append("" );
builder.append("" ).append(id).append("");
builder.append("" ).append(name).append("");
builder.append("" ).append(pid).append("");
builder.append("" );
for (MenuItem menuItem : children) {
builder.append(menuItem.print());
}
builder.append("");
builder.append("");
return builder.toString();
}
}
(2)递归构建树
public static MenuItem buildTree(MenuItem current, List<MenuItem> menuItems) {
if (current instanceof MenuDirectory) {
menuItems.stream()
.filter(item -> item.pid == current.id)
.forEach(item -> {
((MenuDirectory) current).addItem(buildTree(item, menuItems));
});
}
return current;
}
(3)从数据库中查询菜单表测试数据,构建树并渲染。
//数据库中菜单表PO
public static class MenuItemPO {
private int id;
private int type; //0目录,1菜单
private String name;
private int pid;
private String url;
public MenuItemPO(int id, int type, String name, int pid, String url) {
this.id = id;
this.type = type;
this.name = name;
this.pid = pid;
this.url = url;
}
......
//模拟数据库中菜单数据
public static List<MenuItemPO> getMenuItems() {
List<MenuItemPO> menuItems = new ArrayList<>();
menuItems.add(new MenuItemPO(1, 0, "目录1", 0, ""));
menuItems.add(new MenuItemPO(2, 1, "菜单项a", 1, "http://菜单项a"));
menuItems.add(new MenuItemPO(3, 1, "菜单项b", 1, "http://菜单项b"));
menuItems.add(new MenuItemPO(4, 0, "目录2", 1, ""));
menuItems.add(new MenuItemPO(5, 1, "菜单项c", 4, "http://菜单项c"));
menuItems.add(new MenuItemPO(6, 1, "菜单项d", 0, "http://菜单项d"));
return menuItems;
}
//测试
public static void main(String[] args) {
List<MenuItemPO> menuItemPOS = getMenuItems(); //获取数据库中菜单数据
List<MenuItem> menuItems = menuItemPOS.stream() //PO转DTO
.map(po -> po.getType() == 0 ?
new MenuDirectory(
po.getId(),
po.getName(),
po.getPid()) :
new MenuLeaf(
po.getId(),
po.getName(),
po.getPid(),
po.getUrl())).collect(Collectors.toList());
MenuDirectory root = new MenuDirectory(0, "root", -1); //根节点
root = (MenuDirectory) buildTree(root, menuItems); //构建树
String str = root.print(); //渲染树
System.out.println(str);
}
输出的xml格式化:
另外,有的文章中说文件和文件夹也是一种组合模式,没错,但java中的File类不是,它只是文件路径的抽象。An abstract representation of file and directory pathnames.
动态地给一个对象添加一些额外的职责。就增加功能来说, Decorator模式相比生成子类更为灵活。
GOF想要给TextView添加滚动条,字体变粗。优先想到的肯定是使用继承,即ScrollTextView和BorderTextView,但这种方式明显不够灵活,如果既要滚动条又要字体变粗,是不是得再来一个ScrollBorderTextView。又或者加下划线,变粗加下划线,加边框两次等等,当需要给已有的类添加功能时,除了继承,还可以将额外功能做成壳,想要哪个套哪个,这就是更加灵活的装饰器模式。
//View,TextView省略.....
//抽象装饰器
public static class Decorator extends View {
protected View component;
public Decorator(View component) {
this.component = component;
}
@Override
public void draw() {
component.draw();
}
}
//具体装饰器
public static class BorderDecorator extends Decorator {
private int borderWidth;
public BorderDecorator(View component, int borderWidth) {
super(component);
this.borderWidth = borderWidth;
}
private void drawBorder(int width) {
System.out.println("drawBorder, width=" + width);
}
@Override
public void draw() {
super.draw(); //先渲染文字
drawBorder(borderWidth); //在渲染边框
}
}
public static void main(String[] args) {
BorderDecorator borderDecorator =
new BorderDecorator(new TextView("hello"), 2);
borderDecorator.draw();
}
实际使用时,不可能像上面这样"工整",可能没有抽象Decorator,但只要是在造壳子,都可以认为是装饰器模式,如JDK中BufferedReader:
BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream(
"/Users/flyzing/Downloads/data.txt")));
看到这里嵌套了三层从内到外,InputStreamReader嵌套FileInputStream使用的是对象适配器模式,将字节流适配到字符流,BufferedReader嵌套InputStreamReader使用的是装饰器模式,给字符流增加缓冲。
为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
外观模式是最少知识原则的应用,是常见的设计准则,即尽量减少外部依赖,没有固定形式,这里说几种常见场景,如:
(1)系统内部分层,一个Service封装了多个Dao或其他Service,外部调用时只知道外层Service,那么外层Service就是Facade。
(2)系统与系统之间通信,所有提供给外部系统的接口封装到Api类中,所有调用外部系统的接口先封装到Client类中,再给系统内部调用。那么Api类是对外Facade,Client类是对内Facade。
(3)当做一些工具代码时,如代码生成器等,无论内部逻辑多么复杂,多少配置,有多少类,外部都只会提供一个生成类,其中有多种带参数的生成方法 ,那么外部生成类就是Facade。
GOF认为一个子系统只需要一个Facade对象,并且是单例。Facade模式初衷是用于系统与系统之间的交互,这么看上面的第2个场景应该是最符合。
运用共享技术有效地支持大量细粒度的对象。
网上有些文章这样解释享元,享元就是共享对象,用一个HashMap预先存储对象,需要时从中获取。不不不,享元并不简单。
(首先是共享模式的引入,这里做简化,觉得字多的同学可以跳过第1段)
1、GOF又从文档编辑器入手,要创建一个文档编辑器,其中最多的不是行,不是列,而是每个字符,每个字符都可以拥有不同的格式,这就意味着每个字符都是一个对象。写一篇1万字符的文章就同时有1万个字对象在内存中。这可不是好兆头。为了减少对象数量,将字中状态加以区分,可以共享的状态,如文本(只有26种,别抬杠,GOF肯定不是指汉字),以及不能共享的状态,如字体、颜色、大小等各种格式。将可以共享的状态单独变为享元对象(Flyweight),以共享状态为key存放到池中。将不能共享的状态单独变为上下文对象(Context),以字符索引为key构建一个BTree,不是每个字符都有格式,这棵格式树不会太大。渲染字符时,根据字符文本(共享状态)从池中取出享元对象,然后再从格式树中检索出非共享状态,加在一起就可以渲染出一个字符。
2、原型模式可以减少类的数量,享元模式可以减少对象的数量。当我们有大量相似对象存在内存中时,可以使用享元模式。共享模式的关键是区分享元对象的内部状态和外部状态。
(1)内部状态intrinsicState,是可以共享的信息,并且不可变。如字符的文本,无论有多少字符的文章(英文),字符的文本只有26个。
(2)外部状态extrinsicState,是不可以共享的信息,随着外部环境改变而改变,如字符的格式,随着字符索引的变化,可能有不同的大小、颜色、字体。
(3)内部状态随着享元对象一起存入池中(如HashMap),外部状态需要客户端自己存储,
interface Flyweight {
void operation(String extrinsicState);
}
public static class ConcreteFlyWeight implements Flyweight{
private final String intrinsincSate; //内部状态初始化后不能修改
public ConcreteFlyWeight(String intrinsincSate) {
this.intrinsincSate = intrinsincSate;
}
@Override
public void operation(String extrinsicState) { //外部状态由客户端传入,可以改变
System.out.println("内部状态:" + intrinsincSate + ",外部状态:" + extrinsicState);
}
}
public static class FlyWeightFactory {
private final static Map<String, Flyweight> POOL = new HashMap<>();
public static Flyweight getFlyWeight(String intrinsincSate) {
Flyweight flyweight = POOL.get(intrinsincSate);
if (flyweight == null){
synchronized (POOL) { //放入池中,需要加锁
flyweight = POOL.get(intrinsincSate);
if (flyweight == null) {
flyweight = new ConcreteFlyWeight(intrinsincSate);
}
POOL.put(intrinsincSate, flyweight);
}
}
return flyweight;
}
}
public static void main(String[] args) {
Flyweight a = FlyWeightFactory.getFlyWeight("a");
a.operation("字体=宋体,颜色=红色,大小=24");
System.out.println(a);
a = FlyWeightFactory.getFlyWeight("a");
a.operation("字体=黑体,颜色=黑色,大小=12");
System.out.println(a);
}
输出:
内部状态:a,外部状态:字体=宋体,颜色=红色,大小=24
com.example.learn.ConcreteFlyWeight@27973e9b
内部状态:a,外部状态:字体=黑体,颜色=黑色,大小=12
com.example.learn.ConcreteFlyWeight@27973e9b
由此可以看出对象和内部状态都没变,外部状态却不同,这就是享元模式。
3、实际项目中的享元可以简单得多,如JDK中的享元Integer。Integer自动装箱时调用的是Integer.valueOf(int i)方法,默认情况下当i在-128到127之间时,返回值从IntegerCache中取,否则创建新的Integer对象。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
测试代码
public static void main(String[] args) {
Integer a = 127;
Integer b = 127;
System.out.println(a == b); //true
a = 128;
b = 128;
System.out.println(a == b); //false
类加载时,预先在IntegerCache加入的-128到127个Integer就是享元对象。因此,比较Integer等封装类型时不要使用==。
为其他对象提供一种代理以控制对这个对象的访问。
GOF还是从文档编辑器入手,当我们打开一个包含数个大图片的长文档时,为了能够迅速展开页面,我们需要先将图片按照文件长宽预占位置,等到滚动到需要图片渲染的位置,再加载图片。当需要控制某个对象的方法调用时,需要使用代理模式。
interface View {
void draw(); //渲染
}
//模拟图片
public static class Image {
private String fileName;
public Image(String fileName) {
this.fileName = fileName;
}
}
public static class ImageView implements View {
private Image imageImpl;
public ImageView(String fileName) {
imageImpl = load(fileName); //创建ImageView时加载图片
}
@Override
public void draw() {
System.out.println("draw " + imageImpl);
}
//从磁盘加载图片
private Image load(String fileName) {
System.out.println("load " + fileName);
return new Image(fileName);
}
}
//代理
public static class ImageViewProxy implements View {
private ImageView imageView;
private String fileName;
public ImageViewProxy(String fileName) {
this.fileName = fileName; //创建代理时不加载图片
}
@Override
public synchronized void draw() {
if (imageView == null) { //渲染时加载图片
imageView = new ImageView(fileName);
}
imageView.draw();
}
}
public static void main(String[] args) {
View imageProxy = new ImageViewProxy("BigImage.jpg");
imageProxy.draw();
}
是否加载图片,什么时间加载图片都是代理类控制,这就是代理模式。实际项目中代理模式体现形式比较单一,都是在调用某个对象方法前后加点东西,如AOP,过滤器,拦截器等。
interface Subject {
void request();
}
public static class RealSubject implements Subject {
@Override
public void request() {
System.out.println("RealSubject.request()");
}
}
public static class Proxy implements Subject{
private RealSubject realSubject;
public Proxy(RealSubject realSubject) {
this.realSubject = realSubject;
}
@Override
public void request() {
System.out.println("判断是否有权限访问");
realSubject.request();
System.out.println("记录访问日志");
}
}
public static void main(String[] args) {
//Proxy类代替RealSubject
Subject proxy = new Proxy(new RealSubject());
proxy.request();
}
上例中为每一个RealSubject创建一个Proxy类的方式称为静态代理,如果不止一个类需要访问控制时,需要使用动态代理,如需要对所有Service类添加事前的权限判断和事后的日志记录。动态代理JDK和CGLIB中都有API支持,这已经不是设计模式的范畴。
最后说回代理模式,它的代码形式和装饰器模式基本一致,只能依靠用途区分,添加新功能就是装饰器模式,需要对方法访问控制就是代理模式。
设计模式重意不重形,未完待续