经典伴读_GOF设计模式_结构型模式

经典伴读系列文章,不是读书笔记,自己的理解加上实际项目中运用,旨在5天读懂这本书。如果这篇文章对您有些用处,请点赞告诉我O(∩_∩)O。
在这里插入图片描述

结构型模式

GOF中23种设计模式从用途上分为三类,第二类是结构型模式,描述的是如何组合类和对象以获得更大的结构。

Adapter适配器

将一个类的接口转换成客户希望的另外一个接口。 使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

什么是适配?就是消除差异。如在已有的订单服务中,添加多平台订单来源(淘宝,京东),而淘宝,京东的订单结构肯定和已有的订单结构不同,这时候需要使用适配器模式。
在这里插入图片描述

	//已有系统订单
	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桥接

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

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组合

将对象组合成树形结构以表示“部分-整体”的层次结构。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("<MenuLeaf>");
            builder.append("<id>").append(id).append("</id>");
            builder.append("<name>").append(name).append("</name>");
            builder.append("<pid>").append(pid).append("</pid>");
            builder.append("<url>").append(url).append("</url>");
            builder.append("</MenuLeaf>");
            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("<MenuDirectory>");
            builder.append("<id>").append(id).append("</id>");
            builder.append("<name>").append(name).append("</name>");
            builder.append("<pid>").append(pid).append("</pid>");
            builder.append("<children>");
            for (MenuItem menuItem : children) {
                builder.append(menuItem.print());
            }
            builder.append("</children>");
            builder.append("</MenuDirectory>");
            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装饰器

动态地给一个对象添加一些额外的职责。就增加功能来说, 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外观

为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

外观模式是最少知识原则的应用,是常见的设计准则,即尽量减少外部依赖,没有固定形式,这里说几种常见场景,如:
(1)系统内部分层,一个Service封装了多个Dao或其他Service,外部调用时只知道外层Service,那么外层Service就是Facade。
(2)系统与系统之间通信,所有提供给外部系统的接口封装到Api类中,所有调用外部系统的接口先封装到Client类中,再给系统内部调用。那么Api类是对外Facade,Client类是对内Facade。
(3)当做一些工具代码时,如代码生成器等,无论内部逻辑多么复杂,多少配置,有多少类,外部都只会提供一个生成类,其中有多种带参数的生成方法 ,那么外部生成类就是Facade。

GOF认为一个子系统只需要一个Facade对象,并且是单例。Facade模式初衷是用于系统与系统之间的交互,这么看上面的第2个场景应该是最符合。

Flyweight享元

运用共享技术有效地支持大量细粒度的对象。

网上有些文章这样解释享元,享元就是共享对象,用一个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等封装类型时不要使用==。

Proxy代理

为其他对象提供一种代理以控制对这个对象的访问。

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支持,这已经不是设计模式的范畴。
最后说回代理模式,它的代码形式和装饰器模式基本一致,只能依靠用途区分,添加新功能就是装饰器模式,需要对方法访问控制就是代理模式。

设计模式重意不重形,未完待续

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值