加入收藏 | 设为首页 | 会员中心 | 我要投稿 汽车网 (https://www.0577qiche.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 教程 > 正文

JAVA容器设计的演变史:从白盒变黑盒,至迭代器成为设计的一种模式。

发布时间:2023-04-03 12:33:37 所属栏目:教程 来源:
导读:在我们的项目编码中,不可避免的会用到一些容器类,我们可以直接使用List、Map、Set、Array等类型。当然,为了体现业务层面的含义,我们也会根据实际需要自行封装一些专门的Bean类,并在其中封装集合数据来使用。
在我们的项目编码中,不可避免的会用到一些容器类,我们可以直接使用List、Map、Set、Array等类型。当然,为了体现业务层面的含义,我们也会根据实际需要自行封装一些专门的Bean类,并在其中封装集合数据来使用。

看下面的一个场景:

在一个企业级的研发项目事务管理系统里面,包含很多的项目,每个项目下面又包含很多的具体需求,而每个需求下面又会被拆分出若干的具体事项。

上面的示例场景中,对应的数据结构逻辑可以用下图来表示出来:

按照常规思路,我们会怎么去建模呢?为了简化描述,我们仅以项目--需求--任务这个维度来说明下。

首先肯定会去创建Project(项目)、Requirement(需求)、Task(任务)三个类,然后每个类中会包含一个子对象的集合。比如对于Project而言,会包含一个Requirement的集合:

@Data
public class Project {
    private List<Requirement> requirements;
    private int status;
    private String projectName;
    // ...
}
同样道理,我们定义Requirement的时候,也会包含一个Task的集合:

@Data
public class Requirement {
    private List<Task> tasks;
    private int status;
    private String requirementName;
    private Date createTime;
    private Date closeTime;
    // ...
}
上述的例子中,Project、Requirement便是两个典型的“容器”,容器中会存储着若干具体的元素对象。对容器而言,遍历容器内的元素是无法绕过的一个基本操作。

按照上面的容器对象定义实现,在业务逻辑代码中,需要获取某个Project中所有已关闭的需求事项列表,并按照创建时间降序排列,我们要如何做:先从容器中取出所有的需求集合,然后自行对此需求集合进行过滤、排序等操作。

public List<Requirement> getAllClosedRequirements(Project project) {
    return project.getRequirements().stream()
            .filter(requirement -> requirement.getStatus() ==  1)
            .sorted((o1, o2) -> (int) (o2.getCreateTime().getTime() - o1.getCreateTime().getTime()))
            .collect(Collectors.toList());
}
或者,也可能会写成如下更为通俗的处理逻辑:

public List<Requirement> getAllClosedRequirements(Project project) {
    List<Requirement> requirements = project.getRequirements();
    List<Requirement> resultList = new ArrayList<>();
    for (Requirement requirement : requirements) {
        if (requirement.getStatus() == 1) {
            resultList.add(requirement);
        }
    }
    resultList.sort((o1, o2) -> (int) (o2.getCreateTime().getTime() - o1.getCreateTime().getTime()));
    return resultList;
}
很司空见惯的逻辑,的确也没有什么问题。但是,其实我们仅仅只是需要遍历容器中所有的元素,然后找出符合需要的内容,而Project类通过getRequirements()方法将整个内部存储List对象给出来让调用方直接去操作,存在一定的弊端:

调用方通过project.getRequirements()方法获取到项目下全部的需求列表的List存储对象,然后便可以对List中的元素进行任意的处理,比如新增元素、删除元素甚至是清空List,从可靠性角度而言,我们其实并不希望任何调用方都可以去随意操作所有内容,不确定性太大、难以维护。

某些允许调用方进行遍历并删除元素的场景,容器直接通过project.getRequirements()给出具体的集合对象,然后任由调用方自行遍历并删除,一些调用方可能会处理的不够完善,容易踩坑,存在隐患。可以参见我之前一篇文档《JAVA中简单的for循环竟有这么多坑,你踩过吗》里的详细说明。

进一步思考下,其实我们只是想要遍历获取到容器中的元素,是否有更优雅的方式能够实现这一简单诉求,并且还能顺带解决上述这几个小遗憾呢?

带着疑问,我们一起来梳理下容器的演进历程,聊聊作为一个容器应该具备怎样的自我修养吧。

最直白的白盒容器
如上文中所提供的例子场景。示例中直接通过get方法将容器内管理的元素集合给暴露出去,任由调用方自行去处理使用。调用端需要知道这是一个元素集合是一个List类型还是一个Map类型,然后再根据不同类型,决定应该如何去遍历其中的元素,去对其中的元素进行操作。

白盒容器是一个典型的甩手掌柜式的容器,因为它要做的事情非常简单:给个get方法即可!任何调用方都可以直接获取到容器内部的真正元素存储集合,然后自行去对集合做各种操作,而容器则完全不管。

这样有一定的优势:

调用方限制较小,可以按照自己诉求随意发挥,实现自己各种诉求

容器实现简单,容器与业务解耦,就是个纯粹的容器,不夹杂任何的业务逻辑

但是呢,原本我们只是想遍历下容器中所有的元素内容,但是容器却直接将整个家底都交了出来。这就好比小王去小李家想看看小李家的猪里面有几只是母猪,而小李直接将猪圈丢给了小王,让小王自己进猪圈去数一样,这也太不把小王当外人了不是,谁知道小王进去是不是仅仅只是去数了下有几只母猪呢?

由此带来的弊端也就很明显了:

将容器内部的结构完全暴露给外部,业务逻辑中耦合了容器的具体实现细节,后面如果容器需要改造的时候,会导致业务调用逻辑必须跟着改动,影响较大,牵一发动全身。
举个简单的例子:

当前Project中采用List来存储项目下所有的需求数据,而所有的调用端都是按照List的格式来处理需求数据。如果现在需要将Project中改为使用Map来存储需求数据,则原先所有通过project.getRequirements()获取需求数据的地方,都需要配套修改。

对容器内数据的管控力太弱。容器将数据全盘给出,任由调用方随意的去添加、删除元素、甚至是清空元素集合,而容器却无法对其进行约束。
还是上面的例子:

业务调用方使用project.getRequirements()拿到List对象后,便可以对List进行add、remove、clear等各种操作。而很多时候,我们是需要保证对元素的内容的变更或者增减都在统一的地方去实行,这样可以保证数据的准确、也可以做一些统一处理,比如统一记录创建需求的日志之类的。而写操作入口变得不确定,使得整个数据的维护就存在很大的漏洞。

白盒向黑盒的演进
既然甩手掌柜式的白盒容器有着种种弊端,那么我们将其变为一个黑盒容器,不允许将内部的元素集合和盘托出,这样的话,不就解决上述所有的问题了吗?这个思路是正确的,但是对于一个黑盒容器来说,又该如何让调用端能实现对内部托管的元素的逐个遍历获取呢?

回答这个问题前,我们先来想一个问题:我们对List或者Array是怎么遍历的?可以通过记录下标的方式,按照下标所示的位置去逐个获取下标对应位置的元素,然后将下标往后移动,再去读取下一个位置的元素,一直到最后一个。对应代码我们再熟悉不过了:

public void dealWithRequirements(Project project) {
    List<Requirement> requirements = project.getRequirements();
    for (int i = 0; i < requirements.size(); i++) {
        // ...
    }
}
上述处理逻辑中,有两个关键的数据对遍历的动作起着决定作用。一个是下标索引i,用来标记当前遍历到的元素位置;另一个则是集合的总长度,决定着遍历操作是继续还是终止。

回到当前讨论的黑盒容器中,如果调用方拿不到集合自己去遍历,就需要我们在黑盒容器中代替调用方将上述循环逻辑给自行实现。那么容器自身就需要知晓并记录当前遍历到哪个元素下标位置(也可以将其称为游标位置)。而同样由于黑盒的原因,容器内元素集合的总元素个数、当前遍历到的下标位置等信息,都在黑盒内部,调用方无法知晓,那就需要容器给个接口,告诉调用方是否已经遍历完了(是否还有元素没遍历的)

等等,越说这玩意就越觉得眼熟有木有?这不就是一个迭代器(Iterator)吗?

不错,对一个黑盒容器而言,迭代器可以完美实现对其内部元素的遍历诉求,且不会暴露容器内部的数据结构。迭代器的两个关键方法:

hasNext()
告诉调用方是否还有元素可以继续遍历,如果没有了,则遍历结束,否则继续遍历。

next()
获取一个新的元素内容。

这样,对于调用方而言,无需关注到底容器内部是怎么存储集合数据的,也无需知道到底有多少个集合元素,只需要使用这两个方法,便可以轻松完成遍历。

我们按照迭代器的思路,对Project类进行黑盒化改造,如下:

public class Project {
    private List<Requirement> requirements;
    // ...
    private int cursor;
    public boolean hasNext() {
        return cursor < requirements.size();
    }
    public Requirement next() {
        return requirements.get(cursor++);
    }
}
接着,业务方可以按照下面的方式去遍历:

public void dealWithIterator(Project project) {
    while (project.hasNext()) {
        Requirement requirement = project.next();
        // ...
    }
}
这样的话,在Project内部List类型的requirements对象没有暴露给调用方的情况下,依旧可以完成对Project中所有的Requirement元素的遍历处理,也自然就不用担心调用方会对集合进行元素新增或者删除操作了。此外,后续如果有需要,可以方便地将Project当前内部使用的List类型变更为需要的其它类型,比如Array或者Set等,而不用担心需要同步修改所有外部的调用方处理逻辑。
 

(编辑:汽车网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章