段湘宁,杨 梅
(葫芦岛市信息中心,125001)
“对接口编程”是面向对象设计(OOD)的第一个基本原则。它的含义是:使用接口和同类型的组件通讯,即对于所有完成相同功能的组件,应该抽象出一个接口,它们都实现该接口。
具体到 Java 中,可以是接口(interface),或者是抽象类(abstract class),所有完成相同功能的组件都实现该接口,或者从该抽象类继承。我们的客户代码只应该和该接口通讯,这样,当我们需要用其他组件完成任务时,只需要替换该接口的实现,而我们代码的其他部分不需要改变。
“优先使用对象组合,而不是类继承”是面向对象设计的第二个原则。并不是说继承不重要,而是因为每个学习面向对象编程(OOP)的人都知道OO 的基本特性之一就是继承,以至于继承己经被滥用了,而对象组合技术往往被忽视了。
下面分析继承和组合的优缺点。
1)类继承允许根据其他类的实现来定义一个类的实现;
2)对象组合是类继承之外的另二种复用选择;
继承和组合各有优缺点。类继承是在编译时刻静态定义的,且可直接使用,类继承可以较方便地改变父类的实现。但是类继承也有一些不足之处。首先,因为继承在编译时刻就定义了,所以无法在运行时刻改变从父类继承的实现。更糟的是,父类通常至少定义了子类的部分行为,父类的任何改变都可能影响子类的行为。如果继承下来的实现不适合解决新的问题,则父类必须重写或被其他更适合的类替换。这种依赖关系限制了灵活性并最终限制了复用性。
对象组合是通过获得对其他对象的引用而在运行时刻动态定义的。由于组合要求对象具有良好定义的接口,而且,对象只能通过接口访问,所以我们并不破坏封装性;只要类型一致,运行时刻还可以用一个对象来替代另一个对象;更进一步,因为对象的实现是基于接口写的,所以实现上存在较少的依赖关系。
优先使用对象组合有助于保持每个类被封装,并且只集中完成单个任务。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物(这正是滥用继承的后果)。另一方面,基于对象组合的设计会有更多的对象(但只有较少的类),且系统的行为将依赖于对象间的关系而不是被定义在某个类中。
在理想情况下,通常不用为获得复用而去创建新的组件,只需要使用对象组合枝术,通过组装已有的组件就能获得需要的功能。但是事实很少如此,因为可用的组件集合并不丰富。使用继承的复用使得创建新的组件要比组装已有的组件来得容易 这样,继承和对象组合常一起使用。然而,比如前面所说,千万不要乱用继承而忽视了对像组合技术。
相 关 的 设 计 模 式 有:Bridge、Composite、Decorator、Observer、Strategy 等。
下面的例子演示了这个规则,这里实现的是:对同一个数据结构,需要以任意的格式输出。
程序 1 使用基于继承的框架,可以看到,它很难维护和扩展。程序 1 使用基子继承的框架
程序 2 使用基于对象组合技术的框架,每个对象的任务都清楚地分离开,我们可以替换、扩展格式类,而不用考虑其他的任何事情。
程序 2 使用给予对象组合技术的框架
这里用到了类似于“抽象工厂”的组件创建模式,它将组件的创建过程交给manager 来完成,Example Formatter 是所有格式的抽象父类。
“将可变的部分和不可变的部分分离”是面向对象设计的第三个原则。如果使用继承的复用技术,我们可以在抽象基类中定义好不可变的部分,再由其子类去具体实现可变的部分。不可变的部分不需要重复定义,而且便于维护。如果使用对象组合的复用技术,我们可以定义好不可变的部分,而可变的部分可以由不同的组件实现,根据需要,在运行时动态配置。这样,我们就有更多的时间关注可变的部分。
对于对象组合技术而言,每个组件只完成相对较小的功能,相互之间耦合比较松散,复用率较高,通过组合,就能获得新的功能。
通常,我们的方法应该只有尽量少的几行,太长的方法会难以理解,而且,如果方法太长,则应该重新设计。对此,可以总结为以下原则。
.三十秒原则:如果另一个程序员无法在三十秒之内了解开发者的函数做了什么 (What),如何做(How)及为什么要这样做(Why),那就说明该代码是难以维护的,必须得到提高;
.一屏原则:如果一个函数的代码长度超过一个屏幕,应该拆分成更小的子函数;
.一行代码应尽量简短,并且保证一行代码只做一件事:那种看似技巧性的冗长代码只会增加代码维护的难度。
要尽量避免在代码中出现判断语句,来测试一个对象是否是某个特定类的实例。通常,如果需要这么做,那么,重新设计可能会有所帮助。比如在使用Java 做XML 解析时,对每个标签映射了一个Java类,采用 SAX(简单的XML接口 API:Simple API for XML)模型。结果,代码中反复出现了大量的判断语句,来测试当前的标签类型。
可以这样来改进,重新设计文档类型定义 DTD(Document Type Definition),为每个标签增加了一个固定的属性“class name”,同时重新设计了每个标签映射的Java 类的接口,统一了每个对象的操作。增加属性;
通过这样的方式,彻底消除了所有的测试当前的标签类型的判断语句。每个对象通过Class.for Name(a Element.attributes.get Attribute(”class name”)).new Instance();动态创建即可。
有大量参数需要传递的方法,通常很难阅读。我们可以将所有参数封装到一个对象中来完成对象的传递,这也有利于错误跟踪。
许多程序员认为,太多层的对象包装对系统效率有影响,但是,和它带来的好处相比,我们宁愿做包装。毕竟,“封装”也是OO 的基本特性之一,而且,“每个对象完成尽量少 (而且简单)的功能”,也是OO 的一个基本原则。
模型中非期望产出的处理。在DEA模型的投入产出要素中,地区生产总值为期望产出,碳排放为非期望产出,期望产出越大越好,非期望产出越少越好,违反了方程的一致性,必须进行处理。本文以非期望产出作为投入的方法处理碳排放问题。
在许多情况下,提供一个抽象基类有利于做特性化扩展。由于在抽象基类中,大部分的功能和行为己经定义好,使我们更容易理解接口设计者的意图是什么。
由于Java 不允许“多继承”。从一个抽象基类继承,就无法再从其他基类继承了。所以,提供一个抽象接口(interface)是个好主意,一个类可以实现多个接口,从而变相地实现了“多继承”,为类的设计提供了更大的灵活性。
对数据的封装原则应该规范化,不要把一个类的属性暴露给其他类,而应该通过访问方法去保护它们,比如设置为私有方法。如果某个属性的名字改变,只需要修改它的访问方法,而不是修改所有相关的代码。
如果一个子类只是使一个组件变成组件管理器,而不是实现接口功能,或者,重载某个功能,那么,就应该使用一个外部的容器类,而不是创建一个子类。程序3 接口定义了组件的功能。
程序3 发送消息类接口定义
发送消息类Msgreceiver 实现了接口,而其子类Pool 只是管理多个Msgreceiver 对象,并没有提供自己的接口实现。使用继承的方式实现的代码如程序4 所示。
程序4 使用继承的方式实现接口
在这种情况下,建议使用组合方式实现,而不是继承的方式,
类的域、方法个数太多时,应该予以适当的拆分。在构建的过程中,会遇到这样的问题:对同样的数据,有不同的视图。某些属性描述的是数据结构怎样生成,而某些属性描述的是数据结构本身。最好将这两个视图拆分到不同的类中,从类的名称上就可以区分出不同视图的作用。
两个方法处理类内部的同一个数据(域),并不意味着它们就是对该数据(域)做处理。许多时候,该数据(域)应该作为方法的参数输入,而不是直接存取,在工具类的设计中尤其应该注意,两个方法都对List 对象pool 做了操作,但是,实际上,我们可能只是想对List接口的不同实现Vector、Array List 等做存取测试。所以,代码应该如程序7 所示。
要在不改动原有代码的前提下使用类。有两种方活:合成(composition)和继承(inheritance)。所有非primitive 对象都有一个to string()方法,当编译器需要一个string 而它却是个对象的时候,编译器就会自动调用这个方法。所以当编译器从”Source =’+Source 中看到,当想把一个字符串同一个非字符串相加的时候,它就会说“由于string 只能同string 相加,因此要调用 source 的to string(),因为只有这样才能把它转换成 string!”。于是就把这两个string 连起来,然后再以string 的形式把结果返还给输出函数system.out.print ln()。如果想让类也具备这个功能,只要写一个to string()的方法就行了。
为每个类都创建一个main()方法,这也是一种值得提倡的编程方法。这样一来,测试代码就都能放进main()方法里了。即使程序包括很多类,它也只会调用我们在命令行下给出的那个类的 main()方法。于是,当输入“java test l”的时候,它就会调用test l.main()。这种在每个类里都放一个main()的做法,能让类的单元测试变得更容易一些。做完测试之后,不必移除main(),留下它可以供以后的测试用。
JSP 的编程开发中,如果遵循这些规范进行开发,逐步养成编写可复用代码的习惯,一定能体会到其中的好处。一个好的编程习惯并不容易养成,可是一旦养成了一个好的编程习惯,无论对自己还是对整个开发团队,都会受益。
[1]JSP 网站开发详解,北京:电子工业出版社2009。