继承与聚合

一:继承:

继承的概念: 子类可以继承父类除了构造函数,析构函数之外的所有成员,但是能否使用要受继承方式的限制.

继承,表达的是“is-a”的关系,两者之间是一种上下级的关系,比如,我们说“human is a animal“(人是动物的一种,换句话说,动物是人的上级),要表达这种关系,就可以使用继承
 calss animal
{};
class human : public animal  // 继承
{};

继承的类别:
 
1.公有继承:
class A : public B
{
}
父类的公有成员和保护成员被继承时保持不变.
父类的私有成员被继承,但是在子类中不可见.

2.私有继承:
class A : private B
{
}
或者
class A : B
{
}
父类的公有成员和保护成员均被继承为私有的.
父类的私有成员虽被继承,但是在子类中不可见.

3.保护继承:
class A : protected B
{
}
父类的公有成员和保护成员均被继承为保护的.
父类的私有成员虽被继承,但是在子类中不可见.

总结: 
(1).无论哪一种继承方式,父类的私有成员只能通过本类的成员函数访问,类外(包括子类中)均不可访问. 
(2).保护成员存在的必要性:设计类时考虑其可能被继承,为了使子类能够访问父类的隐藏成员而设立的. 
     保护成员和私有成员对于本类来说均可以实现隐藏成员的功能,本类之外均不可访问,只是保护成员为子类使用其提供了可能.

二:C++中类的组合(UML)与聚合:

C++本身并没有组合的概念,组合是来自UML。而组合和聚合,则是表达两个平等对象之间整体和局部的关系,而因为这种关系的紧密性的不同,而又分为了组合和聚合。组合表达的是“contain-a”的关系,整体和局部拥有相同的生命周期,换句话说,如果整体对象不存在了,那么局部对象也会消亡。在C++中,我们通常在整体类中加入一个局部类的对象作为其成员,来表达这种组合关系(整体对象创建的同时,也会创建局部对象,而整体对象消失了,局部对象也会消失),比如,我们说“human conains a head”(人有一个脑袋,人活着,脑袋也活着,如果人死了,脑袋也就是了),
 
class head
{};
class human
{
private:
    head h;  // 数据成员对象表示的组合关系
}

聚合同样表的是整体和局部的关系,但是这种关系要松散得多,它表达的是一种“has-a”的关系,整体和局部拥有各自的生命周期,两者可以单独存在,相互独立,并不像组合一样两者相互依存,但两者也可以聚合在一起,形成一个新的类型。比如,我们说“human has a computer”(人有一台电脑,电脑和人都是各自独立的,而两者聚合在一起,就成了“有电脑的人”),在C++中,我们通常在整体类中加入指向局部类的指针,来表达这种“聚合”关系(整体对象创建的时候,不一定有局部对象,可能已经创建,也可以还没有,而整体对象消失了,局部对象还可以继续存在,也可能消失,想象一下电脑和人的关系,生不带来,死不带去)。
 
class computer
{};
class human
{
/// ...
private:
    computer* pCom; // 指针表示的聚合关系
};

       总结起来,继承表达的是一种上下级的关系(is-a),聚合表达的是一种松散的整体和局部的关系(has-a),而组合表达的是一种紧密的整体局部关系(contain-a)

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

 

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类在语义上包含另外一个类,并且在A在B中以整体的功能充当某个角色的时候,注意:很多时候,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实现时你也可以采用与上面不同的策略:每次取出列表的最后一个元素,将元素入栈时加入列表的最后一个位置。

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

 

继承也能是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做了代理,这就是我们常说的代理模式。

 

 

 

你可能感兴趣的:(继承与聚合)