本章阐述的一些原则,将使你设计出更加有用、健壮和灵活的类和
接口。
第十三条:使类和成员的
可访问性最小化
区别设计的模块是否良好,关键在于这个模块对于外部的其他模块而言,是否隐藏其内部数据和其他实现细节。设计良好的模块会把自己的API和具体实现隔离开,只通过API和外部通信,这个概念成为信息隐藏或封装。
之所以信息隐藏这么重要,是基于这样一个事实:它可以有效解除各模块之间的耦合关系,使得这些模块可以独立开发、测试、优化、使用、
理解和修改。这样可以加快开发速度,实现并行开发。
Java提供了许多机制来协助信息隐藏。访问控制决定了类、接口和成员的可访问性。正确使用访问修饰符格外关键。
总之,尽可能降低可访问性,防止把任何散乱的类、接口和成员变成API的一部分。除了公有静态final域的特殊情形,公有类不应该包含公有域,并且确保公有静态final域所引用的对象都是不可变的。
第十四条:在公有类中
使用访问方法而非公有域
如果类可以再它所在的包的外部进行访问,就提供访问方法,以保留将来改变该类的内部表示法的灵活性。如果公有类暴露了它的数据域,要想在将来改变其内部是不可能的,客户端代码早就遍布各处。
然而,如果类是包级私有的,或者是私有的嵌套类,直接暴露它的数据域并没有本质的
错误。虽然客户端代码与该类的内部表示法紧密相连,但是这些代码被限定在包中。
第十五条:使可变性最小化
不可变类只是其实例不能被修改的类。它们比可变的类更易于设计、实现和使用。
为了使类变成不可变类,要遵循下面原则:
1.不要提供任何会修改对象状态的方法;
2.保证类不会被扩展。为了防止子类化,一般做法是使这个类成为final的
3.使所有的域都是final
4.使所有的域都成为私有的。虽然从技术上讲,允许不可变的类具有公有的final域,但不建议这样,除非确定以后不会再有任何修改。
5.确保对于任何可变组件的互斥访问。
不可变类具有很多优点,是目前流行的函数式编程思想的重要组成部分,也是新兴的clojure,scala等语言的亮点。这里不做过多介绍。
第十六条:复合优先于继承
继承是实现代码重用的有力手段,但它绝非是永远完成这项工作的最佳工具。比如,对普通的具体类进行跨越包边界的继承,是非常
危险的。
与方法调用不同,继承打破了封装性。子类依赖于超类中特定功能的实现。并且,子类必须要跟着超类的更新而演变。
如果超类获得了一个新的方法,但你给子类提供了一个签名相同返回类型不同的方法,那么子类将无法通过编译。如果给子类提供的方法和超类具有相同的签名和返回类型,实际上就覆盖了超类的方法。
有一种方法可以避免这些问题。不用扩展现有的类,而是在新的类中添加一个私有域,它引用现有类的一个实例。这种设计被称作“复合”。因为现有的类变成了新类的一个组件。新类中的每个实例方法都可以调用被包含的现有类的方法,并返回结果。这被称作转发,新类中的方法被称作转发方法。
简而言之,只有当子类和超类之间存在子类型关系时,使用继承才是恰当的。即便如此,如果子类和超类处于不同的包中,并且超类并不是为了继承而设计的,那么继承将会导致脆弱性。为了避免,可以使用复合和转发机制来代替继承,尤其是在存在适当的接口可以实现包装类的时候,包装类不仅比子类更加健壮,而且功能更强大。
第十七条:要么为继承而设计并提供文档说明,要么就禁止继承
为了允许继承,类必须遵守一些约束。
构造器决不能调用可被覆盖的方法。
如果你决定在一个为了继承而设计的类中实现Cloneable或者Serializable接口,就应该意识到,因为clone和readObject方法在行为上类似于构造器,所以不能调用可覆盖的方法。此外,如果继承了Serializable,该类中的readResolve或writeReplace方法必须成为受保护的方法,而不是私有方法。
第十八条:接口优于抽象类
因为java只允许单继承,所以,抽象类作为类型定义受到了很大的
限制。任何一个类,只要遵守约定,就被允许实现一个接口。
1.现有的类可以很容易被更新,以实现新的接口。
2.接口是定义mixin(混合类型)的理想选择。
3.接口允许我们构造非层次结构的类型框架。
虽然接口极为灵活的,但接口一旦被公开,并被实现,再想改变几乎是不可能的。所以必须在初次设计的时候就保证接口正确。
第十九条:接口只用于定义类型
当类实现接口时,接口就充当这个类的实例的类型。
有一种接口叫做常量接口,它不包含任
何方法,只有静态的final域,每个域都导出一个常量。但是这种模式是对接口的不良使用。
如果要导出常量,有几种
合理的方法。如果这些常量与某个现有的类或接口紧密相关,就应该把这些常量添加到这个类或接口中。或者编写一个不可实例化的工具类。
总之,接口用来只定义类型,不应该被用来导出常量。
第二十条:类层次优于标签类
第二十一条:用函数对象表示策略
有些语言支持
函数指针的主要用途就是实现策略模式(比如比较器函数)。为了在Java中实现这种模式,要声明一个接口来表示该策略,并且为每个具体策略声明一个实现了该接口的类。当一个具体策略只被使用一次时,通常使用匿名类来声明和实例化这个具体策略类。当一个具体策略是设计用来重复使用的时候,它的类通常就要被实现为私有的静态成员类,并通过公有的静态final域被导出,其类型为该策略接口。
第二十二条:优先考虑静态成员类
嵌套类是指被定义在另一个类的内部的类。嵌套类存在的目的是为外围类提供服务。嵌套类有四种:静态成员类、非静态成员类、匿名类和局部类。除了第一种,其他三种都称为内部类。
静态成员类和非静态成员类语法相似,前者包含static修饰符。但非静态成员类的每个实例都隐含着与外围类的一个外围实例相关联。如果声明成员类不要求返回外围实例,就要选择静态成员类,节省空间和时间。