作者 | 静幽水
责编 | 郭芮
问题背景
假设,IT公司老板通过观察者模式和程序员小强和小华实现了通信,便于通知他们加班,还可以单独通知不同的内容,例如通知小强加班,通知小华去出差。但随着公司的规模慢慢变大,公司从只有两个程序员和一个秘书的公司成长为一个拥有研发部和市场部两个部门,十几位员工的公司。那这种情况下,之前的通知系统还好用吗?例如老板想要通知研发部经理和市场部经理来办公室开会,并且通知研发部经理手下的所有研发人员今晚加班赶项目进度,该如何实现呢?
首先,将公司里所有的人员抽象出一个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方法在顶层接口中封装了,但它们在不同的实现类中代码完全一样(只是将个人信息和接收到的通知打印出来),为什么不抽象出来用抽象类实现呢?在这里就要了解一下接口和抽象类的异同点,以及它们分别在什么情况下使用。
1.相同点:
(1) 都是上层的抽象层
(2)都不能被实例化
(3 )都能包含抽象方法
(4)抽象类中的抽象方法必须全部被子类实现,如果没有全部实现,那么子类也必须是抽象类。接口中的方法也必须全部被子类实现,如果没有全部实现,子类必须是抽象类。
2.不同点:
(1)方法不同:在抽象类中除了抽象方法之外还可以写非抽象方法(但至少要有一个抽象方法),从而避免在实现类中重复写,提高代码复用性。接口中只能有抽象类(只是方法的声明,没有实现)。
(2)实现方法不同:java中只能单继承,即一个类只能继承一个抽象类。但一个类可以实现多个接口。
(3)抽象程度不同:由高到低,接口>抽象类>实现类。可以说抽象类是接口的中庸之道,我们在使用的时候应该优先选择抽象类。
(4)接口是对动作的抽象,而抽象类是对根源类别的抽象,接口强调的是有没有的关系,抽象类强调的是是不是的关系。例如猴子都会爬树和吃香蕉,这是猴子的共性,可以抽象出抽象类,每个子类都会实现这两个方法。但只有经过训练的猴子才会骑自行车,骑自行车这个方法就不能放在抽象类中,因为子类需要实现所有的抽象方法,明显不是所有猴子都会骑自行车,所有要将这个方法放在一个接口中,让那些会骑自行车的猴子实现这个接口。
(5)设计目的不同:接口是为了对类的行为进行约束,用来规定实现类有什么功能。抽象类是为了代码复用,当不同的类具有相同的行为时,并且这其中还有一部分的行为实现方式一致时,将这些类抽象出一个抽象类,在抽象类中将相同的方法实现,就达到了代码复用的目的。
(6) 接口是隐式抽象的,声明时没有必要使用abstract关键字,接口中的方法也是隐式抽象的,也没有必要使用abstract关键字。接口中的成员变量隐式为static final,而抽象类不是。
好了,再来看上面的类图,既然所有的员工都有获取个人信息和接收通知的方法,就没有必要将它们抽象出接口了,并且这两个方法的实现体在不同的实现类中是相同的,所有将他们抽象出抽象类更能体现代码的复用性。
解决方案
修改类图,将接口改成抽象类,如下:
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:客户端,通过组件接口来操作组合结构里的组件对象。
组合模式的优点:
(1)统一了组合对象和叶子对象,局部和整体对于调用者来说没有区别,所有节点都是Component,例如在上面示例中的通知方法中, for(Staff s:subordinateList)语句中的subordinateList列表中,肯定有组合对象也有叶子对象,即有管理者也有普通员工,我们不需要区分,都看作是Staff类型。有同学会说,骗谁呢,你这里没区分下面不还是判断了吗,下面改写一下,很简单,以getInfo为例,在抽象类中不写实现,在叶子子类中只输出自己的信息,在组合子类中也是先输出自己的信息,然后判断下属列表是否为空,不为空说明还有下属,然后再遍历下属对象并用下属去调用getInfo方法,递归输出每个对象信息。
(2)节点自由添加,想添加一个组合节点或叶子节点只需要找到它的父节点就可以,非常容易扩展,符合开闭原则。
组合模式的缺点:
与依赖倒置原则冲突,就是在客户端创建的时候直接使用了实现类。
组合模式使用场景:
(1) 想要表示对象的部分与整体的层次结构,可以选择组合模式,把整体和部分的操作统一起来,使得层次结构实现更加简单。
(2)如果想统一的使用组合结构中的所有对象,可以选用组合模式。
新的问题
细心的同学应该发现了,既然说组合模式是要让用户对叶子对象和组合对象的使用具有一致性,但是在创建对象的时候却还是使用各自的实现类创建的,这就涉及到组合模式实现的两种方式:安全性和透明性。
安全性:安全性指客户在使用的时候不会发送误操作,能访问的方法都是被支持的方法。
透明性:透明性指客户在使用的时候不需要区分组合对象还是叶子对象。
简单来说,安全性的方式是将对组件的操作(如增加一个子组件,删除一个子组件,获取子组件)定义在组合类Composite中,这样叶子节点对象就不能使用这些功能(本身叶子对应就不应该有这些功能),就不会产生误操作的现象。上面的示例代码都是安全性方式的。但这会带来透明性的问题,客户在使用的时候必须区分到底使用的叶子对象还是组合对象,因为他们的功能是不同的。
透明性方式是将对组件的操作定义在抽象类中,这样客户端只需面对Component,不需要关系具体的组件类型。但这会带来安全性的问题,因为叶子对象也具有了操作组件的方法,客户就有可能误对叶子对象调用这些方法,这样的操作是不安全的。如上面的示例,用这种方式写就是普通员工也有发布通知的功能了,这显然是不允许的。
首先看一下透明方式的类图:
透明性具体代码如下,为了充分体现透明性,代码和上面的改动有些大:
抽象类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.通常在设计组合模式时需要避免这种情况,否则容易出现死循环。但如果真的需要环状引用,就需要构建环状引用,并提供相应的检测和处理。
声明:本文为作者投稿,版权归其所有。
更多精彩推荐
☞AI 世界的硬核之战,Tengine 凭什么成为最受开发者欢迎的主流框架?
☞说了这么多 5G,最关键的技术在这里
☞360金融新任首席科学家:别指望AI Lab做成中台
☞AI图像智能修复老照片,效果惊艳到我了
☞程序员内功修炼系列:10 张图解谈 Linux 物理内存和虚拟内存
☞当 DeFi 遇上 Rollup,将擦出怎样的火花?
你点的每个“在看”,我都认真当成了喜欢