对象的自治和行为的扩展与适配

在坏的设计中,数据往往是分散的,甚至是杂乱的,这就好像一群失去意识的猛兽,我们无法控制、协调以及管理它们。这种漫无头绪的散乱数据,犹如猛兽的肆意妄为,会给系统带来无尽的灾难。随着系统的演化,这种灾难会逐渐蔓延至系统的各个角落。因此,在面向对象设计过程中,对数据分类是识别对象的一个前提。但是,仅仅封装了数据的对象,如果没有操作数据的行为,仍旧是没有意识的死亡对象。

我始终认为,对象在拥有自己数据的情况下,应该是自治的。这种“自治”类似于SOA中服务自治的概念,但由于对象应该保持足够合理的细粒度,因此这种自治是有限度的自治;或者说它体现的是专家的自治。如果对象拥有足够的数据信息,就必须树立这些信息的权威,这些信息的处理就应该由对象自己来完成。如果它拥有的信息量不够,或者根本不具备,则可以委派给其他对象。此时,行为即对象的意识,是对象能够自治的前提。

对象自治依赖于面向对象设计的一个重要原则,即对象的数据与行为应该封装在一起。Craig Larman提出的“信息专家模式”正是说明了这一点,该模式认为拥有信息的对象才是处理这些信息的专家。

对象自治是一个很有趣的概念,我们把对象拟人化,使得对象成为组成社区的基本元素。在这个社区里,每个对象的行动都应该由自己来控制。无论是完成某个操作,还是发出请求,或者响应事件,对象都应该有自己的判断。判断的合理性来自于它掌握的信息量,以及我们赋予它的意识的灵性。在构建软件系统时,我们的目标就是要搭建这样一个由自治对象组成的社区,而不是无序的混沌世界。每当我们在操作数据时,发现数据开始具有发散、混乱、模糊、蔓延等特征时,就是封装数据的信号。不管这些数据的数量,还是大小,它都应该作为对象存在于系统,同时该对象应具备操作该数据的能力。

例如在报表系统中,我们试图将构建好的报表整体导出为Excel文件。我们为导出功能定义了专门的接口ExcelTableExporter,它接收一个报表对象和工作薄对象,导出报表到Excel文件中:

public interface ExcelTableExporter {
    public void export(ReportTable table, WritableWorkbook workbook);
}

这一接口的定义并无不妥之处。然而,当我们在实现export()接口方法时,事情开始变得难以控制。我们需要在export()方法中遍历整个报表,获得报表的行头、列头以及数据单元格,然后计算它们的坐标,获得它们的格式,再写入到Excel单元格中。显然,ExcelTableExporter要做的事情太多了,而它所要处理的报表数据也开始变得发散而混乱。

虽然我们对报表进行了合理的分解与封装,但坐标依旧是散乱的,格式也没有和报表对象封装在一起。组成报表的元素对象仅仅拥有展现的数据值,却不知道自己该放在哪个位置,又该以什么面貌展现。换言之,这些组成报表的对象都不具备充分的自主意识,使得操作它们的ExcelTableExporter心力憔悴。它需要观察每个报表元素对象的数据,元素之间的依赖关系,考虑如何计算它们的坐标,获得符合客户要求的格式。

简言之,职责的控制权应该是拥有相关数据的报表对象,而不应该是ExcelTableExporter

如果我们将这种展现和导出报表的功能看做是将报表数据绘制在Excel画布上,那么ExcelTableExporter就好似一位不太高明的画师,奔忙于全局的掌控与细节的刻画,却因为能力不够而无法二者兼顾。

如果我们让这些组成报表的元素对象拥有绘制自身的能力,境况是否焕然一新呢?此时,ExcelTableExporter只需要取出元素对象,放在Excel画布上,它们自己就知道该往哪儿去,该怎么绘制,根本不用ExcelTableExporter来操心。

根据单一职责原则(SRP),报表元素对象与报表直接相关,本身不应该承担绘制的责任,但放在导出报表这个场景来看,却又是合乎情理的。而且,与绘制相关的数据本身就与报表数据直接相关,例如报表元素的坐标,就依赖于报表数据的个数,以决定它占用的行数和列数。报表的格式同样设置在报表元数据中。不过,从抽象的角度来看,我们应该为其定义不同的接口,这也符合接口隔离原则(ISP)。同时,我们还需要考虑绘制行为的扩展。

例如,在未来我们可能需要考虑将报表绘制为HTML网页。因此,我们可以定义一个绘制元素的接口:

public interface DrawingElement {
    public void draw(ReportCanvas canvas);
    public object getElement();
}

