继承(Is-A)还是聚合(Has-a)?这是一个问题

昨天遇见了一个问题,有人想实现一个常用的数据结构栈(Stack),基于链表的,于是在群里贴了一段代码:

 

public class Stack extends LinkedList {
	public void push(Object object) { // 压栈
	}

	public Object peek() { // 获取栈顶元素
		return null;
	}

	public boolean isEmpty() { // 判断栈是否为空
		return false;
	}

	public Object pop() { // 弹栈
		return null;
	}
}

 

按照这种其实对应的实现代码很简单:

public class Stack extends LinkedList {
	public void push(Object object) { // 压栈
		add(0, object);
	}

	public Object peek() { // 获取栈顶元素
		if (isEmpty()) {
			return null;
		}
		return get(0);
	}

	public boolean isEmpty() { // 判断栈是否为空
		return size()==0;
	}

	public Object pop() { // 弹栈
		if (isEmpty()) {
			return null;
		}
		return remove(0);
	}
}

 

但是这样的设计合理吗?当面对继承和聚合不知道如何选择的时候,如何做出艰难的决定呢?

 

继承是一种Is-a关系,也就是说,如果当你要设计的类B在语义上可以说是A类的话,那么可以让B类继承A类,例如Cat(猫)是Animals(动物),我们就能让Cat extends Animals。而聚合是一种Has-a(包含)关系,A要设计的B类在语义上包含另外一个类,并且在AB中以整体的功能充当某个角色的时候,注意:很多时候,A类在B类外面是透明的。考虑使用聚合。例如,一只猫(Cat)有眼睛(Eyes)。那么可以设计成Cat类包含Eyes类,至于为什么A类在B类外是透明的,如下:

public class Cat {
	private Eyes eyes;
	
	public Cat(Eyes eyes) {
		this.eyes = eyes;
	}

	public void see() {
		eyes.see();
	}
}

 

public class Eyes {
	public void see() {
		System.out.println("see");
	}
}

  

 

在使用Cat类对象时,并不需要关注其内部对于see()方法的实现,很明显的猫也不是眼睛(不是Is-a关系),再来看前面的问题,栈并不是一个链表,所以采用继承的方式是不合适的,继承之所以是Is-a关系,是因为它继承了父类全部的方法,即它拥有了父类所有的行为。在上面的继承实现栈的方法中,栈继承了链表的所有方法,这样你就不能保证栈的特性—first in last out(先进后出)了。因为它同时也是链表,可以调用其从父类继承的方法在任一位置做删除和插入。下面是一种更好的has-a实现方式:

 

public class Stack {
	private List list;
	
	public void push(Object object) { // 压栈
		list.add(0, object);
	}

	public Object peek() { // 获取栈顶元素
		if (isEmpty()) {
			return null;
		}
		return list.get(0);
	}

	public boolean isEmpty() { // 判断栈是否为空
		return list.size()==0;
	}

	public Object pop() { // 弹栈
		if (isEmpty()) {
			return null;
		}
		return list.remove(0);
	}
}

 

这个时候栈对外的表现就纯粹的是栈了,因为它只具有栈的行为。至于里面是如何实现的,用户无需关心,同时也是安全的,用户除了入栈出栈之外,无法做其他的删除插入操作,这里选用了List作为其属性,没有限制用户使用ArrayList或者LinkedList,其实可以使用Collection属性,用户的选择就更大了。当然也能使用数组。可见对外表现一致的栈,其内部实现可以多样化,即在has-a关系中A类在B类外面是透明的. 在利用List实现时你也可以采用与上面不同的策略:每次取出列表的最后一个元素,将元素入栈时加入列表的最后一个位置。

现在你可以做出艰难的决定了。简单地说,如果在语义上你能说BA,那么采用继承,否则使用聚合。聚合使用的场合会多一些。

 

继承也能是has-a,聚合也能使Is-a

如果B类继承了A类,毫无疑问的,B类具有了A类的所有行为,在逻辑上B类其实Has A了。其中super关键字就表示了一个类的父类对象引用,可见说继承是一种has-a的关系逻辑上是没有错误的。为什么继承不能完全的替代has-a关系呢?这是因为继承时,继承需要行为的时候将不需要的行为,甚至是不能出现的行为,如上文链表的任意位置的插入删除行为也继承了下来。java只有一种继承方式,这源自java的设计哲学“C++ --”。在C++中有更多样的继承方式,其中的一种继承就是一种完全的has-a关系而非Is-a关系----私有继承。B类私有继承自A类则将A类中的所有行为全部继承为B类自己的私有行为,这样B类对外再也不能表现出A类的行为,是一种完全的has-a关系。

当然has-a关系也能体现出Is-a的关系,这来自用户的设计,可以在B类中定义所有A类的方法,然后内部调用A类实例去实现,这样B类就能对外表现A类的所有行为了。当然拥有A的同名方法未必能对外表现为B。除非你同时继承A,等等……是不是有点乱了。

 

B类继承A类,B类还包含A类的实例(A a = new A())。这样在B中不是有两个A的实例了一个是super一个是a。确实是这样。但是如果A是一个接口呢?

B实现了接口A,然后B类中中还有一个实现A的类的实例:

 

这样B对外可以作为A使用,而对内调用a的方法来进行实际的操作,B好像什么也没有做啊?不是什么也没有做,B做了代理,这就是我们常说的代理模式。

 (未完不知道续不续)

你可能感兴趣的:(设计模式,数据结构)