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

  • 如何使用设计模式抽象实例化过程。请参考《经典伴读_GOF设计模式_创建型模式》

结构型模式

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{@Overridepublic void saveOrder(MyOrder myOrder) {System.out.println("保存订单" + myOrder);}}//淘宝订单服务interface TaobaoOrderService {void saveTBOrder(TBOrder tbOrder); //保存订单}//通过继承的方式适配public static class OrderServiceAdapterextends MyOrderServiceImpl implements TaobaoOrderService {@Overridepublic 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 OrderServiceAdapterextends MyOrderServiceImpl implements TaobaoOrderService

适配器模式除了通过继承实现,还可以使用对象组合的方式,前者称为类适配器,后者称为对象适配器。java只能继承一个类,因此少用继承,多用对象组合。如一个Adapter对应多个Adaptee的情况。

 //通过对象组合的方式适配public static class OrderServiceAdapter2 implements TaobaoOrderService {private MyOrderService myOrderService;public OrderServiceAdapter2() {this.myOrderService = new MyOrderServiceImpl(); //实际项目中由Spring注入}@Overridepublic 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 {@Overridepublic void drawLineByDev() {System.out.println("WindowX系统画直线");}@Overridepublic void drawTextByDev() {System.out.println("WindowX系统画文字");}}public static class LWindowImpl implements WindowImp {@Overridepublic void drawLineByDev() {System.out.println("Linux系统画直线");}@Overridepublic void drawTextByDev() {System.out.println("Linux系统画文字");}}//将所有和WindowImpl的操作封装在Window层//Window子类不能感知WindowImppublic 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) {//调用方决定使用哪个个系统的APITitleWindow 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;}@Overridepublic 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);}@Overridepublic 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)从数据库中查询菜单表测试数据,构建树并渲染。

    //数据库中菜单表POpublic 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;}@Overridepublic 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);}@Overridepublic 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;}@Overridepublic 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); //truea = 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时加载图片}@Overridepublic 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; //创建代理时不加载图片}@Overridepublic 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 {@Overridepublic void request() {System.out.println("RealSubject.request()");}}public static class Proxy implements Subject{private RealSubject realSubject;public Proxy(RealSubject realSubject) {this.realSubject = realSubject;}@Overridepublic void request() {System.out.println("判断是否有权限访问");realSubject.request();System.out.println("记录访问日志");}}public static void main(String[] args) {//Proxy类代替RealSubjectSubject proxy = new Proxy(new RealSubject());proxy.request();}

上例中为每一个RealSubject创建一个Proxy类的方式称为静态代理,如果不止一个类需要访问控制时,需要使用动态代理,如需要对所有Service类添加事前的权限判断和事后的日志记录。动态代理JDK和CGLIB中都有API支持,这已经不是设计模式的范畴。
最后说回代理模式,它的代码形式和装饰器模式基本一致,只能依靠用途区分,添加新功能就是装饰器模式,需要对方法访问控制就是代理模式。

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

