为什么不推荐使用继承

及城市面向对象的四大特征之一,可以解决代码复用的问题。但是继承层次过深,过复杂会影响到代码的可维护性。

如果我们还需要考虑“是否会下蛋”这样一个行为,那估计就要组合爆炸了。类的继承层次会越来越深、继承关系会越来越复杂。而这种层次很深、很复杂的继承关系,一方面,会导致代码的可读性变差。因为我们要搞清楚某个类具有哪些方法、属性,必须阅读父类的代码、父类的父类的代码……一直追溯到最顶层父类的代码。另一方面,这也破坏了类的封装特性,将父类的实现细节暴露给了子类。子类的实现依赖父类的实现,两者高度耦合,一旦父类代码修改,就会影响所有子类的逻辑。

组合相比继承有哪些优势?

实际上,我们可以利用组合(composition)、接口、委托(delegation)三个技术手段,一块儿来解决刚刚继承存在的问题。

不过,我们知道,接口只声明方法,不定义实现。也就是说,每个会下蛋的鸟都要实现一遍 layEgg() 方法,并且实现逻辑是一样的,这就会导致代码重复的问题。那这个问题又该如何解决呢?

对类的功能的扩展,要多用组合,少用继承
对于类的扩展,在面向对象的编程过程中,我们首先想到的是类的继承,由子类继承父类,从而完成了对子类功能的扩展。但是,面向对象的原则告诉我们,对类的功能的扩展要多用组合,而少用继承。其中的原因有以下几点:
第一、子类对父类的继承是全部的公有和受保护的继承,这使得子类可能继承了对子类无用甚至有害的父类的方法。换句话说,子类只希望继承父类的一部分方法,怎么办?
第二、实际的对象千变万化,如果每一类的对象都有他们自己的类,尽管这些类都继承了他们的父类,但有些时候还是会造成类的无限膨胀。
第三、 继承的子类,实际上需要编译期确定下来,这满足不了需要在运行内才能确定对象的情况。而组合却可以比继承灵活得多,可以在运行期才决定某个对象。
嗨!光说这么多一二三有什么用,我们就是想看看实际情况是不是像上面说的那样呢?还是来看看实际的例子吧!
现在我们需要这样一个HashMap,它除了能按常规的Map那样取值,如get(Object obj)。还能按位取值,像ArrayList那样,按存入对象对的先后顺序取值。
对于这样一个问题,我们首先想到的是做一个类,它继承了HashMap类,然后用一个ArrayList属性来保存存入的key,我们按key的位来取值,代码如下:
Java代码 08 多用组合少用继承 - 图1

  1. public class ListMap extends HashMap {
  2. private List list;
  3. public ListMap() {
  4. super();
  5. this.list = new ArrayList();
  6. }
  7. public Object put(Object key,Object value)
  8. {
  9. if(list.contains(key))
  10. {
  11. list.remove(key);
  12. }
  13. this.list.add(key);
  14. return super.put(key,value);
  15. }
  16. public Object getKey(int i)
  17. {
  18. return this.list.get(i);
  19. }
  20. public Object getValue(int i)
  21. {
  22. return this.get(getKey(i));
  23. }
  24. public int size()
  25. {
  26. return this.list.size();
  27. }
  28. }
  29. 这个ListMap类对HashMap作了一定的扩展,很简单就实现了上面我们所要求的功能。然后我们对该类做一下测试:
  30. ListMap map = new ListMap();
  31. map.put(“a”,”111”);
  32. map.put(“v”,”190”);
  33. map.put(“d”,”132”);
  34. for(int i=0;i<map.size();i++)
  35. {
  36. System.out.println(map.getValue(i));
  37. }

测试结果为:
111
190
132
正是我们所需要看到的结果。如此说来,这个ListMap类就可以放心的使用了吗?有实现了这样功能的类,你的同事或朋友也可能把这个类拿来使用一下,他可能写出来如下的代码:
Java代码 08 多用组合少用继承 - 图2

  1. ListMap map = new ListMap();
  2. map.put(“a”,”111”);
  3. map.put(“v”,”190”);
  4. map.put(“d”,”132”);
  5. String[] list = (String[])map.values().toArray(new String[0]);
  6. for(int i=0;i<list.length;i++)
  7. {
  8. System.out.println(list[i]);
  9. }

