第四五次作业

物理视图

一、概述

物理视图:物理视图对应自身的结构实现建模,图中的类将会称为物理结构中的节点。物理视图分为实现视图和部署视图。实现视图为系统中的构件建模,以及构件之间的依赖关系,通过对依赖关系修改评估对系统可能带来影响。

实现视图将系统中可重用的块包装成具有可替代性的物理单元,这些单元称为构件。实现视图用构件以及构件之间的接口与依赖关系来表示设计元素的具体实现。构件是系统高层的可重用的组成部件。

部署视图表示运行时计算资源的物理布置。这些运行的资源称为节点。在运行时,节点包含构件和对象。构件和对象的分配可以是静态的,它们可以在节点之间迁移。如果含有依赖关系的构件实例放在不同的节点上,部署视图可以展示执行过程中的瓶颈。

二、构件

 构件是定义了良好接口的物理实现单元,它是系统中可替换的部分。举个例子:电路板上的电容器,独立声卡,独立显卡。下面是带接口构件的一个实例图

 

三、节点

节点是表示计算资源在运行时的物理对象,通常具有内存和处理能力。节点可能具有用来辨别各种资源的构造型。节点可以包含构件和对象实例。下面是一个部署图:

 

节点用带有节点名称的立方体表示,可以具有分类(可选)。节点之间的关联代表通信路径。关联有用来辨别不同路径的构造型。节点也有泛化关系,将节点的一般描述和具体的特例联系起来。对象在节点内的存在用嵌套在节点内的对象符号来表示。如果这样表示不方便,对象符号可以包含它所在节点的location标签。节点间对象或构件实例间的迁移也可以表示出来。

模型管理视图

一、 概述

  为了方便管理代码,我们必须将大量的类进行分包管理。模型管理由包与包之间的依赖关系组成。

二、

  包是模型的一部分,模型的每个部分必定属于某个包。UML对于分包的规则不属于强制性的,不过良好的包组织确实是方便管理与维护。包包含顶层的模型元素。每个顶层元素都有一个包,它在这个包中被声明,该包被称作元素的“家”包。元素的内容可以被其他包所引用,但是其所有权属于家包。在一个配置好的控制系统中,建模者必须能够对家包进行访问以修改元素的内容,这为大的模型提供了访问控制机制。包也是任何版本出版机制的单元。包可以用来存储,控制访问,配置管理和构造可重用部件提供了很大的帮助。包之间的依赖关系描述了包的内容之间的依赖关系。

三、包间的依赖关系

依赖关系出现在独立元素之间,但是在任何规模的系统之间,都应该从更高层次观察他们。包之间的依赖关系概述了包中元素的依赖关系,即包间的依赖关系可以从独立元素之间的依赖关系导出。下图展示了一个有依赖关系存在的包结构图:

 

四、访问与引入依赖

     通常,一个包是不能访问另一个包的内容。包是不透明的,除非它们能被访问或者引入依赖关系。包之间可以相互的访问的形式有两种,第一种就是像正常开发过程中创建包的方式一样,直接放到一起;第二种是引入依赖关系,例如导入依赖包的方式,例如导入jdbc的驱动包。

五、模型和子系统

     模型是从某一个视角观察到的对系统进行完全扫描的包。它是从一个视点提供一个系统的封闭的描述。通常模型为树形结构。根包包含了存在于它体内的嵌套包,嵌套包组成了从给定观点出发的系统的所有细节。子系统是具有单独说明和实现部分的包。它表示具有对系统其它部分存在干净接口的连贯模型单元,通常表示按照一定功能要求或实现要求对系统的划分。模型和子系统都用具有构造型关键字的包表示。例图参考本文中唯一的那张图。

创建型模式、

一,创建型模式(Creational Pattern)

  1. 1.  抽象工厂(Abstract Factory)

定义:为创建一组相关或相互依赖的对象提供一个接口,而且无须指定它们的具体类。并通过组合方式被其他对象集成使用。
 2. Factory Method
(工厂方法)

定义:定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。   

3.Builder(建造模式)

定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

说明:一个复杂对象可能由多个简单对象构成,构成顺序不同或者简单对象实现不同,所获得的复杂对象结果也不同。


  1. 2.  Prototype(原型模式)

