第7章 复用类
复用代码是Java众多引人注目的功能之一。
复用的前提是:使用类而不改变现有程序代码。在本章中,我们将介绍两种方式:
- 组合:在新类中添加现有类的对象。
- 继承:按照现有类的类型来创建新类。
- 代理:继承与组合之间的中庸之道。
7.1 组合语法
使用组合技术,只需将对象引用置于新类中即可。 例如:
class WaterSource {
private String s;
public WaterSource() {
System.out.println("WaterSource");
s = "Constructed";
}
public String toString() {
return s;
}
}
public class SprinklerSystem {
private String v1, v2, v3, v4;
private WaterSource source = new WaterSource();
private int i;
private float f;
public String toString() {
return "v1 = " + v1 + " " +
"v2 = " + v2 + " " +
"v3 = " + v3 + " " +
"v4 = " + v4 + "\n" +
"i = " + i + " " + "f = " + f + " " +
"souce = " + source;
}
public static void main(String[] args) {
SprinklerSystem sprinklers = new SprinklerSystem();
System.out.println(sprinklers);
}
}
上述代码中有个特殊的方法:toString()。每个非基本类型的对象都有该方法,并且当编译器需要一个String而只有对象时,该方法便会被调用。
结果显示,正如第二章介绍的:类中域为基本类型时能够自动被初始化为零,对象引用会被初始化为null,试图直接使用它们,则会得到一个异常。
初始化引用有以下几种方式,按照初始化顺序分别为:
- 在定义对象处。
- 实例初始化。
- 在类的构造器中。
- 惰性初始化:在正要使用这些对象前。
下例分别采用这几种方式进行了初始化:
class Soap {
private String s;
Soap() {
System.out.println("Soap");
s = "Constructed";
}
public String toString() {
return s;
}
}
public class Bath {
private String s1 = "Happy",
s2 = "Happy",
s3,s4;
private Soap castille;
private int i;
private float toy;
public Bath(){
System.out.println("Inside Bath()");
s3 = "Joy";
toy = 3.14f;
castille = new Soap();
}
{
i = 47;
}
public String toString(){
if(s4 == null)
s4 = "Joy";
return "s1 = " + s1 + "\n" +
"s2 = " + s2 + "\n" +
"s3 = " + s3 + "\n" +
"s4 = " + s4 + "\n" +
"i = " + i + "\n" +
"toy = " + toy + "\n" +
"castille = " + castille;
}
public static void main(String[] args) {
Bath b = new Bath();
System.out.println(b);
}
}
7.2 继承语法
继承是所有OOP语言不可缺少的组成部分。在Java中,每当创建一个类,都发生了继承,因为该类会隐式地继承Object。
继承的语法使用关键字extends实现,例如:
class Cleanser {
private String s = "Cleanser";
public void append(String a) { s += a; }
public void dilute() { append(" dilute()"); }
public void apply() { append(" apply()"); }
public void scrub() { append(" scrub()"); }
public String toString() { return s; }
public static void main(String[] args) {
Cleanser cleanser = new Cleanser();
cleanser.dilute();
cleanser.apply();
cleanser.scrub();
System.out.println(cleanser);
}
}
public class Detergent extends Cleanser {
public void scrub() {
append(" Detergent.scrub()");
super.scrub();
}
public void foam() {
append(" foam()");
}
public static void main(String[] args) {
Detergent d = new Detergent();
d.dilute();
d.apply();
d.scrub();
d.foam();
System.out.println(d);
}
}
该程序使用+=操作符拼接字符串,并且在两个类中均含有main()方法。但只有命令行所调用的那个类,main()方法才会被调用。例如,使用命令java Detergent 或java Cleanser。
由于Detergent是由关键字extends从Cleanser导出的,所以它可以自动获得基类中所有它能访问的方法,尽管这些方法没有在导出类中显式定义。
正如scrub()方法所示:使用基类中定义的方法及对它进行修改是可行的。并且,在新版本中可以调用基类继承而来的方法,此时,需要使用关键字super:超类。
在导出类中,并非只能使用基类定义过的方法,也可添加新方法。例如foam()方法。
7.2.1 初始化基类
继承涉及了基类和导出类两个类,从外部看,导出类像是一个与基类具有相同接口的新类,或许还会有一些额外的方法和域。
但并非如此,当创建了一个导出类的对象时,在导出类的构造器中会自动调用基类构造器。该对象包含了一个基类的子对象,它与直接创建的基类对象一样,不过后者来自于外部,前者被包装在导出类对象内部。
下面,展示了上述机制在继承关系上是如何工作的:
class Art {
Art() {
System.out.println("Art constructor");
}
}
class Drawing extends Art {
Drawing() {
System.out.println("Drawing constructor");
}
}
public class Cartoon extends Drawing {
public Cartoon() {
System.out.println("Cartoon constructor");
}
public static void main(String[] args) {
Cartoon x = new Cartoon();
}
}
可以发现,构建过程是从基类向外扩散的,即基类构造器在导出类构造器调用前被调用。并且,默认构造器会自动调用基类构造器。
带参数的构造器
如果没有默认的基类构造器,或者想要调用一个带参数的基类构造器,就必须使用关键super显式地编写调用基类构造器语句,并且配以适当的参数列表:
class Game {
Game(int i) {
System.out.println("Game Constructor");
}
}
class BoardGame extends Game {
BoardGame(int i) {
super(i);
System.out.println("BoardGame Constructor");
}
}
public class Chess extends BoardGame {
public Chess() {
super(11);
System.out.println("Chess Constructor");
}
public static void main(String[] args) {
Chess chess = new Chess();
}
}
如果不在BoardGame()中调用基类构造器,编译器则报无法找到符合Game()形式的构造器。并且,调用基类构造器的语句必须是在导出类构造器中的第一条语句,否则编译器会报错。
7.3 代理
Java并没有提供对代理的直接支持。它是继承和组合之间的中庸之道:将目标类对象置于代理类中,并在代理类中暴露目标类的所有方法。 例如,太空船需要一个控制模块:
public class SpaceShipControls {
void up(int velocity) {}
void down(int velocity) {}
void left(int velocity) {}
void right(int velocity) {}
void forward(int velocity) {}
void back(int velocity) {}
void turboBoost() {}
}
构造太空船的一种方式为继承:
public class SpaceShip extends SpaceShipControls{
private String name;
public SpaceShip(String name) {
this.name = name;
}
public String toString(){
return name;
}
public static void main(String[] args) {
SpaceShip protector = new SpaceShip("NSEA Protector");
protector.forward(100);
}
}
上面做法的缺陷是:基类所有的方法在导出类中完全暴露了。代理可以解决这个问题:
public class SpaceShipDelegation {
private String name;
private SpaceShipControls controls = new SpaceShipControls();
public SpaceShipDelegation(String name) {
this.name = name;
}
public void up(int velocity) {
controls.up(velocity);
}
public void down(int velocity) {
controls.down(velocity);
}
public void left(int velocity) {
controls.left(velocity);
}
public void right(int velocity) {
controls.right(velocity);
}
public void forward(int velocity) {
controls.forward(velocity);
}
public void back(int velocity) {
controls.back(velocity);
}
public void turboBoost() {
controls.turboBoost();
}
public static void main(String[] args) {
SpaceShip protector = new SpaceShip("NSEA Protector");
protector.forward(100);
}
}
使用代理可以让我们拥有更多的控制力,我们可以选择只提供在目标类中的方法的子集,并可以对这些方法做些加强。
7.4 结合使用组合和继承
同时使用组合和继承是很常见的事,例如:
class Plate {
Plate(int i) {
System.out.println("Plate constructor");
}
}
class DinnerPlate extends Plate {
DinnerPlate(int i) {
super(i);
System.out.println("DinnerPlate constructor");
}
}
class Utensil {
Utensil(int i) {
System.out.println("Utensil constructor");
}
}
class Spoon extends Utensil {
Spoon(int i) {
super(i);
System.out.println("Spoon constructor");
}
}
class Fork extends Utensil {
Fork(int i) {
super(i);
System.out.println("Fork constructor");
}
}
class Knife extends Utensil {
Knife(int i) {
super(i);
System.out.println("Knife constructor");
}
}
class Custom {
Custom(int i) {
System.out.println("Custom constructor");
}
}
public class PlaceSetting extends Custom{
private Spoon spoon;
private Fork fork;
private Knife knife;
private DinnerPlate plate;
PlaceSetting(int i) {
super(i +1);
spoon = new Spoon(i+2);
fork = new Fork(i+3);
knife = new Knife(i+4);
plate = new DinnerPlate(i+5);
System.out.println("PlaceSetting constructor");
}
public static void main(String[] args) {
PlaceSetting setting = new PlaceSetting(9);
}
}
编译器虽然强制初始化基类,但并未提醒用户必须初始化成员对象,因此,这需要自己注意。并且,通过将组合和继承同时使用,可以更好地将类与类清晰地分离开,以便更加容易地复用这些代码。
7.4.1 确保正确清理
在Java中,我们习惯忘记对象并让垃圾回收器在必要时释放其内存,而不是手动销毁对象,。但我们并不清楚垃圾回收器何时,或是否会被调用,因此,如果要某个类清理一些东西,则必须显式的编写一个特殊方法,并确保客户端程序员知晓他们必须要调用这一方法。
例如,下例是能在屏幕上绘制图案的计算机辅助设计系统:
class Shape {
Shape(int i) {
System.out.println("Shape constructor");
}
void dispose() {
System.out.println("Shape dispose");
}
}
class Circle extends Shape {
Circle(int i) {
super(i);
System.out.println("Drawing Circle");
}
void dispose() {
System.out.println("Erasing Circle");
super.dispose();
}
}
class Triangle extends Shape {
Triangle(int i) {
super(i);
System.out.println("Drawing Triangle");
}
void dispose() {
System.out.println("Erasing Triangle");
super.dispose();
}
}
class Line extends Shape {
private int start,end;
Line(int start,int end) {
super(start);
this.start = start;
this.end = end;
System.out.println("Drawing Line: " + start +", " + end );
}
void dispose() {
System.out.println("Erasing Line: " + start +", " + end );
super.dispose();
}
}
public class CADSystem extends Shape{
private Circle circle;
private Triangle triangle;
private Line[] lines = new Line[3];
public CADSystem(int i) {
super(i + 1);
for (int j = 0; j < lines.length; j++)
lines[j] = new Line(j, j*j);
circle = new Circle(1);
triangle = new Triangle(1);
System.out.println("Combined constructor");
}
public void dispose() {
System.out.println("CADSystem.dispose()");
triangle.dispose();
circle.dispose();
for (int i = 0; i < lines.length; i++)
lines[i].dispose();
super.dispose();
}
public static void main(String[] args) {
CADSystem x = new CADSystem(47);
try{
// Code and exception handling...
}finally{
x.dispose();
}
}
}
此系统中的一切都是某种Shape,每个类都覆写了Shape的dispose()方法,并运用super来调用该方法的基类版本。该方法的作用是:将未存于内存之中的东西恢复到对象存在之前的状态。并且,需要注意该方法中清理的顺序:首先清理当前类,然后再调用基类的清理方法。
在main()方法中,我们可以到看有两个关键字:try和finally。try表示下面的块是保护区,需要被特殊处理。而finally则代表,无论try块发生什么,finally子句总会被执行。
由于垃圾回收器可能永远也不会被调用,即使被调用,它也可能以任何顺序来回收对象。所以,最好的办法是:除了内存外,不能依赖垃圾回收器做任何事,如果需要清理,最好是编写自己的清理方法,但不要使用finalize()。
7.4.2 名称屏蔽
如果基类拥有某个已被重载多次的方法,那么在导出类中重新定义该方法的其他重载形式,并不会屏蔽其在基类中的任何版本:
class Homer {
char doh(char c){
System.out.println("doh(char)");
return 'd';
}
float doh(float c){
System.out.println("doh(float)");
return 1.0f;
}
}
class MilHouse{}
class Bart extends Homer{
void doh(MilHouse m){
System.out.println("doh(MilHouse)");
}
}
public class Hide {
public static void main(String[] args) {
Bart bart = new Bart();
bart.doh(1);
bart.doh('x');
bart.doh(1.0f);
bart.doh(new MilHouse());
}
}
可以看到,虽然导出类中引入了一个新的重载方法,但导出类对象依然可以调用基类中的重载方法。 不过,当导出类和基类中的方法签名与返回类型完全相同时,则会发生覆盖。
7.5 在组合与继承之间选择
组合和继承都允许在新的类中放置子对象,组合是显式的这样做,而继承则是隐式地做。
组合的使用场景:
组合技术通常用于在新类中使用现有类的功能:新类与现有类的接口完全不同,现有类可能可以满足新类的某个或多个功能。 此时,只需在新类中嵌入一个现有类的private对象。
有时,允许类的用户直接访问新类中的组合成分也是极具意义的。当用户能够了解到所组装的部件时,会使设计更加易于理解。 如下面的例子所示:
class Engine {
public void start() {}
public void rev() {}
public void stop() {}
public void service() {
System.out.println("service start");
}
}
class Wheel {
public void inflate(int psi) {}
}
class Window {
public void rollup() {}
public void rolldown() {}
}
class Door {
public Window window = new Window();
public void open() {}
public void close() {}
}
public class Car {
public Engine engine = new Engine();
public Wheel[] wheels = new Wheel[4];
public Door left = new Door(),
right = new Door();
public Car() {
for (int i = 0; i < wheels.length; i++) {
wheels[i] = new Wheel();
}
}
public static void main(String[] args) {
Car car = new Car();
car.left.window.rollup();
car.wheels[0].inflate(72);
}
}
在上例中,car的组合也是问题分析的一部分,所以将成员置为public有助于客户端程序员了解如何使用该类,并降低了类开发所面临的代码复杂度。但这仅仅是个特例,一般情况下应该使域称为private,以便更好地实现封装。
继承的使用场景:
当你拥有一个通用类,并为了某种特殊需求而要将其特殊化时,就需要使用继承。
7.6 protected关键字
了解了继承后,关键字protected也具有了其意义:就类用户而言,这是private的,但对于任何继承于此类的导出类或其他任何位于同一个包内的类来说,它却是可以访问的。
尽管可以创建protected域,但最好的方式还是将域保持为private,以便我们一直具有更改底层实现的权利。 不过,我们可以通过protected来控制类的继承者对方法的访问权限:
class Villain{
private String name;
protected void set(String name){
this.name = name;
}
public Villain(String name) {
this.name = name;
}
public String toString() {
return "Villain [name=" + name + "]";
}
}
public class Orc extends Villain{
private int orcNumber;
public Orc(String name,int orcNumber) {
super(name);
this.orcNumber = orcNumber;
}
public void change(String name,int orcNumber){
set(name);
this.orcNumber = orcNumber;
}
public String toString() {
return "Orc [orcNumber=" + orcNumber + "] " + super.toString();
}
public static void main(String[] args) {
Orc orc = new Orc("Limburger", 12);
System.out.println(orc);
orc.change("Bob", 19);
System.out.println(orc);
}
}
可以发现,由于set()为protected,所以导出类中的change()方法可以访问基类的set()方法。
7.7 向上转型
继承技术不仅仅是为新类提供方法,其最重要的是用来表现导出类和基类之间的关系:导出类是基类的一种类型。并且,这是直接由语言支撑的。由于继承可以确保基类中的所有方法在导出类中也同样有效,所以向基类发送的所有消息同样也可以向导出类发送。 例如:
class Instrument{
public void play(){}
static void tune(Instrument i){
//...
i.play();
}
}
public class Wind extends Instrument{
public static void main(String[] args) {
Wind flute = new Wind();
tune(flute);
}
}
在此例中,tune()方法参数为Instrument类型,但在main()方法中,将Wind引用传递给该方法并未出错。实际上,Wind也是一种Instrument类型,并且,Instrument中的任何方法,Wind中都存在。在传递参数过程中,其实将Wind引用转换为了Instrument,即向上转型。
7.7.1 为什么称为向上转型
在继承图的典型布局方式中:基类在顶部,导出类在其下部散开。导出类转型为基类,就是在继承图中向上移动,即"向上转型"。
由于向上转型是从一个专有类型向通用类型的转换,所以总是很安全的。即:导出类可能比基类含有更多的方法,但它至少具备基类中所含的方法。
7.7.2 再论组合与继承
在面向对象过程中,我们经常使用的编程方法有:
- 直接将数据和方法包装进一个类中,并使用该类的对象。
- 运用组合技术使用现有类来开发新类。
- 当需要将新类向基类进行向上转型时,使用继承。
7.8 final关键字
Java的关键字final的含义为:无法改变的。
下面谈论了可能使用到final的三种情况:数据、方法和类。
7.8.1 final 数据
许多编程语言都有某种方法,来想编译器告知一块数据是恒定不变的。在Java中,使得成员数据恒定不变的场景如下:
-
一个永不改变的编译时常量:该必须是基本类型,并且以关键字final表示,在定义时必须对其赋值,并且在运行期间,其数值无法被修改。
-
一个在运行时被初始化的值,而并不希望其被改变:当该变量为对象引用时,其保证的是该引用恒定不变,但其引用所指向的对象本身却依然可以被修改。
下面的示例示范了final域的情况:
class Value{
int i;
public Value(int i) {
this.i = i;
}
}
public class FinalData {
private static Random random = new Random(66);
private String id;
public FinalData(String id) {
this.id = id;
}
private final int valueOne = 9;
private static final int VALUE_TWO = 99;
public static final int VALUE_THREE = 39;
private final int i4 = random.nextInt(20);
static final int INT_5 = random.nextInt(20);
private Value v1 = new Value(11);
private final Value v2 = new Value(22);
private static final Value VAL_3 = new Value(33);
private final int[] a = { 1, 2, 3, 4, 5, 6 };
public String toString() {
return id + ": " + "i4 = " + i4 + ", INT_5 = " + INT_5 ;
}
private static final int x = random.nextInt(100);
private final int y = random.nextInt(100);
public static void main(String[] args) {
FinalData fd1 = new FinalData("fd1");
//! fd1.valueOne++; //Error: can't change value
fd1.v2.i++;
fd1.v1 = new Value(9);
for (int i = 0; i < fd1.a.length; i++) {
fd1.a[i]++;
//! fd1.a = new int[3];
//! fd1.v2 = new Value(0);
}
FinalData fd2 = new FinalData("fd2");
System.out.println(fd1);
System.out.println(fd2);
}
}
上述代码中,valueOne、VALTWO和VALTHREE均为编译期常量,但VAL_THREE是编译期常量的一种最为典型的定义方式:
- public:该变量可被外界直接访问。
- static:该变量只存在一份。
- final:该变量为常量。
- 使用大写字母命名,并且字与字之间用下划线隔开。
并不是所有final修饰的基本类型数值在编译期都知道它的值。 例如上例中的:i4和INT_5,它们都是 在运行时使用随机生成的数值初始化的。
由于INT_5被static修饰,只在第一次加载Class对象时被初始化,而不是每次创建新对象时被初始化。 所以,我们可以发现fd1和fd2的两个实例中,i4的数值是不同的,INT_5 的值是相同的。
当final作用于引用时,我们无法修改该引用再次指向其他对象,但却可以修改该引用所指对象本身。并且,数组也是一种引用。
空白final
所谓空白final指:声明为final但又未在定义时给定初值的域。但无论如何,编译器确保任何被final修饰的数据在使用前必须被初始化。 例如:
class Poppet{
private int i;
public Poppet(int i) {
this.i = i;
}
}
public class BlankFinal {
private final int i =0;
private final int j ;
private final Poppet p;
public BlankFinal() {
j = 1;
p = new Poppet(1);
}
public BlankFinal(int x) {
this.j = x;
this.p = new Poppet(x);
}
public static void main(String[] args) {
new BlankFinal();
new BlankFinal(66);
}
}
正如上例所示,编译器强制必须在域的定义处或每个构造器中用表达式对final进行赋值。
final参数
Java允许在参数列表中以声明的方式将参数指明为final。这意味着,在方法中无法将其更改:
class Gizmo{
public void spin(){}
}
public class FinalArguments {
void with(final Gizmo gizmo){
//! gizmo = new Gizmo();
}
void without(Gizmo gizmo){
gizmo = new Gizmo();
gizmo.spin();
}
void f(final int i){
//! i++;
}
public static void main(String[] args) {
FinalArguments arguments = new FinalArguments();
arguments.with(null);
arguments.without(null);
}
}
上述代码分别展示了基本类型和对象引用的参数被修饰为final的结果:
- 基本类型参数:可以读该参数,但无法修改参数。
- 对象引用:可以使用并修改该引用指向的对象,但无法改变该引用。
这一特性主要用于向匿名内部类传递数据。
7.8.2 final方法
使用final方法的原因:将方法锁定,以防任何继承类通过覆盖修改它的含义。
final和private关键字
类中所有的private方法都隐式地指定为final。由于无法取用private方法,所以也无法覆盖它。如果试图覆盖它,编译器允许,但实际上却没有被覆盖:
class WithFinals {
private final void f() {
System.out.println("WithFinals.f()");
}
private void g() {
System.out.println("WithFinals.g()");
}
}
class OverridingPrivate extends WithFinals {
private final void f() {
System.out.println("OverridingPrivate.f()");
}
private void g() {
System.out.println("OverridingPrivate.g()");
}
public static void main(String[] args) {
OverridingPrivate overridingPrivate = new OverridingPublic();
overridingPrivate.f();
overridingPrivate.g();
}
}
public class OverridingPublic extends OverridingPrivate {
public final void f() {
System.out.println("OverridingPublic.f()");
}
public void g() {
System.out.println("OverridingPublic.g()");
}
public static void main(String[] args) {
OverridingPublic overridingPublic = new OverridingPublic();
overridingPublic.f();
overridingPublic.g();
}
}
从OverridingPrivate.main()的运行结果可以看出:private方法无法被覆盖。即所有private方法都隐式地指定为final。
private方法无法触及而且能有效隐藏,它只是所属类的组织结构,与其他任何事物无关。
7.8.3 final类
当将某个类的整体定义为final时,就表明该类无法被任何类所继承。 例如:
class SmallBrain{}
final class Dinosaur{
int i = 7;
int j = 1;
SmallBrain x = new SmallBrain();
void f(){}
}
//! class Further extends Dinosaur{}
public class Jurassic {
public static void main(String[] args) {
Dinosaur dinosaur = new Dinosaur();
dinosaur.f();
dinosaur.i = 40;
dinosaur.j++;
}
}
注意,不论类是否被定义为final:
- 域:都可以被定义为是或不是final,且相同的规则同样适用于该类。
- 方法:由于final类禁止继承,所以final类中的方法都隐式地指定为final,即无法被覆盖。
7.8.4 有关final的忠告
要预见类是如何被复用是十分困难的,特别是对于一个通用类而言更是如此。如果将一个类或方法指定为final,可能会妨碍其他程序员在项目中通过继承来复用该类。
7.9 初始化及类的加载
在Java中,所有的事物都是对象,并且,每个类的编译代码都存在于它自己的独立文件中。该文件只有在需要使用程序代码时才会被加载。即:类的代码在初次使用时才加载。所以,加载发生在:创建类的第一个对象,或访问static域或方法时。
static初始化也发生在加载时,所有的static对象和static代码段会依定义类时的书写顺序而依次初始化。并且只初始化一次。
7.9.1 继承与初始化
了解包括继承在内的初始化全过程,对系统全局性的把握是很有益的。 请看下例:
class Insect {
private int i = 9;
protected int j;
public Insect() {
System.out.println("i = " + i + " , j=" + j);
j = 39;
}
private static int x1 = printInit("static Insect.x1 initialized");
static int printInit(String s) {
System.out.println(s);
return 66;
}
}
public class Beetle extends Insect {
private int k = printInit("Beetle.k initialized");
public Beetle() {
System.out.println("k = " + k);
System.out.println("j = " + j);
}
private static int x2 = printInit("static Beetle.x2 initialized");
public static void main(String[] args) {
System.out.println("Beetle constructor");
Beetle beetle = new Beetle();
}
}
当Beetle运行时,首先试图访问Beetle.main(),于是加载器开始启动并找出该类的编译代码。在对其进行加载过程,通过extends得知其有一个基类,此时,开始加载其基类,并以此类推。加载完成根基类后,开始初始化其static域,然后是下一个导出类,以此类推。这种方式保证了导出类中依赖于基类成员初始化的static成员可以被正确初始化。
类全部加载完成后,对象就可以被创建了。首先,创建根基类对象,按照成员初始化顺序进行初始化,最后调用其构造器。然后是下一个导出类,以此类推。
7.10 总结
继承和组合都能从现有类型生成新类型:
- 组合:将现有类型作为新类型底层实现的一部分来加以复用。
- 继承:复用的是接口,因此在使用时,可以向上转型,对多态来讲至关重要。
在面向对象编程中,一般优先选择组合或代理,只有在确实必要时才使用继承。
在设计一个系统时,目标应该是找到或创建某些类,并且每个类都有具体用途。其不能包含太多的功能而难以复用,也不能在不添加其他功能时无法使用。如果设计变得过于复杂,应该将现有类拆分为更小部分而添加更多的对象。