Java设计模式设计模式

看了这篇文章你将彻底了解组合模式

2020-01-05  本文已影响0人  静幽水

问题背景:

在上一篇观察者模式的文章中,IT公司老板通过观察者模式和程序员小强和小华实现了通信,便于通知他们加班,还可以单独通知不同的内容,例如通知小强加班,通知小华去出差。但随着公司的规模慢慢变大,公司从只有两个程序员和一个秘书的公司成长为一个拥有研发部和市场部两个部门,十几位员工的公司。但这就导致了之前的通知系统不再那么容易使用,例如老板想要通知研发部经理和市场部经理来办公室开会,并且通知研发部经理手下的所有研发人员今晚加班赶项目进度,该如何实现呢?更多内容请关注我的weixin公号【程序员修炼】

首先,将公司里所有的人员抽象出一个Staff接口类,接口里是所有成员的共同属性和方法,如获取个人信息的方法和接收通知的方法。

Staff接口类

public interface Staff {
    //获取个人信息
    public String getInfo();
    //接收通知
    public void doSomething(String notice);
}

然后公司里的员工可以分为两类,一类是管理者,手下还有若干管理者或员工,另一类就是普通的员工,没有下属。手先定义管理者的接口:

import java.util.ArrayList;

//管理者接口
public interface IManager extends Staff {
    //增加手下的员工
    public void addSubordinate(Staff staff);
    //获得手下员工的信息
    public ArrayList<Staff> getSubordinate();
    //通知员工
    public void notifyStaff(String note1,String note2);

    //从列表中删除下属成员
    public void removeSubordinate(Staff staff);
}

管理者接口有四个方法,分别是增加和删除下属成员,获取下属信息和通知下属消息。

普通员工接口类,在这里指的当然就是程序员啦,只是一个空接口:

public interface IProgrammer extends Staff{
   
}

在来看普通员工(程序员)的实现类,有两个属性,分别是姓名和职位,虽然大家都是程序员,但也是有职位之分的。然后是接收通知方法和获取个人信息方法的实现,接收通知方法的实现很简单,就是把自己的名字和职位以及被通知的内容打印出来(这里打印姓名和职位是为了和管理者进行对比)。

public class Programmer implements IProgrammer{
    private String name;
    private String position;

    public Programmer(String name,String position) {
        this.name = name;
        this.position = position;
    }


    @Override
    public String getInfo() {
        String info = "姓名:"+this.name+",职位:"+this.position;
        return info;
    }

    @Override
    public void doSomething(String notice) {
        System.out.println(this.name+","+this.position+","+notice);
    }
}

接下来是管理者的实现,同样拥有姓名和职位两个属性,并且还有维护一个下属成员的列表,用来保存手下所有的员工对象。增加下属就是往列表中添加一个对象,删除就是从下属列表中将该对象移除。查看下属成员是将下属成员列表返回。同时还有通知下属和接收上级通知的方法,通知下属时会遍历下属成员列表,如果该下属是普通员工(程序员),调用程序员的接收通知的方法,如果手下是管理者,调用管理者的接收通知的方法,同时将通知再向下级传递。

import java.util.ArrayList;

//管理者
public class Manager implements IManager{
    private String name;

    private String position;
    //他的手下列表
    ArrayList<Staff> subordinateList = new ArrayList<Staff>();

    public Manager(String name, String position) {
        this.name = name;
        this.position = position;
    }

    //增加一个手下
    @Override
    public void addSubordinate(Staff staff) {
        this.subordinateList.add(staff);

    }
 //删除一个下属
    @Override
    public void removeSubordinate(Staff staff) {
        this.subordinateList.remove(staff);

    }
    //查看我的手下
    @Override
    public ArrayList<Staff> getSubordinate() {
        return this.subordinateList;
    }

    @Override
    public String getInfo() {
        String info = "姓名:"+this.name+",职位:"+this.position;
        return info;
    }
//接收通知的方法
    @Override
    public void doSomething(String notice) {
        System.out.println(this.name+","+this.position+","+notice);
    }
    //通知手下的方法
    @Override
    public void notifyStaff(String notice1,String notice2){
        ArrayList<Staff> subordinateList = this.getSubordinate();
        for(Staff s:subordinateList){
            //如果手下是程序员,调用程序员的接收通知的方法
            if(s instanceof Programmer){
                ((Programmer) s).doSomething(notice1);
            }else {
                //手下是管理者,调用管理者的接收通知的方法,同时将通知再向下级传递
                ((Manager) s).doSomething(notice2);
                ((Manager) s).notifyStaff(notice1,notice2);
            }
        }
    }
}