定义:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。原型模式实际上就是实现Cloneable接口,重写clone()方法,或者自定义拷贝对象方法,如将对象序列化后再反序列化输出。

     

5.Singleton(单例模式)

定义:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例

         

物理视图

  物理视图对应用本身的实现结构建模。如将其组织为构件和在运行结点上进行配置。这些视图提供了将类映射至构件和结点的机会。有两种物理视图:实现视图和配置视图。

  实现视图对模型中的构件建模,即应用程序搭建的软件单元;以及构件之间的依赖,从而可以估计所预计到的更改的影响。它还对类及其它元素至构件的分配建模。

  配置视图表达了运行时段构件实例在结点实例中的分布。结点是运行资源,如计算机设备或内存。该视图允许分布式的结果和资源分配被评估。

模型管理视图

  模型管理视图对模型本身的组织建模。模型由一系列包含模型元素(如类、状态机、用例)的包构成。包可以包含其它包,因此,模型指派了一个根包,间接包含了模型的所有内容。包是操纵包内容,以及访问控制和配置控制的单元。每个模型元素被包或其它元素所拥有

  模型是某个视角、给定精度的对系统的完整描述。因此,可能存在不同视角下系统的多个模型--例如,分析模型和设计模型。模型显示为特殊的包。

  子系统是另外一种特殊的包。它代表了系统的一部分,并具有明晰的接口,接口可以实现

为独特的构件。

 

  模型管理信息经常在类图中出现。

1 结构型模式介绍

结构型模式描述如何组织类和对象以组成更大的结构。结构型类模式采用继承机制来组合接口和实现,结构型对象模式则采用组合聚合来组合对象以实现新功能,可以在运行时刻改变对象组合关系,具有更大灵活性,故这里只关注结构型对象模式。一般常见的结构型模式有 7 种:ABCDFFP(Adapter,Bridge,Composite,Decorator,Façade,Flyweight,Proxy) 。

1.1.1 适配器模式( Adapter )

适配器模式将某个类的接口转换成客户端期望的另一个接口表示,目的是消除由于接口不匹配所造成的类的兼容性问题。主要分为三类:类的适配器模式、对象的适配器模式、接口的适配器模式。

1.1.2 类的适配器模式

核心思想就是:有一个 Source 类,拥有一个方法,待适配,目标接口是 Targetable ,通过 Adapter 类,将 Source 的功能扩展到 Targetable 里,实现代码:

输出:

这样 Targetable 接口的实现类就具有了 Source 类的功能。

1.1.3 对象的适配器模式

基本思路和类的适配器模式相同,只是将 Adapter 类作修改,这次不继承 Source 类,而是持有 Source 类的实例,以达到解决兼容性的问题。

2 行为型模式介绍

行为型模式,共 11 种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

11 中模式的关系:第一类:通过父类与子类的关系进行实现。第二类:两个类之间。第三类:类的状态。第四类:通过中间类。

 

一、适配器模式

说到适配器,我们最熟悉的莫过于电源适配器了,也就是手机的充电头。它就是适配器模式的一个应用。

试想一下,你有一条连接电脑和手机的 USB 数据线,连接电脑的一端从电脑接口处接收 5V 的电压,连接手机的一端向手机输出 5V 的电压,并且他们工作良好。

中国的家用电压都是 220V,所以 USB 数据线不能直接拿来给手机充电,这时候我们有两种方案:

  • 单独制作手机充电器,接收 220V 家用电压,输出 5V 电压。
  • 添加一个适配器,将 220V 家庭电压转化为类似电脑接口的 5V 电压,再连接数据线给手机充电。

如果你使用过早期的手机,就会知道以前的手机厂商采用的就是第一种方案:早期的手机充电器都是单独制作的,充电头和充电线是连在一起的。现在的手机都采用了电源适配器加数据线的方案。这是生活中应用适配器模式的一个进步。

 

适配器模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。

适配的意思是适应、匹配。通俗地讲,适配器模式适用于 有相关性但不兼容的结构,源接口通过一个中间件转换后才可以适用于目标接口,这个转换过程就是适配,这个中间件就称之为适配器。

