类设计技巧
我们不会面面俱到,也不希望过于沉闷,简单地介绍几点技巧。应用这些技巧可以使你设计的类更能得到专业OOP圈子的认可。
一定要保证数据私有。
这是最重要的;绝对不要破坏封装性。有时候,可能需要编写一个访问器方法或更改器方法,但是最好还是保持实例字段的私有性。很多惨痛的教训告诉我们,数据的表示形式很可能会改变,但它们的使用方式却不会经变化。当数据保持私有时,表示形式的变化不会对类的使用者产生影响,而且也更容易检测bug。
一定要对数据进行初始化。
Java不会为你初始化局部变量,但是会对对象的实例字段进行初始化。最好不要依赖于系统的默认值,而是应该显式地初始化所有的数据,可以提供默认值,也可以在所有构造器中设置默认值。
不要在类中使用过多的基本类型。
这个想法是要用其他的类替换使用多个相关的基本类型。这样会使类更易于理解,也更易于修改。例如,用一个名为Address的新类替换一个Customer类中以下的实例字段:
private String street;
private String city;
private String state;
private int zip;
这样一来,可以很容易地处理地址的变化,例如,可能需要处理国际地址。
不是所有的字段都需要单独的字段访问器和字段更改器。
你可能需要获得或设置员工的工资。而一旦构造了员工对象,肯定不需要更改雇用日期。另外,在对象中,常常包含一些不希望别人获得或设置的实例字段,例如,Address类中的州缩写数组。
分解有过多职责的类。
这样说似乎有点含糊,究竟多少算是“过多”?每个人的看法都不同。但是,如果明显地可以将一个复杂的类分解成两个更为简单的类,就应该将其分解(但另一方面,也不要走极端。如果设计10个类,每个类只有一个方法,显然就有些矫枉过正了)。
下面是一个反面的设计示例。
public class CardDeck // bad design
{
private int[] value;
private int[] suit;
public CardDeck() { . . . }
public void shuffle() { . . . }
public int getTopValue() { . . . }
public int getTopSuit() { . . . }
public void draw() { . . . }
}
实际上,这个类实现了两个独立的概念:一副牌(包含shuffle方法和draw方法)和一张牌(包含查看面值和花色的方法)。最好引入一个表示一张牌的Card类。现在有两个类,每个类完成自己的职责:
public class CardDeck
{
private Card[] cards;
public CardDeck() { . . . }
public void shuffle() { . . . }
public Card getTop() { . . . }
public void draw() { . . . }
}
public class Card
{
private int value;
private int suit;
public Card(int aValue, int aSuit) { . . . }
public int getValue() { . . . }
public int getSuit() { . . . }
}
类名和方法名要能够体现它们的职责。
与变量应该有一个能够反映其含义的名字一样,类也应该如此(在标准类库中,也存在着一些含义不明确的例子,如Date类实际上是一个用于描述时间的类)。
类名命名的惯例
对此有一个很好的惯例:类名应当是一个名词(Order),或者是前面有形容词修饰的名词 (RushOrder),或者是有动名词(有“-ing”后缀)修饰的名词(例如,BillingAddress)。
方法命名的惯例
对于方法来说,要遵循标准惯例:访问器方法用小写get开头(getSalary),更改器方法用小写的set开头(setSalary)。
优先使用不可变的类。
LocalDate类以及java.time包中的其他类是不可变的————没有方法能修改对象的状态。类似plusDays的方法并不是更改对象,而是返回状态已修改的新对象。
更改对象的问题在于,如果多个线程试图同时更新一个对象,就会发生并发更改。其结果是不可预料的。如果类是不可变的,就可以安全地在多个线程间共享其对象。
因此,要尽可能让类是不可变的,这是一个很好的想法。对于表示值的类,如一个字符串或一个时间点,这尤其容易。计算会生成新值,而不是更新原来的值。
当然,并不是所有类都应当是不可变的。如果员工加薪时让raiseSalary方法返回一个新的Employee对象,这会很奇怪。