(转)多用组合,少用继承

最近在学习设计模式,发现一个好文
作者:跳刀的兔子
链接:https://www.cnblogs.com/shipengzhi/articles/2086419.html

对类的功能的扩展,要多用组合,少用继承。 

对于类的扩展,在面向对象的编程过程中,我们首先想到的是类的继承,由子类继承父类,从而完成了对子类功能的扩展。但是,面向对象的原则告诉我们,对类的功能的扩展要多用组合,而少用继承。其中的原因有以下几点:

  • 第一、子类对父类的继承是全部的公有和受保护的继承,这使得子类可能继承了对子类无用甚至有害的父类的方法。换句话说,子类只希望继承父类的一部分方法,怎么办?
  • 第二、实际的对象千变万化,如果每一类的对象都有他们自己的类,尽管这些类都继承了他们的父类,但有些时候还是会造成类的无限膨胀。
  • 第三、 继承的子类,实际上需要编译期确定下来,这满足不了需要在运行内才能确定对象的情况。而组合却可以比继承灵活得多,可以在运行期才决定某个对象。

嗨!光说这么多一二三有什么用,我们就是想看看实际情况是不是像上面说的那样呢?还是来看看实际的例子吧!

实例1

现在我们需要这样一个HashMap,它除了能按常规的Map那样取值,如get(Object obj)。还能按位取值,像ArrayList那样,按存入对象对的先后顺序取值。
对于这样一个问题,我们首先想到的是做一个类,它继承了HashMap类,然后用一个ArrayList属性来保存存入的key,我们按key的位来取值,代码如下:

public class ListMap extends HashMap {  
    private List list;  
    public ListMap() {  
             super();  
              this.list = new ArrayList();  
    }  
    public Object put(Object key,Object value)  
    {  
             if(list.contains(key))  
              {  
                     list.remove(key);  
              }  
             this.list.add(key);  
              return super.put(key,value);  
    }  
    public Object getKey(int i)  
    {  
              return this.list.get(i);  
    }  
    public Object getValue(int i)  
    {  
              return this.get(getKey(i));  
    }  
    public int size()  
    {  
              return this.list.size();  
    }  
}  
//这个ListMap类对HashMap作了一定的扩展,很简单就实现了上面我们所要求的功能。然后我们对该类做一下测试:  
ListMap map = new ListMap();  
         map.put("a","111");  
         map.put("v","190");  
         map.put("d","132");  
          for(int i=0;i<map.size();i++)  
          {  
                 System.out.println(map.getValue(i));  
          } 

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

ListMap map = new ListMap();  
         map.put("a","111");  
         map.put("v","190");  
         map.put("d","132");  
         String[] list = (String[])map.values().toArray(new String[0]);  
          for(int i=0;i<list.length;i++)  
          {  
                 System.out.println(list[i]);  
          }  

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

public class MyListMap {  
  private HashMap map;  
  private List list;  
  public MyListMap()  
  {  
           this.map = new HashMap();  
            this.list = new ArrayList();  
  }  
  public Object put(Object key,Object value)  
  {  
           if(list.contains(key))  
            {  
                   list.remove(key);  
            }  
           this.list.add(key);  
            return this.map.put(key,value);  
  }  
  public Object getKey(int i)  
  {  
            return this.list.get(i);  
  }  
  public Object getValue(int i)  
  {  
            return this.map.get(getKey(i));  
  }  
  public int size()  
  {  
            return this.list.size();  
  }  
}  

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

public interface WhatIHave  
{  
          public void g();  
}  
public class Resource  
{  
          public void f()  
          {  
               ……  
          }  
          public void h()  
          {  
               ……  
          }  
}  

上面是两个基础类,很明显,我们所要的类既要有g()方法,也要有f()和h()方法。

Public class WhatIWant implements WhatIHave  
{  
  private Resource res;  
  public WhatIWant()  
  {  
         res = new Resource();  
  }  
  public void g()  
  {  
         ……  
  }  
  public void f()  
  {  
           this.res.f();  
  }  
  public void h()  
  {  
           this.res.h();  
  }  
}  

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

package decorator;  
  
interface Product {  
  public double money();  
}  
  
//咖啡类:  
class Coffee implements Product {  
  public double money() {  
      return 12;  
  }  
  }  
    
  //加糖:  
  class Sugar implements Product {  
  private Product product;  
    
  public Sugar(Product product) {  
      this.product = product;  
  }  
    
  public double money() {  
      return product.money() + 2;  
  }  
  }  
    
  //加冰:  
  class Ice implements Product {  
  private Product product;  
    
  public Ice(Product product) {  
      this.product = product;  
  }  
    
  public double money() {  
      return product.money() + 1.5;  
  }  
  }  
    
  //加奶:  
  class Milk implements Product {  
  private Product product;  
    
  public Milk(Product product) {  
      this.product = product;  
  }  
    
  public double money() {  
      return product.money() + 4.0;  
  }  
  }  
    
  //加巧克力:  
  class Chocolate implements Product {  
  private Product product;  
    
  public Chocolate(Product product) {  
      this.product = product;  
  }  
    
  public double money() {  
      return product.money() + 5.5;  
  }  
  }  
  public class DecoratorModel{  
  public static void main(String [] args){  
      Product coffee = new Coffee();  
      Product sugarCoffee = new Sugar(coffee);  
      Product sugarmilkCoffee = new Milk(sugarCoffee);  
      System.out.println("加糖咖啡:"+sugarCoffee.money());  
      System.out.println("加糖加奶咖啡:"+sugarmilkCoffee.money());  
  }  
}  

我们来看客户端的调用。
如果顾客想要黑咖啡,调用如下:
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());
通过上面的例子,我们可以看到组合的又一个很优越的好处:能够在运行期创建新的对象。如上面我们的加冰咖啡,我们没有这个类,却能通过组合在运行期创建该对象,这的确大大的增加了我们程序的灵活性。
如果咖啡店的老板再要求你增加加压咖啡,你就不会再担心了,只给他增加了一个类就解决了所有的问题。

要点总结:

  • 继承缺点1:子类可能会继承到无用甚至有害的方法
  • 继承缺点2:子类可能会快速的膨胀
  • 适配器模式很好的解决了继承中子类继承无用甚至有害的方法的问题
  • 装饰器模式很好的解决了继承中子类快速膨胀的问题
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353

推荐阅读更多精彩内容

  • 可能心理咨询师是最多的听到“我道理都懂,就是做不到”这样的话了。首先声明,前提并非咨询师给他讲了大堆道理,然后来...
    等雨季阅读 147评论 1 2
  • 为了给幼小衔接的小朋友增加识字量,从爸爸妈妈读故事书过度到自己阅读,禁不住同班同学妈妈假期天天在群里发阅读小视频,...
    回声520阅读 176评论 0 2
  • 在过去的一周,一切都有序地进行着。 早起也尽量早睡;坚持运动;健康饮食。 每天要吃的青蛙大部分都吃完,书买了一堆,...
    潘雁阅读 149评论 0 0
  • 描述 给定一个未排序的整数数组,找出最长连续序列的长度。 说明 要求你的算法复杂度为O(n) 样例 给出数组[10...
    6默默Welsh阅读 493评论 0 0