家用电源和 USB 数据线有相关性:家用电源输出电压,USB 数据线输入电压。但两个接口无法兼容,因为一个输出 220V,一个输入 5V,通过适配器将输出 220V 转换成输出 5V 之后才可以一起工作。

让我们用程序来模拟一下这个过程。

首先,家庭电源提供 220V 的电压:

Java 实现

class HomeBattery {

    int supply() {

        // 家用电源提供一个 220V 的输出电压

        return 220;

    }

}

USB 数据线只接收 5V 的充电电压:

class USBLine {

    void charge(int volt) {

        // 如果电压不是 5V,抛出异常

        if (volt != 5) throw new IllegalArgumentException("只能接收 5V 电压");

        // 如果电压是 5V,正常充电

        System.out.println("正常充电");

    }

}

先来看看适配之前,用户如果直接用家庭电源给手机充电:

public class User {

    @Test

    public void chargeForPhone() {

        HomeBattery homeBattery = new HomeBattery();

        int homeVolt = homeBattery.supply();

        System.out.println("家庭电源提供的电压是 " + homeVolt + "V");

 

        USBLine usbLine = new USBLine();

        usbLine.charge(homeVolt);

    }

}

运行程序,输出如下:

家庭电源提供的电压是 220V

 

java.lang.IllegalArgumentException: 只能接收 5V 电压

这时,我们加入电源适配器:

class Adapter {

    int convert(int homeVolt) {

        // 适配过程:使用电阻、电容等器件将其降低为输出 5V

        int chargeVolt = homeVolt - 215;

        return chargeVolt;

    }

}

然后,用户再使用适配器将家庭电源提供的电压转换为充电电压:

public class User {

    @Test

    public void chargeForPhone() {

        HomeBattery homeBattery = new HomeBattery();

        int homeVolt = homeBattery.supply();

        System.out.println("家庭电源提供的电压是 " + homeVolt + "V");

 

        Adapter adapter = new Adapter();

        int chargeVolt = adapter.convert(homeVolt);

        System.out.println("使用适配器将家庭电压转换成了 " + chargeVolt + "V");

 

        USBLine usbLine = new USBLine();

        usbLine.charge(chargeVolt);

    }

}

运行程序,输出如下:

家庭电源提供的电压是 220V

使用适配器将家庭电压转换成了 5V

正常充电

这就是适配器模式。在我们日常的开发中经常会使用到各种各样的 Adapter,都属于适配器模式的应用。

但适配器模式并不推荐多用。因为未雨绸缪好过亡羊补牢,如果事先能预防接口不同的问题,不匹配问题就不会发生,只有遇到源接口无法改变时,才应该考虑使用适配器。比如现代的电源插口中很多已经增加了专门的充电接口,让我们不需要再使用适配器转换接口,这又是社会的一个进步。

 

二、桥接模式

考虑这样一个需求:绘制矩形、圆形、三角形这三种图案。按照面向对象的理念,我们至少需要三个具体类,对应三种不同的图形。

抽象接口 IShape:

public interface IShape {

    void draw();

}

三个具体形状类:

class Rectangle implements IShape {

    @Override

    public void draw() {

        System.out.println("绘制矩形");

    }

}

 

class Round implements IShape {

    @Override

    public void draw() {

        System.out.println("绘制圆形");

    }

}

 

class Triangle implements IShape {

    @Override

    public void draw() {

        System.out.println("绘制三角形");

    }

}

 

接下来我们有了新的需求,每种形状都需要有四种不同的颜色:红、蓝、黄、绿。

这时我们很容易想到两种设计方案:

  • 为了复用形状类,将每种形状定义为父类,每种不同颜色的图形继承自其形状父类。此时一共有 12 个类。
  • 为了复用颜色类,将每种颜色定义为父类,每种不同颜色的图形继承自其颜色父类。此时一共有 12 个类。

乍一看没什么问题,我们使用了面向对象的继承特性,复用了父类的代码并扩展了新的功能。

但仔细想一想,如果以后要增加一种颜色,比如黑色,那么我们就需要增加三个类;如果再要增加一种形状,我们又需要增加五个类,对应 5 种颜色。

更不用说遇到增加 20 个形状,20 种颜色的需求,不同的排列组合将会使工作量变得无比的庞大。看来我们不得不重新思考设计方案。

