熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> JSP教程 >> 正文

為什麼extends是有害的(一)

2022-06-13   來源: JSP教程 

  概述
  大多數好的設計者象躲避瘟疫一樣來避免使用實現繼承(extends 關系)%的代碼應該完全用interfaces寫不用具體的基類事實上四人幫的設計模式的書大量的關於怎樣用interface繼承代替實現繼承這個文章描述設計者為什麼有這樣的怪癖的想法
  
  Extends是有害的也許對於Charles Manson這個級別的不是但是足夠糟糕的它應該在任何的可能的時候被避開四人幫的設計模式花了很大的部分討論用interface繼承代替實現繼承
  
  好的設計者在他的代碼中大部分用interface而不是具體的基類這個文章討論為什麼設計者會有這樣怪癖的習慣並且也介紹一些基於interface的編程基礎
  
  Interface和Class
  一次我參加一個Java用戶組的會議在會議中Jams Gosling(Java之父)做發起人講話在那令人難忘的Q&A部分有人問他如果你重新構造Java你想改變什麼?我想拋棄classes他回答在笑聲平息後它解釋說真正的問題不是由於class本身而是實現繼承(extends 關系)接口繼承(implements關系)是更好的你應該盡可能的避免實現繼承
  
  失去了靈活性
  為什麼你應該避免實現繼承呢?第一個問題是明確的使用具體類名將你固定到特定的實現給底層的改變增加了不必要的困難
  
  在當前的敏捷編程方法中核心是並行的設計和開發的概念在你詳細設計程序前你開始編程這個技術不同於傳統方法的形式傳統的方式是設計應該在編碼開始前完成但是許多成功的項目已經證明你能夠更快速的開發高質量代碼相對於傳統的按部就班的方法但是在並行開發的核心是主張靈活性你不得不以某一種方式寫你的代碼以至於最新發現的需求能夠盡可能沒有痛苦的合並到已有的代碼中
  
  勝於實現你也許需要的特征你只需實現你明確需要的特征而且適度的對變化的包容如果你沒有這種靈活並行的開發那簡直不可能
  
  對於Inteface的編程是靈活結構的核心為了說明為什麼讓我們看一下當使用它們的時候會發生什麼考慮下面的代碼
  [/代碼]
  f()
  {  LinkedList list = new LinkedList();
    //
    g( list );
  }
  
  g( LinkedList list )
  {
    listadd( );
    g( list )
  }
  [/代碼]
  現在假設一個對於快速查詢的需求被提出以至於這個LinkedList不能夠解決你需要用HashSet來代替它在已有代碼中變化不能夠局部化因為你不僅僅需要修改f()也需要修改g()(它帶有LinkedList參數)並且還有g()把列表傳遞給的任何代碼
  象下面這樣重寫代碼:
  [/代碼]
  f()
  {  Collection list = new LinkedList();
    //
    g( list );
  }
  
  g( Collection list )
  {
    listadd( );
    g( list )
  }
  [/代碼]
  這樣修改Linked list成hash可能只是簡單的用new HashSet()代替new LinkedList()就這樣沒有其他的需要修改的地方
  作為另一個例子比較下面兩段代碼
  [/代碼]
  f()
  {  Collection c = new HashSet();
    //
    g( c );
  }
  
  g( Collection c )
  {
    for( Iterator i = erator(); ihasNext() )
      do_something_with( inext() );
  }
  [/代碼]
  和
  [/代碼]
  f()
  {  Collection c = new HashSet();
    //
    g( erator() );
  }
  
  g( Iterator i )
  {  while( ihasNext() )
      do_something_with( inext() );
  }
  [/代碼]
  g()方法現在能夠遍歷Collection的派生就像你能夠從Map中得到的鍵值對事實上你能夠寫iterator它產生數據代替遍歷一個Collection你能夠寫iterator它從測試的框架或者文件中得到信息這會有巨大的靈活性
  耦合
  對於實現繼承一個更加關鍵的問題是耦合令人煩躁的依賴就是那種程序的一部分對於另一部分的依賴全局變量提供經典的例子證明為什麼強耦合會引起麻煩例如如果你改變全局變量的類型那麼所有用到這個變量的函數也許都被影響所以所有這些代碼都要被檢查變更和重新測試而且所有用到這個變量的函數通過這個變量相互耦合也就是如果一個變量值在難以使用的時候被改變一個函數也許就不正確的影響了另一個函數的行為這個問題顯著的隱藏於多線程的程序
  
  作為一個設計者你應該努力最小化耦合關系你不能一並消除耦合因為從一個類的對象到另一個類的對象的方法調用是一個松耦合的形式你不可能有一個程序它沒有任何的耦合然而你能夠通過遵守OO規則最小化一定的耦合(最重要的是一個對象的實現應該完全隱藏於使用他的對象)例如一個對象的實例變量(不是常量的成員域)應該總是private我意思是某段時期的無例外的不斷的(你能夠偶爾有效地使用protected方法但是protected實例變量是可憎的事)同樣的原因你應該不用get/set函數他們對於是一個域公用只是使人感到過於復雜的方式(盡管返回修飾的對象而不是基本類型值的訪問函數是在某些情況下是由原因的那種情況下返回的對象類是一個在設計時的關鍵抽象)
  
  這裡我不是書生氣在我自己的工作中我發現一個直接的相互關系在我OO方法的嚴格之間快速代碼開發和容易的代碼實現無論什麼時候我違反中心的OO原則如實現隱藏我結果重寫那個代碼(一般因為代碼是不可調試的)我沒有時間重寫代碼所以我遵循那些規則我關心的完全實用—我對干淨的原因沒有興趣
  
  脆弱的基類問題
  現在讓我們應用耦合的概念到繼承在一個用extends的繼承實現系統中派生類是非常緊密的和基類耦合當且這種緊密的連接是不期望的設計者已經應用了綽號脆弱的基類問題去描述這個行為基礎類被認為是脆弱的是因為你在看起來安全的情況下修改基類但是當從派生類繼承時新的行為也許引起派生類出現功能紊亂你不能通過簡單的在隔離下檢查基類的方法來分辨基類的變化是安全的而是你也必須看(和測試)所有派生類而且你必須檢查所有的代碼它們也用在基類和派生類對象中因為這個代碼也許被新的行為所打破一個對於基礎類的簡單變化可能導致整個程序不可操作
  
  讓我們一起檢查脆弱的基類和基類耦合的問題下面的類extends了Java的ArrayList類去使它像一個stack來運轉
  [/代碼]
  class Stack extends ArrayList
  {  private int stack_pointer = ;
  
    public void push( Object article )
    {  add( stack_pointer++ article );
    }
  
    public Object pop()
    {  return remove( stack_pointer );
    }
  
    public void push_many( Object[] articles )
    {  for( int i = ; i < articles.length; ++i )
        push( articles[i] );
    }
  }
  [/代碼]
  
  甚至一個象這樣簡單的類也有問題。Tw.wiNGwIT.Com思考當一個用戶平衡繼承和用ArrayList的clear()方法去彈出堆棧時:
  [/代碼]
  Stack a_stack = new Stack();
  a_stack.push("1");
  a_stack.push("2");
  a_stack.clear();
  [/代碼]
  
  這個代碼成功編譯,但是因為基類不知道關於stack指針堆棧的情況,這個stack對象當前在一個未定義的狀態。下一個對於push()調用把新的項放入索引2的位置。(stack_pointer的當前值),所以stack有效地有三個元素-下邊兩個是垃圾。(Java的stack類正是有這個問題,不要用它).
  
  對這個令人討厭的繼承的方法問題的解決辦法是為Stack覆蓋所有的ArrayList方法,那能夠修改數組的狀態,所以覆蓋正確的操作Stack指針或者拋出一個例外。(removeRange()方法對於拋出一個例外一個好的候選方法)。
  
  這個方法有兩個缺點。第一,如果你覆蓋了所有的東西,這個基類應該真正的是一個interface,而不是一個class。如果你不用任何繼承方法,在實現繼承中就沒有這一點。第二,更重要的是,你不能夠讓一個stack支持所有的ArrayList方法。例如,令人煩惱的removeRange()沒有什麼作用。唯一實現無用方法的合理的途徑是使它拋出一個例外,因為它應該永遠不被調用。這個方法有效的把編譯錯誤成為運行錯誤。不好的方法是,如果方法只是不被定義,編譯器會輸出一個方法未找到的錯誤。如果方法存在,但是拋出一個例外,你只有在程序真正的運行時,你才能夠發現調用錯誤。
  
  對於這個基類問題的一個更好的解決辦法是封裝數據結構代替用繼承。這是新的和改進的Stack的版本:
  [/代碼]
  class Stack
  {
    private int stack_pointer = 0;
    private ArrayList the_data = new ArrayList();
    
    public void push( Object article )
  {
    the_data.add( stack_poniter++, article );
  }
  
  public Object pop()
  {
    return the_data.remove( --stack_pointer );
  }
  
  public void push_many( Object[] articles )
  {
    for( int i = 0; i < o.length; ++i )
      push( articles[i] );
  }
  }

From:http://tw.wingwit.com/Article/program/Java/JSP/201311/19788.html
    推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.