运行的结果如下:
132
111
190
哎哟,怎么回事啊?与上面的顺序不对了。你朋友过来找你,说你写的代码怎么不对啊?你很吃惊,说把代码给我看看。于是你看到了上面的代码。你大骂道,混蛋,怎么不是用我的getValue方法啊?你朋友搔搔头道,values方法不是一样的吗?你也没告诉我不能用啊?
通过上面的例子,我们看到了继承的第一个危害:继承不分青红皂白的把父类的公有和受保护的方法统统继承下来。如果你的子类没有对一些方法重写,就 会对你的子类产生危害。上面的ListMap类,你没有重写继承自HashMap类的values方法,而该方法仍然是按HashMap的方式取值,没有 先后顺序。这时候,如果在ListMap类的对象里使用该方法取得的值,就没有实现我们上面的要求。
接上面的那个例子,你听了朋友的抱怨,摇摇头,想想也是,不能怪他。你只得把values方法在ListMap类重写一遍,然后又嘀咕着,我是不是该把HashMap类的公有方法在ListMap类里全部重写?很多方法根本没有必要用到啊?……
对了,很多方法在ListMap里根本不必用到,但是你用继承的话,还不得不在ListMap里重写它们。如果用组合的话,就没有上面的烦恼了:
Java代码 08 多用组合少用继承 - 图3

  1. public class MyListMap {
  2. private HashMap map;
  3. private List list;
  4. public MyListMap()
  5. {
  6. this.map = new HashMap();
  7. this.list = new ArrayList();
  8. }
  9. public Object put(Object key,Object value)
  10. {
  11. if(list.contains(key))
  12. {
  13. list.remove(key);
  14. }
  15. this.list.add(key);
  16. return this.map.put(key,value);
  17. }
  18. public Object getKey(int i)
  19. {
  20. return this.list.get(i);
  21. }
  22. public Object getValue(int i)
  23. {
  24. return this.map.get(getKey(i));
  25. }
  26. public int size()
  27. {
  28. return this.list.size();
  29. }
  30. }

这样,你的朋友就只能使用你的getKey和getValue方法了。如果他向你抱怨没有values方法,你尽可以满足他的要求,给他添加上那个方法,而不必担心可能还有方法没有被重写了。
我们来看Adapter模式,该模式的目的十分简单:我手里握有一些实现了WhatIHave接口的实现,可我觉得这些实现的功能不够用,我还需要从Resource类里取一些功能来为我所用。Adapter模式的解决方法如下:
Java代码 08 多用组合少用继承 - 图4

  1. public interface WhatIHave
  2. {
  3. public void g();
  4. }
  5. public class Resource
  6. {
  7. public void f()
  8. {
  9. ……
  10. }
  11. public void h()
  12. {
  13. ……
  14. }
  15. }

上面是两个基础类,很明显,我们所要的类既要有g()方法,也要有f()和h()方法。
Java代码 08 多用组合少用继承 - 图5

  1. Public class WhatIWant implements WhatIHave
  2. {
  3. private Resource res;
  4. public WhatIWant()
  5. {
  6. res = new Resource();
  7. }
  8. public void g()
  9. {
  10. ……
  11. }
  12. public void f()
  13. {
  14. this.res.f();
  15. }
  16. public void h()
  17. {
  18. this.res.h();
  19. }
  20. }

