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

简介:

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

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

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

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

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

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

这一接口的定义并无不妥之处。然而,当我们在实现export()接口方法时,事情开始变得难以控制。我们需要在export()方法中遍历整个报表,获得报表的行头、列头以及数据单元格,然后计算它们的坐标,获得它们的格式,再写入到Excel单元格中。显然,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; } }

倘若将来需要支持Html,可以定义RowHeaderHtmlElement类实现DrawingElement接口。如果二者之间存在一些共同逻辑,则可以提取一个共同的基类RowHeaderElementBase。

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

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

在实现上,我们还有一个问题需要解决。ExcelTableExporter的export()方法实现使用了jxl,DrawingElement类封装的Label或Number对象事实上需要绘制到jxl的WritableSheet中,而不是我们自己抽象的ReportCanvas。为了保证DrawingElment接口的抽象性,以及未来的可扩展性,draw()方法的输入参数必须是与实现无关的抽象类型。如果修改方法的定义为接受WritableSheet对象,就会限定为jxl,无法轻易变更,这是绝对不可取的。它违背了“供应商绑定”的反模式。

由于WritableSheet对象与ReportCanvas之间没有任何关系,强制的类型转换也无法保证将WritableSheet对象传递给DrawingElement对象的draw()方法。除非我们修改WritableSheet的定义,使其实现ReportCanvas接口。但这是不可能的,因为WritableSheet是第三方提供的公开接口,我们不能修改。这时,就需要考虑二者之间的适配。通过运用Adapter模式,我们可以引入一个间接对象WritableSheetAdapter,让其实现ReportCanvas接口,同时重用WritableSheet提供的职责。在jxl中,WritableSheet被定义为接口,通过WritableWorkbook创建。所以,我们可以考虑将WritableWorkbook创建的对象传递给WritableSheetAdapter:

public  class WritableSheetAdapter  implements ReportCanvas {  private WritableSheet sheet;  public WritableSheetAdapter(WritableSheet sheet) {  this.sheet = sheet; } @Override  public  void addElement(DrawingElement element) { sheet.addCell((WritableCell)element.getElement()); } }

WritableSheetAdapter既实现了ReportCanvas接口,同时又组合了WritableSheet对象,完成了WritableSheet到ReportCanvas的适配,使得DrawingElement对象可以接受它:

WritableSheet sheet = workbook.createSheet(sheetName, 0); WritableSheetAdapter sheetAdapter =  new WritableSheetAdapter(sheet);  //遍历报表元素,以数据单元格为例  for (DrawingElement element : table.getCellGroups()) { element.draw(sheetAdapter); }









本文转自wayfarer51CTO博客,原文链接:http://blog.51cto.com/wayfarer/499369 ,如需转载请自行联系原作者
相关文章
|
12天前
|
安全 中间件 数据安全/隐私保护
中间件的定义,包括它的功能、应用场景以及优势。
中间件是位于操作系统和应用软件间的系统软件,提供数据交换、应用集成、流程管理和安全保障等服务。常用于分布式系统、微服务架构和企业级应用,实现高效、低耦合的系统运行。其优势在于降低开发成本、提升系统性能、简化扩展和维护。中间件推动了软件技术的发展和创新。
15 1
|
1月前
|
设计模式 uml C++
C++中的装饰器模式:灵活地扩展功能
C++中的装饰器模式:灵活地扩展功能
34 0
|
1月前
|
安全 编译器 程序员
C++对C的扩展(下)
C++对C的扩展
29 0
|
2月前
|
设计模式 Java
装饰者模式:打破继承限制,实现灵活的功能扩展
装饰者模式:打破继承限制,实现灵活的功能扩展
29 0
|
6月前
运放扩展专题
运放扩展是指通过在运放电路中添加一些外部元器件,来扩展运放的功能和性能。运放扩展技术在电子领域中应用广泛,能够实现各种电路的设计和优化。 运放扩展技术的基本原理: 运放扩展技术的基本原理是在运放电路中添加一些外部元器件,改变运放的输入、输出特性,从而实现各种电路的设计和优化。常见的运放扩展技术包括负反馈、正反馈、积分、微分、比较器、滤波等。 负反馈: 负反馈是一种常用的运放扩展技术,它可以改变运放的增益、输入电阻和输出电阻等特性,从而实现电路的设计和优化。负反馈电路一般由运放、反馈电阻和输入电阻组成,通过改变反馈电阻和输入电阻的比例,可以改变电路的增益和输入电阻。 正反馈: 正反馈是一种
18 0
|
1月前
|
安全 程序员 编译器
C++对C的扩展(上)
C++对C的扩展
31 0
|
1月前
模块功能复用和扩展性
模块功能复用和扩展性 模块功能复用和扩展性是软件工程中的重要概念,主要体现在设计和实现阶段。
19 1
|
2月前
|
搜索推荐 安全 定位技术
产品服务功能特性
产品服务功能特性
23 3
|
4月前
|
SQL XML JSON
Hasor【部署 04】Dataway接口配置服务扩展能力实例分享
Hasor【部署 04】Dataway接口配置服务扩展能力实例分享
53 0
|
6月前
|
设计模式 Java
JAVA设计模式7:适配者模式,彻底解决两不兼容接口之间的问题
JAVA设计模式7:适配者模式,彻底解决两不兼容接口之间的问题

热门文章

最新文章