形状和颜色,都是图形的两个属性。他们两者的关系是平等的,所以不属于继承关系。更好的的实现方式是:将形状和颜色分离,根据需要对形状和颜色进行组合,这就是桥接模式的思想。

桥接模式:将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体模式或接口模式。

官方定义非常精准、简练,但却有点不易理解。通俗地说,如果一个对象有两种或者多种分类方式,并且两种分类方式都容易变化,比如本例中的形状和颜色。这时使用继承很容易造成子类越来越多,所以更好的做法是把这种分类方式分离出来,让他们独立变化,使用时将不同的分类进行组合即可。

说到这里,不得不提一个设计原则:合成 / 聚合复用原则。虽然它没有被划分到六大设计原则中,但它在面向对象的设计中也非常的重要。

合成 / 聚合复用原则:优先使用合成 / 聚合,而不是类继承。

继承虽然是面向对象的三大特性之一,但继承会导致子类与父类有非常紧密的依赖关系,它会限制子类的灵活性和子类的复用性。而使用合成 / 聚合,也就是使用接口实现的方式,就不存在依赖问题,一个类可以实现多个接口,可以很方便地拓展功能。

让我们一起来看一下本例使用桥接模式的程序实现:

新建接口类 IColor,仅包含一个获取颜色的方法:

public interface IColor {

    String getColor();

}

每种颜色都实现此接口:

public class Red implements IColor {

    @Override

    public String getColor() {

        return "红";

    }

}

 

public class Blue implements IColor {

    @Override

    public String getColor() {

        return "蓝";

    }

}

 

public class Yellow implements IColor {

    @Override

    public String getColor() {

        return "黄";

    }

}

 

public class Green implements IColor {

    @Override

    public String getColor() {

        return "绿";

    }

}

 

在每个形状类中,桥接 IColor 接口:

class Rectangle implements IShape {

 

    private IColor color;

 

    void setColor(IColor color) {

        this.color = color;

    }

 

    @Override

    public void draw() {

        System.out.println("绘制" + color.getColor() + "矩形");

    }

}

 

class Round implements IShape {

 

    private IColor color;

 

    void setColor(IColor color) {

        this.color = color;

    }

 

    @Override

    public void draw() {

        System.out.println("绘制" + color.getColor() + "圆形");

    }

}

 

class Triangle implements IShape {

 

    private IColor color;

 

    void setColor(IColor color) {

        this.color = color;

    }

 

    @Override

    public void draw() {

        System.out.println("绘制" + color.getColor() + "三角形");

    }

}

 

测试函数:

@Test

public void drawTest() {

    Rectangle rectangle = new Rectangle();

    rectangle.setColor(new Red());

    rectangle.draw();

   

    Round round = new Round();

    round.setColor(new Blue());

    round.draw();

   

    Triangle triangle = new Triangle();

    triangle.setColor(new Yellow());

    triangle.draw();

}

运行程序,输出如下:

绘制红矩形

绘制蓝圆形

绘制黄三角形

这时我们再来回顾一下官方定义:将抽象部分与它的实现部分分离,使它们都可以独立地变化。抽象部分指的是父类,对应本例中的形状类,实现部分指的是不同子类的区别之处。将子类的区别方式 —— 也就是本例中的颜色 —— 分离成接口,通过组合的方式桥接颜色和形状,这就是桥接模式,它主要用于 两个或多个同等级的接口。

 

三、组合模式

上文说到,桥接模式用于将同等级的接口互相组合,那么组合模式和桥接模式有什么共同点吗?

事实上组合模式和桥接模式的组合完全不一样。组合模式用于 整体与部分的结构,当整体与部分有相似的结构,在操作时可以被一致对待时,就可以使用组合模式。例如:

  • 文件夹和子文件夹的关系:文件夹中可以存放文件,也可以新建文件夹,子文件夹也一样。
  • 总公司子公司的关系:总公司可以设立部门,也可以设立分公司,子公司也一样。
  • 树枝和分树枝的关系:树枝可以长出叶子,也可以长出树枝,分树枝也一样。

在这些关系中,虽然整体包含了部分,但无论整体或部分,都具有一致的行为。