最后写个客户端调用上面的方法,先创建一个大老板对象boss,然后一个研发部经理,两个研发部小组长和四个程序员,将程序员加到对应的组长名下,并将组长加到经理名下,最后将经理加到老板名下。老板发出通知,所有的普通员工今晚要加班,所有的管理者来会议室开会。

组织结构图如下:

在这里插入图片描述
import java.util.ArrayList;

public class Client {
    public static void main(String[] args) throws InterruptedException {
        Manager boss = new Manager("李大头","老板");
        Manager RDManger = new Manager("张三","研发部经理");
        Manager marketingManager  = new Manager("李四","市场部经理");
        Manager group1 = new Manager("王五","研发部组长一");
        Manager group2 = new Manager("赵六","研发部组长二");
        Programmer programmer1 = new Programmer("小强","java程序员");
        Programmer programmer2 = new Programmer("小华","java程序员");
        Programmer programmer3 = new Programmer("小甲","python程序员");
        Programmer programmer4 = new Programmer("小乙","c++程序员");

        boss.addSubordinate(RDManger);
        boss.addSubordinate(marketingManager);
        RDManger.addSubordinate(group1);
        RDManger.addSubordinate(group2);


        group1.addSubordinate(programmer1);
        group1.addSubordinate(programmer2);
        group2.addSubordinate(programmer3);
        group2.addSubordinate(programmer4);
        boss.notifyStaff("今晚加班","来开会");
//        System.out.println(boss.getInfo());
//        System.out.println(getAllInfo(boss));
    }
    public static String getAllInfo(Manager manager){
        ArrayList<Staff> subordinateList = manager.getSubordinate();
        String info = "";
        for(Staff s:subordinateList){
            if(s instanceof Programmer){
                info = info + s.getInfo() +"\n";
            }else {
                info = info +s.getInfo() +"\n" + getAllInfo((Manager)s);
            }
        }
        return info;
    }
}

程序运行结果:

张三,研发部经理,来开会
王五,研发部组长一,来开会
小强,java程序员,今晚加班
小华,java程序员,今晚加班
赵六,研发部组长二,来开会
小甲,python程序员,今晚加班
小乙,c++程序员,今晚加班
李四,市场部经理,来开会

上面程序的类图如下:

在这里插入图片描述

有何问题:

上面的程序乍一看似乎没有什么问题,而且我们也把管理者和普通员工类里的共性封装起来了,但有基础的同学或许看出来了,程序没有很好的进行代码复用,虽然把获取个人信息的getInfo方法和接收通知的doSomething方法在顶层接口中封装了,但它们在不同的实现类中代码完全一样(只是将个人信息和接收到的通知打印出来),为什么不抽象出来用抽象类实现呢?在这里就要了解一下接口和抽象类的异同点,以及它们分别在什么情况下使用。更多内容请关注我的weixin公号【程序员修炼】
1.相同点:

2.不同点:

好了,再来看上面的类图,既然所有的员工都有获取个人信息和接收通知的方法,就没有必要将它们抽象出接口了,并且这两个方法的实现体在不同的实现类中是相同的,所有将他们抽象出抽象类更能体现代码的复用性。

解决方案:

修改类图,将接口改成抽象类,如下:


在这里插入图片描述

Staff抽象类:

package Composite;

public abstract class Staff {
    private String name;
    private String position;

    public Staff(String name, String position) {
        this.name = name;
        this.position = position;
    }

    public String getInfo() {
        String info = "姓名:"+this.name+",职位:"+this.position;
        return info;
    }
    public void doSomething(String notice) {
        System.out.println(this.name+","+this.position+","+notice);
    }
}

管理者Manager类:

package Composite;


import java.util.ArrayList;

//管理者
public class Manager extends Staff {

    //他的手下列表
    ArrayList<Staff> subordinateList = new ArrayList<Staff>();

    public Manager(String name, String position) {
        super(name, position);
    }

    //增加一个手下
    public void addSubordinate(Staff staff) {
        this.subordinateList.add(staff);

    }

    public void removeSubordinate(Staff staff) {
        this.subordinateList.remove(staff);

    }
    //查看我的手下
    public ArrayList<Staff> getSubordinate() {
        return this.subordinateList;
    }
    //通知手下的方法
    public void notifyStaff(String notice1,String notice2){
        ArrayList<Staff> subordinateList = this.getSubordinate();
        for(Staff s:subordinateList){
            //如果手下是程序员,调用程序员的接收通知的方法
            if(s instanceof Programmer){
                ((Programmer) s).doSomething(notice1);
            }else {
                //手下是管理者,调用管理者的接收通知的方法,同时将通知再向下级传递
                ((Manager) s).doSomething(notice2);
                ((Manager) s).notifyStaff(notice1,notice2);
            }
        }
    }
}

