13,使类和成员的可访问性最小化
要设计良好的模块与设计不好的模块,最重要的因素在于,这个模块对外部的其他模块而言,是否隐藏内部数据和其他实现细节。设计良好的模块会把实现细节所有实现细节,把它的API与实现清晰的隔离开来。然后,各模块只通过它们的API进行通信,一个模块不需要知道其他模块的的内部工作情况,这个概念被称为信息隐藏或封装,是软件设计的基本原则之一。
信息隐藏可以有效的解除组成系统各模块之间的耦合关系,使得这些模块可以独立地开发,测试,优化,使用,理解和修改。
提高了软件的可重用性,降低了构建大型项目的风险,因为模块之间联系不紧密。
对于顶层的类和接口,只有两种可能的访问级别:包私有的(package-private)和共有的(public)。
通过把或者接口做成私有,它实际上就成了包实现的一部分,而不该是API的一部分,以后可以对它进行修改替换。
如果把它做成共有的,你就有责任永远支持它,以保持它的兼容性。
如果一个包级私有的顶层类(接口)只是在某一个类的内部被用到,就应该考虑使它成为唯一使用它的那个类的私有类。
- 实例域决不能是共有的,包含共有域的类并不是线程安全的。
- 同样的建议也适用于静态域,只有一种情况例外。假设常量构成了类提供的整个抽象中的一部分。可以通过静态final域来暴露这些常量。
如果final域包含对象的引用,它便拥有非final域的所有缺点。虽然引用本身不会变,但引用的对象却可以修改。
除了共有静态域的特殊情况外,共有类都不应该包含公有域。并且要确保静态final公有域所引用的对象时不可变的。
14,在公有类中使用访问方法而非公有域。
使用getter,setter来代替公有域访问。
- 如果类可以在它的所在的包外部进行访问,就提供访问方法。
- 如果类是包的私有级的,或私有嵌套类,直接暴露它的数据域并没有什么过错。
15,使可变性最小化
不可变类只是其实例不能被修改的类,如String。
理由:
不可变的类比可变的类更加易于设计,实现和使用。它们不容易出错,更安全。
遵循的规则:
- 不要提供任何会修改对象状态的方法。
- 保证类不会被扩展。一般使这个类称为final的。
- 使所有的域都是final的。在缺乏同步机制情况下,实例从一个线程被传到另一个线程,必须确保正确行为。
- 使所有的域都称为私有的。
- 确保对任何可变组件的互斥访问。如果类具有指向可变对象的域,则必须保证客户端无法获得指向这些对象的引用。并且,永远不要用客户端提供的对象来初始化这样的域,也不要从任何访问方法中返回该对象的引用。
不可变对象只有一种状态,即被创建时的状态。使用函数的做法返回一个新的实例,而不是修改这个实例。
对于频繁用得到的值,为他们提供共有的静态final常量。
有关序列化,如果选择让自己的不可变类实现Serializable接口,并且它包含一个或者多个指向可变对象的域,就必须提供一个显式的readObject或者readResolve方法,或者使用ObjectOutputStream.writeUnshred和ObjectOutputStream.readUnshred,即使默认的序列化形式可以接受,也是如此。
16,复合优先于继承
与方法调用不同,继承打破了封装性。换句话说,子类依赖于其超类中特定功能的实现细节。超类的实现可能会变化,这时子类可能遭到破坏。
因而,子类必须要跟着超类更新而演变,除非超类是专门为了扩展而设计的,并且具有很好的文档说明。
导致子类脆弱的一个相关原因是,它们的超类在后续的版本中可以获得新的方法。
为了解决上面这个问题,不用扩展现有的类,而是在新的类中增加一个私有域,它引用现有类的一个实例。这种设计叫做复合,因为现有的类变成了新类的一个组件。新类中的每个实例都可以调用被包含的现有类实例中对应的方法,并返回它的结果。这被称为转发,新类中的方法被称为转发方法。
这样得到的类将会非常稳固,它不依赖于现有的实现细节。即使现有的类添加了新的方法,也不会影响新的类。装饰纸模式就是这样实现的,对现有的类进行装饰,增加了功能。
注意:
包装类几乎没有任何缺点,但它不适合用在回调框架(CallBack FrameWork)中;在回调框架中,对象把自身的引用传给其他对象,用于后续的回调,但被包装的类不知道它的外部包装类。
当只有子类真正是超类的子类型时,才适用继承。对于两个类A和B,只有当两者之间确实存在"is a"关系的时候,类B才扩展类A。如果不能确定每个B确实也是A,那么就不应该扩展。
复合通常情况下,B应该只包含A的一个实例,并且只暴露一个较小的,较简单API:A本质上不是B一部分,只是它的实现细节而已。
在使用继承而不是复合之前,问自己最后一组问题:
- 对于你正在扩展的类,它的API有没有缺陷
- ?如果有,你是否愿意把这些缺陷传到类的API中?
- 继承机制会把超类的所有缺陷都传到子类中,而复合允许设计新的API来隐藏这些缺陷。
17,要么为继承而生,并提供详细的文档,要么就禁止继承
- 首先,该文档必须精确地描述覆盖每个方法所带来的影响。
对于每个共有的方法或者构造器,必须指明调用了哪些可覆盖的方法,是以什么顺序调用的,每个调用结果又是如何影响后续的处理过程。更一般,类必须说明,在哪些情况下回调用可覆盖的方法。
- 为了使程序员能够编写出更加有效的子类,无需承受不必要的痛苦,类必须通过某种形式提供适当的钩子,以便能够进入到它的内部工作流程中,这种形式可以是精心选择的受保护的方法,也可以是受保护的域。
- 构造器决不能调用可被覆盖的方法,无论是直接调用还是间接调用。
- 对于普通的类,它们不是为了子类化而设计和编写文档的。可以禁止子类化:把类声明为final;把构造器变成私有的;或包级私有的,并增加静态工厂来替代构造器;或确保这个类永远不会调用它的任何可覆盖的方法,并在文档中说明。
必须在发布前先编写子类对类进行测试。
经验表明,3个子类通常就可以测试一个可扩展的类。
18,接口优于抽象类
- 现有的类可以很容易被更新,以实现新的接口。
- 接口是定义mixin(混合类型的理想选择)。
mixin是指这样的类型:类除了它的基本类型之外(primary type),还可以实现这个mixin类型,以表明它提供了某些可供选择的行为。例如:Comprarable表明它的实例可以与其他的可比较的对象进行排序。
- 接口允许我们构造非层次结构的类型框架。
接口使得安全地增强类类的功能成为可能。通过对你导出的每个重要接口都提供一个抽象的骨架实现类,把接口和抽像类的有点结合起来。接口任然是定义类型,但是骨架类接管理所有与接口实现相关的工作。(骨架实现类就是抽象类,但抽象方法是由接口确定的基本方法)
19,接口只用于定义类型
不要使用常量接口模式。如果要导出常量用枚举类型添加到这个类或者接口中;或使用不可实例化工具类来导出这些常量。
接口应该只被用来定义类型,不应该用来导出常量。
20,类层次优先于标签类
例如,一个类通过不同构造参数创建圆和方形,可以计算面积。这时可以将它子类化:将共有的面积等抽象到一个类,子类圆和方形分别继承这个类。
多个程序员可以独立地扩展层次结构,并且可以不访问根类源代码就能相互操作。每种类型都有一种相关的独立的数据类型,允许程序员指明变量的类型,限制变量,并将参数输入到特殊的类型。
反映类型之间本质上的层次关系,有助于增强灵活性,并进行更好的编译时类型检查。
21,用函数对象表示策略
- 简而言之,函数指针的主要用途就是策略模式。
- 为了在java中实现这种模式,要声明一个接口来表示策略,并且为每个具体的策略声明一个实现了该接口的类。
- 当一个具体策略只被使用一次,通常使用匿名类来声明和实例化这个具体策略类。
- 当具体策略时设计来重复使用的时候,它的类通常就要被实现为私有的静态成员类,用通过共有的静态final域被导出,其类型为该策略接口。
22,优先考虑静态成员类(讨论嵌套类)
嵌套类是指被指定义在另一个类的内部的类。存在目的应该只是为了它的外围类提供服务。
如果嵌套类将来可能会用于其它的某个环境中,它就应该是顶级类。
嵌套类有四种:静态成员类(static member class);非静态成员类(nonstatic member calss);匿名类(anonymous class);局部类(local class)
除了第一种外,其它三种都被称为内部类(inner class)。
静态成员类:
最好把它当普通的类,只是碰巧被声明在另一个类的内部而已,它可以访问外围类的成员变量,包括哪些私有的成员。静态成员类是外围类的一个静态成员,与其他的静态成员一样,也遵循同样的可访问规则,如果私有只能在内部被访问。
静态成员类的一种常见用法是作为公有的辅助类,仅当它与外部类一起使用时才有意义。
静态成员类可以在它外围实例之外独立存在,而非静态成员类在没有外围实例的情况下不可能创建出实例对象。
- 如果声明成员类不要去访问外围实例,就要始终把static修饰符放在它的声明中,使他成为静态成员类。如果省略了static修饰符,则每个实例都将包含一个额外的指向外围对象的引用。保存这份引用要消耗时间和空间,并且会导致外围实例在符合垃圾回收时仍然得以保留。
- 如果在没有外围实例的情况下,也需要分配实例,就不能用非静态成员类,因为非静态成员类的实例必须要有一个外围实例。
私有静态成员类:
用来代表外围类所代表的对象的组件。
与静态成员类不同,每个外围对象都有一个私有静态成员类关联;与非静态成员变量不同的是,它并不需要访问外围类实例。
匿名类:
匿名类没有名字,它不是外围类的一个成员。它并不与其他成员以前被声明,而是在使用时被声明和实例化。