组合模式:又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。

考虑这样一个实际应用:设计一个公司的人员分布结构,结构如下图所示。

 

我们注意到人员结构中有两种结构,一是管理者,如老板,PM,CFO,CTO,二是职员。其中有的管理者不仅仅要管理职员,还会管理其他的管理者。这就是一个典型的整体与部分的结构。

3.1.不使用组合模式的设计方案

要描述这样的结构,我们很容易想到以下设计方案:

新建管理者类:

public class Manager {

    // 职位

    private String position;

    // 工作内容

    private String job;

    // 管理的管理者

    private List<Manager> managers = new ArrayList<>();

    // 管理的职员

    private List<Employee> employees = new ArrayList<>();

 

    public Manager(String position, String job) {

        this.position = position;

        this.job = job;

    }

   

    public void addManager(Manager manager) {

        managers.add(manager);

    }

 

    public void removeManager(Manager manager) {

        managers.remove(manager);

    }

 

    public void addEmployee(Employee employee) {

        employees.add(employee);

    }

 

    public void removeEmployee(Employee employee) {

        employees.remove(employee);

    }

 

    // 做自己的本职工作

    public void work() {

        System.out.println("我是" + position + ",我正在" + job);

    }

   

    // 检查下属

    public void check() {

        work();

        for (Employee employee : employees) {

            employee.work();

        }

        for (Manager manager : managers) {

            manager.check();

        }

    }

}

新建职员类:

public class Employee {

    // 职位

    private String position;

    // 工作内容

    private String job;

 

    public Employee(String position, String job) {

        this.position = position;

        this.job = job;

    }

 

    // 做自己的本职工作

    public void work() {

        System.out.println("我是" + position + ",我正在" + job);

    }

}

客户端建立人员结构关系:

public class Client {

   

    @Test

    public void test() {

        Manager boss = new Manager("老板", "唱怒放的生命");

        Employee HR = new Employee("人力资源", "聊微信");

        Manager PM = new Manager("产品经理", "不知道干啥");

        Manager CFO = new Manager("财务主管", "看剧");

        Manager CTO = new Manager("技术主管", "划水");

        Employee UI = new Employee("设计师", "画画");

        Employee operator = new Employee("运营人员", "兼职客服");

        Employee webProgrammer = new Employee("程序员", "学习设计模式");

        Employee backgroundProgrammer = new Employee("后台程序员", "CRUD");

        Employee accountant = new Employee("会计", "背九九乘法表");

        Employee clerk = new Employee("文员", "给老板递麦克风");

        boss.addEmployee(HR);

        boss.addManager(PM);

        boss.addManager(CFO);

        PM.addEmployee(UI);

        PM.addManager(CTO);

        PM.addEmployee(operator);

        CTO.addEmployee(webProgrammer);

        CTO.addEmployee(backgroundProgrammer);

        CFO.addEmployee(accountant);

        CFO.addEmployee(clerk);

 

        boss.check();

    }

}

运行测试方法,输出如下(为方便查看,笔者添加了缩进):

我是老板,我正在唱怒放的生命

        我是人力资源,我正在聊微信

        我是产品经理,我正在不知道干啥

                 我是设计师,我正在画画

                 我是运营人员,我正在兼职客服

                 我是技术主管,我正在划水

                         我是程序员,我正在学习设计模式

                         我是后台程序员,我正在CRUD

        我是财务主管,我正在看剧

                 我是会计,我正在背九九乘法表

                 我是文员,我正在给老板递麦克风

这样我们就设计出了公司的结构,但是这样的设计有两个弊端:

  • name 字段,job 字段,work 方法重复了。
  • 管理者对其管理的管理者和职员需要区别对待。

关于第一个弊端,虽然这里为了讲解,只有两个字段和一个方法重复,实际工作中这样的整体部分结构会有相当多的重复。比如此例中还可能有工号、年龄等字段,领取工资、上下班打卡、开各种无聊的会等方法。

大量的重复显然是很丑陋的代码,分析一下可以发现, Manager 类只比 Employee 类多一个管理人员的列表字段,多几个增加 / 移除人员的方法,其他的字段和方法全都是一样的。