经典伴读_GOF设计模式_结构型模式相关推荐

  1. 设计模式_结构型模式学习

    其中,单例模式用来创建全局唯一的对象.工厂模式用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),由给定的参数来决定创建哪种类型的对象.建造者模式是用来创建复杂对象,可以通过设置不同的可 ...

  2. 备战面试日记(3.3) - (设计模式.23种设计模式之结构型模式)

    本人本科毕业,21届毕业生,一年工作经验,简历专业技能如下,现根据简历,并根据所学知识复习准备面试. 记录日期:2022.1.9 大部分知识点只做大致介绍,具体内容根据推荐博文链接进行详细复习. 文章 ...

  3. 设计模式 之 结构型模式

    设计模式 之 结构型模式 模式 & 描述 包括 结构型模式 这些设计模式关注类和对象的组合.继承的概念被用来组合接口和定义组合对象获得新功能的方式. 适配器模式(Adapter Pattern ...

  4. 组合模式(Bridge Pattern) – 设计模式之结构型模式

    组合模式(Bridge Pattern) – 设计模式之结构型模式: 目录 组合模式(Component Pattern) 类图 例子1: 过程: 类图: 代码: 抽象组件:PlayerComposi ...

  5. 设计模式之结构型模式(5种)

    目录 结构型模式(Structural Pattern):怎么构造一个对象(行为.属性) 一.适配器模式 二.桥接模式(Bridge) 三.装饰者模式 设计模式在JAVA I/O库中的应用 案例 使用 ...

  6. 设计模式(17)-----结构型模式-----外观设计模式

    假如你现在还在为自己的技术担忧,假如你现在想提升自己的工资,假如你想在职场上获得更多的话语权,假如你想顺利的度过35岁这个魔咒,假如你想体验BAT的工作环境,那么现在请我们一起开启提升技术之旅吧,详情 ...

  7. JAVA23种设计模式(2)-结构型模式7种

    JAVA23种设计模式(2)-结构型模式7种 把类结合在一起形成更大的结构 适配器模式(adapter) 一句话:将一个类的接口转换成另一种接口.让原本接口不兼容的类可以兼容 这是平时比较常见的一种模 ...

  8. 设计模式3——结构型模式

    结构型模式描述如何将类或对象按某种布局组成更大的结构,它分为类结构型和对象结构型模式,前者采用继承机制来组织接口和类,后者采用组合或聚合来组合对象. 由于组合关系或聚合关系比继承关系耦合度低,满足&q ...

  9. 设计模式:结构型模式-桥接、外观、组合、享元模式

    结构型模式 结构型模式描述如何将类或对象按某种布局组成更大的结构.它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者采用组合或聚合来组合对象. 由于组合关系或聚合关系比继承关系耦 ...

最新文章

  1. Java Collection框架—List\ set \map 的异同世界
  2. POJ - 2549 Sumsets
  3. 制作越狱版本的ipa文件
  4. html页面和Chrome开发者工具elements界面不一致的一个可能原因:没有在Chrome开发者工具里打开对Shadow DOM显示的支持
  5. 【目标检测】ICCV21_TOOD: Task-aligned One-stage Object Detection
  6. 简单快速导出word文档
  7. 设置角标_iPhone桌面角标颜色
  8. python 判断一个点(坐标)是否在一个多边形内利用射线法
  9. 价值98元的千神资源网模板
  10. 矿区无人机影像地物提取(语义分割)
  11. Ubuntu用户连续N次输入错误密码进行登陆时自动锁定X分钟
  12. Codeforces Round #666 (Div. 2)D. Stoned Game(博弈问题)
  13. iOS开发之isa、superclass(课程总结)
  14. 【从C到C++学习笔记】bool类型/const限定符/#define//结构体对齐
  15. 文档管理系统mindoc安装
  16. Softmax函数及其导数
  17. 今天开始写博客记录程序媛成长过程
  18. 在局域网内怎样使两台计算机共享,实现局域网内两台windows计算机之间数据共享...
  19. 【北京线下】FMI2018人工智能大数据技术沙龙第869期
  20. 假如互联网公司做铁道部12306订票网站

热门文章

  1. ARM-ADC模数转换
  2. 全网最好用的图文识别、证件扫描、PDF转换等工具,已解锁永久会员!
  3. PM:iOS 为什么感觉比 Android 流畅?
  4. 电脑鼠标右击刷新一直转圈
  5. 郑州、昆明、韶关等多地全面推行商品房买卖合同电子签约
  6. jmeter如何定位网络延时_JMeter 如何模拟不同的网络速度
  7. 电脑运行卡顿?六个方法打开任务管理器解决
  8. C语言占位符 格式占位符
  9. 无人机寻迹要两个单片机吗_基于OpenMV的循迹无人机设计
  10. 关于ECharts怎么隐藏掉坐标轴