普通员工Programmer类

package Composite;
public class Programmer extends Staff{

    public Programmer(String name, String position) {
        super(name, position);
    }
}

Client类保存不变,运行结果也和上面一致。

模式讲解:

上面使用的模式就是组合模式(Composite),组合模式的定义:

将对象组合成树型结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。

上面的管理者和员工就是部分和整体的关系,符合树状结构。组合模式的通用类图:


在这里插入图片描述

Component:抽象的组件对象,定义组合对象的共有方法和属性,可以定义一些默认的行为或属性。这个抽象类既可以代表叶子对象,也可以代表组合对象,这样用户在操作的时候,对单个对象和组合对象的使用就具有了一致性。

Leaf:叶子节点对象,定义和实现叶子对象的行为,不再博涵其他子节点对象。

Composite:组合对象,通常会存储子组件,定义包含子组件的那些行为,并实现在组件接口中定义的与子组件有关的操作。

Client:客户端,通过组件接口来操作组合结构里的组件对象。

组合模式的优点

组合模式的缺点:与依赖倒置原则冲突,就是在客户端创建的时候直接使用了实现类。

组合模式使用场景

新的问题:

细心的同学应该发现了,既然说组合模式是要让用户对叶子对象和组合对象的使用具有一致性,但是在创建对象的时候却还是使用各自的实现类创建的,这就涉及到组合模式实现的两种方式:安全性和透明性。

安全性:安全性指客户在使用的时候不会发送误操作,能访问的方法都是被支持的方法。

透明性:透明性指客户在使用的时候不需要区分组合对象还是叶子对象。

简单来说,安全性的方式是将对组件的操作(如增加一个子组件,删除一个子组件,获取子组件)定义在组合类Composite中,这样叶子节点对象就不能使用这些功能(本身叶子对应就不应该有这些功能),就不会产生误操作的现象。上面的示例代码都是安全性方式的。但这会带来透明性的问题,客户在使用的时候必须区分到底使用的叶子对象还是组合对象,因为他们的功能是不同的。

透明性方式是将对组件的操作定义在抽象类中,这样客户端只需面对Component,不需要关系具体的组件类型。但这会带来安全性的问题,因为叶子对象也具有了操作组件的方法,客户就有可能误对叶子对象调用这些方法,这样的操作是不安全的。如上面的示例,用这种方式写就是普通员工也有发布通知的功能了,这显然是不允许的。更多内容请关注我的weixin公号【程序员修炼】

首先看一下透明方式的类图:

在这里插入图片描述

透明性具体代码如下,为了充分体现透明性,代码和上面的改动有些大:

抽象类Staff(相当于Component)

package Composite;

import java.util.ArrayList;

public abstract class Staff {

    public abstract void getInfo();
    public abstract void doSomething(String notice);

    //增加一个手下
    public void addSubordinate(Staff staff) {
        throw new UnsupportedOperationException("不支持这个功能");

    }
    public void removeSubordinate(Staff staff) {
        throw new UnsupportedOperationException("不支持这个功能");

    }
    //查看我的手下
    public ArrayList<Staff> getSubordinate() {
        return null;
    }
    public void notifyStaff(String notice1,String notice2){

    }
}

抽象类中定义了对员工操作的所有方法,包括增加,删除,查看和通知。还包括普通员工和管理者都有的获取个人信息方法和接收通知方法,这两个方法定义为抽象方法。

普通员工类(相当于Leaf)

package Composite;
public class Programmer extends Staff{

    private String name;
    private String position;

    public Programmer(String name, String position) {
        this.name = name;
        this.position = position;
    }

    @Override
    public void getInfo() {
        String info = "姓名:"+this.name+",职位:"+this.position;
        System.out.println(info);
    }

    @Override
    public void doSomething(String notice) {
        System.out.println(this.name+","+this.position+","+notice);
    }
}

这个类比较简单,就是增加了两个属性和实现了两个抽象方法。

管理者类(相当于Composite)

package Composite;
import java.util.ArrayList;

//管理者
public class Manager extends Staff {

    private String name;
    private String position;

    public Manager(String name, String position) {
        this.name = name;
        this.position = position;
    }

    //他的手下列表
    ArrayList<Staff> subordinateList = null;

    public void addSubordinate(Staff staff) {
        //延迟初始化
        if(subordinateList == null){
            subordinateList = new ArrayList<Staff>();
        }
        subordinateList.add(staff);
    }
    public ArrayList<Staff> getSubordinate() {
        return this.subordinateList;
    }
    @Override
    public void getInfo() {
        //先将自己输出
        String info = "姓名:"+this.name+",职位:"+this.position;
        System.out.println(info);
        if(this.subordinateList !=null){
            //输出下属对象
            for(Staff s:subordinateList){
                //递归调用
                s.getInfo();
            }
        }
    }