有读者应该会想到:我们可以将重复的字段和方法提取到一个工具类中,让 Employee 和 Manager 都去调用此工具类,就可以消除重复了。

这样固然可行,但属于 Employee 和 Manager 类自己的东西却要通过其他类调用,并不利于程序的高内聚。

关于第二个弊端,此方案无法解决,此方案中 Employee 和 Manager 类完全是两个不同的对象,两者的相似性被忽略了。

所以我们有更好的设计方案,那就是组合模式!

3.2.使用组合模式的设计方案

组合模式最主要的功能就是让用户可以一致对待整体和部分结构,将两者都作为一个相同的组件,所以我们先新建一个抽象的组件类:

public abstract class Component {

    // 职位

    private String position;

    // 工作内容

    private String job;

 

    public Component(String position, String job) {

        this.position = position;

        this.job = job;

    }

 

    // 做自己的本职工作

    public void work() {

        System.out.println("我是" + position + ",我正在" + job);

    }

 

    abstract void addComponent(Component component);

 

    abstract void removeComponent(Component component);

 

    abstract void check();

}

管理者继承自此抽象类:

public class Manager extends Component {

    // 管理的组件

    private List<Component> components = new ArrayList<>();

 

    public Manager(String position, String job) {

        super(position, job);

    }

 

    @Override

    public void addComponent(Component component) {

        components.add(component);

    }

 

    @Override

    void removeComponent(Component component) {

        components.remove(component);

    }

 

    // 检查下属

    @Override

    public void check() {

        work();

        for (Component component : components) {

            component.check();

        }

    }

}

职员同样继承自此抽象类:

public class Employee extends Component {

 

    public Employee(String position, String job) {

        super(position, job);

    }

 

    @Override

    void addComponent(Component component) {

        System.out.println("职员没有管理权限");

    }

 

    @Override

    void removeComponent(Component component) {

        System.out.println("职员没有管理权限");

    }

 

    @Override

    void check() {

        work();

    }

}

修改客户端如下:

public class Client {

 

    @Test

    public void test(){

        Component boss = new Manager("老板", "唱怒放的生命");

        Component HR = new Employee("人力资源", "聊微信");

        Component PM = new Manager("产品经理", "不知道干啥");

        Component CFO = new Manager("财务主管", "看剧");

        Component CTO = new Manager("技术主管", "划水");

        Component UI = new Employee("设计师", "画画");

        Component operator = new Employee("运营人员", "兼职客服");

        Component webProgrammer = new Employee("程序员", "学习设计模式");

        Component backgroundProgrammer = new Employee("后台程序员", "CRUD");

        Component accountant = new Employee("会计", "背九九乘法表");

        Component clerk = new Employee("文员", "给老板递麦克风");

        boss.addComponent(HR);

        boss.addComponent(PM);

        boss.addComponent(CFO);

        PM.addComponent(UI);

        PM.addComponent(CTO);

        PM.addComponent(operator);

        CTO.addComponent(webProgrammer);

        CTO.addComponent(backgroundProgrammer);

        CFO.addComponent(accountant);

        CFO.addComponent(clerk);

 

        boss.check();

    }

}

运行测试方法,输出结果与之前的结果一模一样。

可以看到,使用组合模式后,我们解决了之前的两个弊端。一是将共有的字段与方法移到了父类中,消除了重复,并且在客户端中,可以一致对待 Manager 和 Employee 类:

  • Manager 类和 Employee 类统一声明为 Component 对象
  • 统一调用 Component 对象的 addComponent 方法添加子对象即可。

3.3.组合模式中的安全方式与透明方式

读者可能已经注意到了,Employee 类虽然继承了父类的 addComponent 和 removeComponent 方法,但是仅仅提供了一个空实现,因为 Employee 类是不支持添加和移除组件的。这样是否违背了接口隔离原则呢?

接口隔离原则:客户端不应依赖它不需要的接口。如果一个接口在实现时,部分方法由于冗余被客户端空实现,则应该将接口拆分,让实现类只需依赖自己需要的接口方法。

答案是肯定的,这样确实违背了接口隔离原则。这种方式在组合模式中被称作透明方式.