draw()方法负责将报表元素绘制到ReportCanvas对象中。ReportCanvas体现了“画布”的隐喻,作为载体用来添加绘制出来的报表元素。

public interface ReportCanvas {
    public void addElement(DrawingElement element);
}

对于Excel而言,实现draw()方法就是在内部创建单元格对象。如果使用开源项目jxl来完成excel文件的生成,则该单元格对象可以是Label对象,也可以是jxl.write.Number对象。不过,ReportCanvas是不关心这些的,它只需要能够添加DrawingElement即可。这里就体现出了抽象DrawingElement的好处。

当报表元素对象在实现该接口时,如果是针对Excel的导出,就可以把诸如Label和Number这样的单元格对象封装到实现类中。例如报表中的行头对象就可以实现DrawingElement接口:

public class RowHeaderExcelElement implements DrawingElment{
    private object cell;

    @Override
    public void draw(ReportCanvas canvas) {
        canvas.addElement(this);
    }

    @Override
    public object getElement() {
        if (isNumber()) {
            cell = createNumberCell();
        } else {
            cell = createLabelCell(); 
        }
        return cell;
    }
}

这里的RowHeaderExcelElement类就体现了“自治”思想,因为它自己知道该如何将自己拥有的数据绘制到ReportCanvas。

而从功能扩展的角度讲,如果将来需要支持Html,就可以定义新的RowHeaderHtmlElement类实现DrawingElement接口。

因为引入了DrawingElement接口,报表元素对象就将绘制元素对象的数据与行为都封装了起来,使其成为了自治的对象。由于报表元素对象自身具备绘制功能,使得ExcelTableExporter的工作变得轻松自如,只需发出绘制的请求即可:

for (DrawingElement element : table.getReportUnits()) {
    element.draw(canvas);
}

通过合理地将职责进行转移,尽可能站在每个对象自身的角度进行合理的职责分配,从原则上实现各个对象的“自治”,就能够各司其职,避免出现一个庞大的无所不能的“上帝”对象。

原文发布于微信公众号 - 逸言(YiYan_OneWord)

原文发表时间:2017-11-27

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏WindCoder

网易MySQL微专业学习笔记(十一)-MySQL业务优化与设计

这个系列属于个人学习网易云课堂MySQL数据库工程师微专业的相关课程过程中的笔记,本篇为其“MySQL业务优化与设计”中的MySQL数据类型相关笔记。

341
来自专栏Java Edge

2 数据库结构优化

3106
来自专栏Java编程技术

UML建模(类图)

类图是面向对象系统建模中重要的图,是定义其它图的基础。类图主要是用来展现软件系统中的类、接口以及它们之间的静态结构。

642
来自专栏java思维导图

自下向上的编写容易阅读的代码(上)

我在 关于极简编程的思考 中曾提到要编写可阅读的代码。因为代码是编写一次,阅读多次。 阅读者包括代码编写者,以及后来的维护人员。能让阅读代码更轻松,有利于增强项...

3328
来自专栏Linyb极客之路

简洁又快速地处理集合——Java8 Stream(上)

本篇文章主要是让大家能够理解 Stream,理解它的基本原理,理解我们为什么需要使用 Stream 以及它的好处,而具体的实战环节我会在下篇文章中讲解。

983
来自专栏哈雷彗星撞地球

设计模式简介

设计模式(Design pattern)是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。 设计模式代表了最佳的实践,通常被有经验的面向对...

704
来自专栏瓜大三哥

UVM(十二)之各register model

UVM(十二)之各register model 1. register model的必要性 考虑一个问题,当验证平台向DUT发了某些激励后,我们期望DUT中的某...

20610
来自专栏微信公众号:Java团长

一位Java工程师的阶段性工作总结

1.1.1、通常的模块分布:一般如果你要实现一个web应用,你从后台将数据展示到前端页面,在一个比较大的公司,你少不了跟其他项目有交集(你调用他的接口,他依赖你...

601
来自专栏数据和云

性能优化:B*Tree 索引中的数据块分配(五)

黄玮(Fuyuncat) 资深Oracle DBA,个人网www.HelloDBA.com,致力于数据库底层技术的研究,其作品获得广大同行的高度评价. 编辑手记...

2626
来自专栏Java与Android技术栈

响应式和函数式,两个容易混淆的概念

传统的编程方式,是顺序执行的,上一个任务没有完成的话需要等待直至完成之后才会执行下一个任务。无论是提升机器的性能还是代码的性能,本质上都需要依赖上一个任务的完成...

711

扫描关注云+社区