在糟糕的设计中,数据往往是分散的,甚至是杂乱无章的,就像一群没有意识的野兽,我们无法控制、协调和管理。这种毫无头绪的零散数据,犹如野兽的鲁莽行为,会给系统带来无穷无尽的灾难。随着系统的演进,这场灾难会逐渐蔓延到系统的每一个角落。因此,在面向对象的设计过程中,数据分类是识别对象的前提。但是,一个仅仅封装了数据的对象,如果没有对数据进行操作的行为,仍然是一个没有意识的死对象。我一直认为,只要对象拥有自己的数据,它们就应该是自治的。这种“自治”类似于SOA中的服务自治的概念,但是由于对象应该是合理细粒度的,所以这种自治是有限的自治;或者它体现了专家的自主权。如果对象有足够的数据信息,则必须建立该信息的权限,并且对这些信息的处理应该由对象自己来完成。如果它没有足够的信息,或者根本没有,它可以委托给其他对象。这时,行为就是客体的意识,是客体能够自主的前提。对象自治依赖于面向对象设计的一个重要原则,即对象的数据和行为应该封装在一起。CraigLarman提出的“信息专家模型”就说明了这一点。该模型认为信息的对象是处理这些信息的专家。对象自治是一个非常有趣的概念。我们将物体拟人化,使物体成为社区的基本元素。在这个共同体中,每个对象的行为都应该由自己来控制。无论是完成一个操作,发送一个请求,还是响应一个事件,对象都应该有自己的判断。判断的合理性取决于它所掌握的信息量,以及我们赋予它的意识的灵性。在构建软件系统时,我们的目标是构建这样一个自治对象的社区,而不是无序的混乱世界。每当我们对数据进行操作,发现数据开始出现发散、混乱、模糊、蔓延等特征时,就是对数据进行封装的信号。这些数据无论数量大小,都应该作为对象存在于系统中,并且对象应该具有操作数据的能力。比如在报表系统中,我们尝试将构建好的报表整体导出为一个Excel文件。我们为导出功能定义了一个特殊的接口ExcelTableExporter,它接收一个报表对象和一个工作簿对象,将报表导出到Excel文件:publicinterfaceExcelTableExporter{publicvoidexport(ReportTabletable,WritableWorkbookworkbook);}这个定义没有错界面。然而,当我们实现export()接口方法时,事情开始变得难以管理。我们需要在export()方法中遍历整个报表,获取报表的行标题、列标题和数据单元格,然后计算它们的坐标,获取它们的格式,然后写入到Excel单元格中。显然,ExcelTableExporter要做的事情太多了,它要处理的报表数据也变得纷繁复杂。虽然我们对报表进行了合理的分解和封装,但是坐标还是比较散乱,格式也没有和报表对象一起封装。构成报表的元素对象只有显示的数据值,但不知道应该放在哪里,应该如何显示。也就是说,构成报表的这些对象本身并没有足够的自我意识,这让操作它们的ExcelTableExporter疲惫不堪。需要观察每个报表元素对象的数据,元素之间的依赖关系,考虑如何计算它们的坐标,得到满足客户要求的格式。总之,职责的控制应该是拥有相关数据的报表对象,而不是ExcelTableExporter。如果我们把这个显示和导出报表的功能看成是在Excel画布上绘制报表数据,那么ExcelTableExporter就像一个技艺不高的画家,忙于整体控制和细节描述,却因能力不足而做不到两个都。如果我们让这些构成报表的元素对象具备了自己绘制的能力,是不是就会面目全非呢?这时候ExcelTableExporter只需要将元素对象取出来,放到Excel画布上,它们就知道去哪里,怎么画了。不用担心ExcelTableExporter。根据单一职责原则(SRP),报表元素对象与报表有直接关系,本身不应该承担绘制的责任,但是从导出报表的场景来看也是合理的。而且,与绘图相关的数据本身与报表数据是直接相关的,比如报表元素的坐标是根据报表数据的个数来决定它占据的行数和列数。报告的格式也在报告元数据中设置。但是,从抽象的角度来看,我们应该为它定义不同的接口,这也符合接口隔离原则(InterfaceSegregationPrinciple,ISP)。同时,我们还需要考虑绘制行为的扩展。例如,将来我们可能需要考虑将报告呈现为HTML网页。因此,我们可以定义一个绘制元素的接口:publicinterfaceDrawingElement{publicvoiddraw(ReportCanvascanvas);publicobjectgetElement();}draw()方法负责将报表元素绘制到ReportCanvas对象中。ReportCanvas体现了“画布”的比喻,作为添加绘制报表元素的载体。publicinterfaceReportCanvas{publicvoidaddElement(DrawingElementelement);}对于Excel来说,draw()方法的实现是在内部创建一个cell对象。如果使用开源项目jxl生成excel文件,cell对象可以是Label对象,也可以是jxl.write.Number对象。然而ReportCanvas并不关心这些,它只需要能够添加DrawingElement。这是抽象DrawingElement的好处。报表元素对象实现该接口时,如果导出到Excel,可以将Label、Number等单元格对象封装到实现类中。比如报表中的行头对象可以实现DrawingElement接口:);}el{cell=createLabelCell();}returncell;}}这里的RowHeaderExcelElement类体现了“自主”的思想,因为它知道如何将自己拥有的数据绘制到ReportCanvas中。从功能扩展的角度,如果以后需要支持Html,可以定义一个新的RowHeaderHtmlElement类来实现DrawingElement接口。由于引入了DrawingElement接口,报表元素对象封装了绘图元素对象的数据和行为,使其成为一个自治对象。由于报表元素对象本身具有绘图功能,ExcelTableExporter的工作变得轻松自如,只需要发出一个绘图请求:for(DrawingElementelement:table.getReportUnits()){element.draw(canvas);}职责转移,尽可能从每个对象的角度合理分配职责。原则上可以实现每个对象的“自治”,各司其职,避免出现一个巨大的无所不能的“上帝”对象。【本文为专栏作家“张艺”原创稿件,转载请联系原作者】点此阅读更多该作者好文