透明方式:在 Component 中声明所有管理子对象的方法,包括 add 、remove 等,这样继承自 Component 的子类都具备了 add、remove 方法。对于外界来说叶节点和枝节点是透明的,它们具备完全一致的接口。

这种方式有它的优点:让 Manager 类和 Employee 类具备完全一致的行为接口,调用者可以一致对待它们。

但它的缺点也显而易见:Employee 类并不支持管理子对象,不仅违背了接口隔离原则,而且客户端可以用 Employee 类调用 addComponent 和 removeComponent 方法,导致程序出错,所以这种方式是不安全的。

那么我们可不可以将 addComponent 和 removeComponent 方法移到 Manager 子类中去单独实现,让 Employee 不再实现这两个方法呢?我们来尝试一下。

将抽象类修改为:

public abstract class Component {

    // 职位

    private String position;

    // 工作内容

    private String job;

 

    public Component(String position, String job) {

        this.position = position;

        this.job = job;

    }

 

    // 做自己的本职工作

    public void work() {

        System.out.println("我是" + position + ",我正在" + job);

    }

 

    abstract void check();

}

可以看到,我们在父类中去掉了 addComponent 和 removeComponent 这两个抽象方法。

Manager 类修改为:

public class Manager extends Component {

    // 管理的组件

    private List<Component> components = new ArrayList<>();

 

    public Manager(String position, String job) {

        super(position, job);

    }

 

    public void addComponent(Component component) {

        components.add(component);

    }

 

    void removeComponent(Component component) {

        components.remove(component);

    }

 

    // 检查下属

    @Override

    public void check() {

        work();

        for (Component component : components) {

            component.check();

        }

    }

}

Manager 类单独实现了 addComponent 和 removeComponent 这两个方法,去掉了 @Override 注解。

Employee 类修改为:

public class Employee extends Component {

 

    public Employee(String position, String job) {

        super(position, job);

    }

 

    @Override

    void check() {

        work();

    }

}

客户端建立人员结构关系:

public class Client {

 

    @Test

    public void test(){

        Manager boss = new Manager("老板", "唱怒放的生命");

        Employee HR = new Employee("人力资源", "聊微信");

        Manager PM = new Manager("产品经理", "不知道干啥");

        Manager CFO = new Manager("财务主管", "看剧");

        Manager CTO = new Manager("技术主管", "划水");

        Employee UI = new Employee("设计师", "画画");

        Employee operator = new Employee("运营人员", "兼职客服");

        Employee webProgrammer = new Employee("程序员", "学习设计模式");

        Employee backgroundProgrammer = new Employee("后台程序员", "CRUD");

        Employee accountant = new Employee("会计", "背九九乘法表");

        Employee clerk = new Employee("文员", "给老板递麦克风");

        boss.addComponent(HR);

        boss.addComponent(PM);

        boss.addComponent(CFO);

        PM.addComponent(UI);

        PM.addComponent(CTO);

        PM.addComponent(operator);

        CTO.addComponent(webProgrammer);

        CTO.addComponent(backgroundProgrammer);

        CFO.addComponent(accountant);

        CFO.addComponent(clerk);

 

        boss.check();

    }

}

 

运行程序,输出结果与之前一模一样。

这种方式在组合模式中称之为安全方式。

安全方式:在 Component 中不声明 add 和 remove 等管理子对象的方法,这样叶节点就无需实现它,只需在枝节点中实现管理子对象的方法即可。

安全方式遵循了接口隔离原则,但由于不够透明,Manager 和 Employee 类不具有相同的接口,在客户端中,我们无法将 Manager 和 Employee 统一声明为 Component 类了,必须要区别对待,带来了使用上的不方便。

安全方式和透明方式各有好处,在使用组合模式时,需要根据实际情况决定。但大多数使用组合模式的场景都是采用的透明方式,虽然它有点不安全,但是客户端无需做任何判断来区分是叶子结点还是枝节点,用起来是真香。

 

总结

到这里我们就把结构型模式的前三种介绍完了,让我们总结一下:

  • 适配器模式:用于有相关性但不兼容的接口
  • 桥接模式:用于同等级的接口互相组合
  • 组合模式:用于整体与部分的结构

 

posted @ 2021-02-19 16:47  19A6  阅读(91)  评论(0编辑  收藏  举报