    @Override
    public void doSomething(String notice) {
        System.out.println(this.name+","+this.position+","+notice);

    }
    //通知手下的方法
    public void notifyStaff(String notice1,String notice2){
        if(this.subordinateList !=null){
            for(Staff s:subordinateList){
                //如果手下是程序员,调用程序员的接收通知的方法
                //手下是管理者,调用管理者的接收通知的方法,同时将通知再向下级传递
                s.doSomething(notice2);
                s.notifyStaff(notice1,notice2);
            }
        }
    }
}

这个类比较复杂,处理实现了获取个人信息和接收通知的方法外,还重写了增加,删除,查看下属员工的方法,以及发布通知的方法。

客户端:

package Composite;
public class Client {
    public static void main(String[] args) throws InterruptedException {
        Staff boss = new Manager("李大头","老板");
        Staff RDManger = new Manager("张三","研发部经理");
        Staff marketingManager  = new Manager("李四","市场部经理");
        Staff group1 = new Manager("王五","研发部组长一");
        Staff group2 = new Manager("赵六","研发部组长二");
        Staff programmer1 = new Programmer("小强","java程序员");
        Staff programmer2 = new Programmer("小华","java程序员");
        Staff programmer3 = new Programmer("小甲","python程序员");
        Staff programmer4 = new Programmer("小乙","c++程序员");

        boss.addSubordinate(RDManger);
        boss.addSubordinate(marketingManager);
        RDManger.addSubordinate(group1);
        RDManger.addSubordinate(group2);
        
        group1.addSubordinate(programmer1);
        group1.addSubordinate(programmer2);
        group2.addSubordinate(programmer3);
        group2.addSubordinate(programmer4);
        boss.notifyStaff("今晚加班","来开会");
        System.out.println("---------------------");
        boss.getInfo();
    }

}

在这里能够看出,普通员工和管理者没有任何区别,客户端不需要区分组合对象和叶子对象了,统一使用组件对象(Staff),调用的方法也是在组件对象中定义的方法。

运行结果:

张三,研发部经理,来开会
王五,研发部组长一,来开会
小强,java程序员,来开会
小华,java程序员,来开会
赵六,研发部组长二,来开会
小甲,python程序员,来开会
小乙,c++程序员,来开会
李四,市场部经理,来开会
---------------------
姓名:李大头,职位:老板
姓名:张三,职位:研发部经理
姓名:王五,职位:研发部组长一
姓名:小强,职位:java程序员
姓名:小华,职位:java程序员
姓名:赵六,职位:研发部组长二
姓名:小甲,职位:python程序员
姓名:小乙,职位:c++程序员
姓名:李四,职位:市场部经理

相关扩展:

组合模式中的递归:指的是对象递归组合,对象本身的递归,在设计上称为递归关联,是对象关联关系的一种。理论上是没有层次限制的。

最大化Component定义:在透明性的组合模式中,能够看出组合对象和叶子对象的方法都被定义在了Component中,这其实是与类的设计原则相冲突的,一个父类应该只定义那些子类有意义的操作,而Component很多方法对于叶子对象没有意义。解决方法就是为这些方法提供默认实现,或者抛出不支持该功能的异常,如果子类需要这个方法就覆盖实现,不需要就不需要管。

父组件引用:在上面的示例都是从上到下的引用,也就是父到子的引用,而很多时候我们需要从下到上的引用,还是上面的例子,如果想要从普通的程序员找到他的小组长或者部门经理该怎么做呢?解决方法是在保持父组件到子组件引用的基础上,再添加保持子组件到父组件的引用。在Component中定义对父组件的引用,组合对象和叶子对象都继承这个引用。在组合对象添加子组件对象的时候,为子组件设置父组件引用,在组合对象删除一个子组件对象的时候,再重新设置相关子组件的父组件的引用。把这些实现到composite中,这样所有的子类都可以继承到这些方法,从而更容易的维护子组件到父组件的引用。由于篇幅关系,这里不再写代码。

环状引用:指的是对象之间的引用形成了一个环,一个对象经过若干次引用之后又包含了这个对象本身,就构成了一个环状引用,如A包含B,B包含C,C包含A.通常在设计组合模式时需要避免这种情况,否则容易出现死循环。但如果真的需要环状引用,就需要构建环状引用,并提供相应的检测和处理。

上一篇下一篇

猜你喜欢

热点阅读