上 面就是一个Adapter模式最简单的解决问题的思路。我们主要到,对于Resource类,该模式使用的是组合,而不是继承。这样使用是有多个原因:第 一,Java不支持多重继承,如果需要使用好几个不同的Resource类,则继承解决不了问题。第二,如果Resource类还有一个方法:k(),我 们在WhatIWant类里使用不上的话,继承就给我们造成多余方法的问题了。
如果说Adapter模式对组合的应用的目的十分简单明确,那么Decorator模式对组合的应用简直就是令人叫绝。
让我们还是从Decorator模式的最佳例子说起,咖啡店需要售卖各种各样的咖啡:黑咖啡、加糖、加冰、加奶、加巧克力等等。顾客要买咖啡,他可以往咖啡任意的一种或几种产品。
这个问题一提出来,我们最容易想到的是继承。比如说加糖咖啡是一种咖啡,满足ia a的句式,很明显,加糖咖啡是咖啡的一个子类。于是,我们马上可以赋之行动。对于咖啡我们做一个咖啡类:Coffee,咖啡加 糖:SugarCoffee,咖啡加冰:IceCoffee,咖啡加奶:MilkCoffee,咖啡加巧克力:ChocolateCoffee,咖啡加糖 加冰:SugarIceCoffee……
哎哟,我们发现问题了:这样下去我们的类好多啊。可是咖啡店的老板还不放过我们,他又逼着我们增加蒸汽咖啡、加压咖啡,结果我们发现,每增加一种新的类型,我们的类好像是成几何级数增加,我们都要疯了。
这个例子向我们展示了继承的第二个缺点,会使得我们的子类快速的膨胀下去,达到惊人的数量。
怎么办?我们的Decorator模式找到了组合来为我们解决问题。下面我们来看看Decorator模式是怎么来解决这个问题的。
首先是它们的共同接口:
Java代码 08 多用组合少用继承 - 图6

  1. package decorator;
  2. interface Product {
  3. public double money();
  4. }
  5. //咖啡类:
  6. class Coffee implements Product {
  7. public double money() {
  8. return 12;
  9. }
  10. }
  11. //加糖:
  12. class Sugar implements Product {
  13. private Product product;
  14. public Sugar(Product product) {
  15. this.product = product;
  16. }
  17. public double money() {
  18. return product.money() + 2;
  19. }
  20. }
  21. //加冰:
  22. class Ice implements Product {
  23. private Product product;
  24. public Ice(Product product) {
  25. this.product = product;
  26. }
  27. public double money() {
  28. return product.money() + 1.5;
  29. }
  30. }
  31. //加奶:
  32. class Milk implements Product {
  33. private Product product;
  34. public Milk(Product product) {
  35. this.product = product;
  36. }
  37. public double money() {
  38. return product.money() + 4.0;
  39. }
  40. }
  41. //加巧克力:
  42. class Chocolate implements Product {
  43. private Product product;
  44. public Chocolate(Product product) {
  45. this.product = product;
  46. }
  47. public double money() {
  48. return product.money() + 5.5;
  49. }
  50. }
  51. public class DecoratorModel{
  52. public static void main(String [] args){
  53. Product coffee = new Coffee();
  54. Product sugarCoffee = new Sugar(coffee);
  55. Product sugarmilkCoffee = new Milk(sugarCoffee);
  56. System.out.println(“加糖咖啡:”+sugarCoffee.money());
  57. System.out.println(“加糖加奶咖啡:”+sugarmilkCoffee.money());
  58. }
  59. }


我们来看客户端的调用。
如果顾客想要黑咖啡,调用如下:
Product prod = new Coffee();
System.out.println(prod.money());
如果顾客需要加冰咖啡,调用如下:
Product prod = new Ice(new Coffee());
System.out.println(prod.money());
如果顾客想要加糖加冰加奶加巧克力咖啡,调用如下:
Product prod = new Chocolate(new Milk(new Ice(new Sugar())));
System.out.println(prod.money());
通过上面的例子,我们可以看到组合的又一个很优越的好处:能够在运行期创建新的对象。如上面我们的加冰咖啡,我们没有这个类,却能通过组合在运行期创建该对象,这的确大大的增加了我们程序的灵活性。
如果咖啡店的老板再要求你增加加压咖啡,你就不会再担心了,只给他增加了一个类就解决了所有的问题。