天天看點

面向對象六大原則面向對象六大原則總結六大原則

面向對象六大原則

原文連結:https://github.com/simple-android-framework-exchange/android_design_patterns_analysis/blob/master/oop-principles/oop-principles.md

以volley源碼為例來分析六大原則

單一職責原則(Single Responsibility Principle)

單一職責原則的英文名稱是Single Responsibility Principle,簡稱是SRP,簡單來說就是一個類就隻做一件事情,最大的問題就是對職責的定義,什麼是類的職責,怎麼劃分類的職責.

遵守這個原則,那麼你的類就會劃分詳細,每個類都有自己的職責,實作高内聚,低耦合!(我的了解是内部聯系密切,完成一項功能,與外部子產品的獨立性高)

示例

在volley示例中,展現SRP原則的就是HttpStack這個類族了,HttpStack定義了一個執行網絡請求的接口,代碼如下:

/**
 * An HTTP stack abstraction.
 */
public interface HttpStack {
    /**
     * 執行Http請求,并且傳回一個HttpResponse
     */
    public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
        throws IOException, AuthFailureError;

}
           

可以看到,HttpStack隻有一個函數,清晰明了,它的職責就是執行網絡請求并且傳回一個Response,它的職責很單一,這樣在需要修改執行網絡請求的相關代碼時我們隻需要修改實作HttpStack接口的類,而不會影響其他類的代碼,如果某個類的職責包含有執行網絡請求,解析網絡請求,進行gzip壓縮,封裝請求參數等等,那麼在你修改某處代碼時候你必須要謹慎了,以免影響了其它的功能,但是當職責單一的時候,你修改代碼能夠基本不影響其它功能,這就一定程度上保證了代碼的可維護性,注意,單一職責原則并不是說一個類隻有一個函數,而是說這個類中的函數所做的工作必須是高度相關的,也就是高内聚,HttpStack抽象了執行網絡請求的具體過程,接口簡單清晰,也便于擴充.

我們知道,API 9以下使用HttpClient執行網絡請求會好一些,API 9以上則建議使用HttpURLConnection,這就需要執行網絡請求的具體實作能夠可擴充,可替換,是以我們對于執行網絡請求這個功能必須要抽象出來,HttpStack就是這個職責的抽象.

優點

  • 類的複雜性降低,實作什麼職責都有清晰明确的定義;
  • 可讀性提高
  • 可維護性提高
  • 變更引起的風險降低,變更是必不可少的,如果接口的單一職責做得好,一個接口修改隻對相應的實作類有影響,對其他接口沒有影響,對系統的擴充性,維護性都有非常大的幫助.

裡氏替換原則(Liskov Substitution Principle)

面向對象的三大特點是繼承,封裝,多态,裡氏替換原則就是依賴繼承,多态這兩大特性,裡氏替換原則簡單來說就是所有引用基類的地方必須能透明地使用其子類的對象.通俗地說,就是父類能出現的地方子類就可以出現,而且替換為子類也不會産生任何錯誤或者異常,使用者可能根本就不需要知道是父類還是子類,但是反過來就不行了,有子類出現的地方,父類未必就能适應.

示例

還是以HttpStack為例,Volley定義了HttpStack來表示執行網絡請求這個抽象概念,在執行網絡請求時,我們隻需要定義一個HttpStack對象,然後調用performRequest即可,至于HttpStack的具體實作由更高層的調用者給出,示例如下:

public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
    File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);

    String userAgent = "volley/0";
    // 代碼省略
    // 1、構造HttpStack對象
    if (stack == null) {
        if (Build.VERSION.SDK_INT >= ) {
            stack = new HurlStack();
        } else {
            // Prior to Gingerbread, HttpUrlConnection was unreliable.
            // See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
            stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
        }
    }
    // 2、将HttpStack對象傳遞給Network對象
    Network network = new BasicNetwork(stack);
    // 3、将network對象傳遞給網絡請求隊列
    RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
    queue.start();

    return queue;
}
           

BasicNetwork的代碼如下:

/**
 * A network performing Volley requests over an {@link HttpStack}.
 */
public class BasicNetwork implements Network {
    // HttpStack抽象對象
    protected final HttpStack mHttpStack;

    protected final ByteArrayPool mPool;

    public BasicNetwork(HttpStack httpStack) {

        this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE));
    }


    public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) {
        mHttpStack = httpStack;
        mPool = pool;
    }
}
           

上述代碼中,BasicNetwork構造函數依賴的是HttpStack抽象接口,任何實作了HttpStack接口的類型都可以作為參數傳遞給BasicNetwork用以執行網絡請求。這就是所謂的裡氏替換原則,任何父類出現的地方子類都可以出現,這不就保證了可擴充性嗎?! 此時,用手撐着你的小腦門作思考狀… 任何實作HttpStack接口的類的對象都可以傳遞給BasicNetwork實作網絡請求的功能,這樣Volley執行網絡請求的方法就有很多種可能性,而不是隻有HttpClient和HttpURLConnection。

喔,原來是這樣!此時我們放下裝逼的模樣,呈現一副若有所得的樣子。細想一下,架構可不就是這樣嗎? 架構不就是定義一系列相關的邏輯骨架與抽象,使得使用者可以将自己的實作傳遞進給架構,進而實作變化萬千的功能。

優點

  • 代碼共享,減少建立類的工作量,每個子類都擁有父類的方法和屬性
  • 提高代碼的重用性
  • 提高代碼的可擴充性,實作父類的方法就可以”為所欲為”了,很多開源架構的擴充接口都是通過繼承父類來完成的.
  • 提高産品或者項目的開放性

缺點

  • 繼承是侵入性的,隻要繼承,就必須擁有父類的所有屬性和方法.
  • 降低代碼的靈活性,子類必須擁有父類的屬性和方法,讓子類多了點限制.
  • 增強了耦合性,當父類的常量變量和方法被修改,必須考慮子類的修改,而且在缺乏規範的環境下,這種修改可能帶來非常糟糕的結果-大片的代碼重構

依賴倒置原則(Dependence Inversion Principle)

依賴倒置原則這個名字看着有點不好了解,“依賴”還要“倒置”,這到底是什麼意思?

依賴倒置原則的幾個關鍵點如下:

* 高層子產品不應該依賴低層子產品,兩者都應該依賴其抽象;

* 抽象不應該依賴細節;

* 細節應該依賴抽象。

在Java語言中,抽象就是指接口或抽象類,兩者都是不能直接被執行個體化的;細節就是實作類,實作接口或繼承抽象類而産生的類就是細節,其特點就是可以直接被執行個體化,也就是可以加上一個關鍵字 new 産生一個對象。依賴倒置原則在 Java 語言中的表現就是:子產品間的依賴通過抽象發生,實作類之間不發生直接的依賴關系,其依賴關系是通過接口或抽象類産生的. 軟體先驅們總是喜歡将一些理論定義得很抽象,弄得神不楞登的,其實就是一句話:面向接口程式設計,或者說是面向抽象程式設計,這裡的抽象指的是接口或者抽象類。面向接口程式設計是面向對象精髓之一。

示例

采用依賴倒置原則可以減少類間的耦合性,提高系統的穩定性,降低并行開發引起的風險,提高代碼的可讀性和可維護性。

第二章節中的BasicNetwork實作類依賴于HttpStack接口( 抽象 ),而不依賴于HttpClientStack與HurlStack實作類 ( 細節 ),這就是典型的依賴倒置原則的展現。假如BasicNetwork直接依賴了HttpClientStack,那麼HurlStack就不能傳遞給了,除非HurlStack繼承自HttpClientStack。但這麼設計明顯不符合邏輯,HurlStack與HttpClientStack并沒有任何的is-a的關系,而且即使有也不能這麼設計,因為HttpClientStack是一個具體類而不是抽象,如果HttpClientStack作為BasicNetwork構造函數的參數,那麼以為這後續的擴充都需要繼承自HttpClientStack。這簡直是一件不可忍受的事了!

優點

  • 可擴充性好
  • 耦合度低

開閉原則(Open-Close Principle)

開閉原則是 Java 世界裡最基礎的設計原則,它指導我們如何建立一個穩定的、靈活的系統。開閉原則的定義是 : 一個軟體實體如類、子產品和函數應該對擴充開放,對修改關閉。在軟體的生命周期内,因為變化、更新和維護等原因需要對軟體原有代碼進行修改時,可能會給舊代碼中引入錯誤。是以當軟體需要變化時,我們應該盡量通過擴充的方式來實作變化,而不是通過修改已有的代碼來實作。

示例

在軟體開發過程中,永遠不變的就是變化。開閉原則是使我們的軟體系統擁抱變化的核心原則之一。對擴充可放,對修改關閉給出了高層次的概括,即在需要對軟體進行更新、變化時應該通過擴充的形式來實作,而非修改原有代碼。當然這隻是一種比較理想的狀态,是通過擴充還是通過修改舊代碼需要根據代碼自身來定。

在Volley中,開閉原則展現得比較好的是Request類族的設計。我們知道,在開發C/S應用時,伺服器傳回的資料格式多種多樣,有字元串類型、xml、json等。而解析伺服器傳回的Response的原始資料類型則是通過Request類來實作的,這樣就使得Request類對于伺服器傳回的資料格式有良好的擴充性,即Request的可變性太大。

例如我們傳回的資料格式是Json,那麼我們使用JsonObjectRequest請求來擷取資料,它會将結果轉成JsonObject對象,我們看看JsonObjectRequest的核心實作。

/**
 * A request for retrieving a {@link JSONObject} response body at a given URL, allowing for an
 * optional {@link JSONObject} to be passed in as part of the request body.
 */
public class JsonObjectRequest extends JsonRequest<JSONObject> {

 // 代碼省略
  @Override
  protected Response<JSONObject> parseNetworkResponse(NetworkResponse response) {
      try {
          String jsonString =
              new String(response.data, HttpHeaderParser.parseCharset(response.headers));
          return Response.success(new JSONObject(jsonString),
                  HttpHeaderParser.parseCacheHeaders(response));
      } catch (UnsupportedEncodingException e) {
          return Response.error(new ParseError(e));
      } catch (JSONException je) {
          return Response.error(new ParseError(je));
      }
  }
}
           

JsonObjectRequest通過實作Request抽象類的parseNetworkResponse解析伺服器傳回的結果,這裡将結果轉換為JSONObject,并且封裝到Response類中。

例如Volley添加對圖檔請求的支援,即ImageLoader( 已内置 )。這個時候我的請求傳回的資料是Bitmap圖檔。是以我需要在該類型的Request得到的結果是Request,但支援一種資料格式不能通過修改源碼的形式,這樣可能會為舊代碼引入錯誤。但是你又需要支援新的資料格式,此時我們的開閉原則就很重要了,對擴充開放,對修改關閉。我們看看Volley是如何做的。

/**
 * A canned request for getting an image at a given URL and calling
 * back with a decoded Bitmap.
 */
public class ImageRequest extends Request<Bitmap> {
    // 代碼省略

    // 将結果解析成Bitmap,并且封裝套Response對象中
    @Override
    protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
        // Serialize all decode on a global lock to reduce concurrent heap usage.
        synchronized (sDecodeLock) {
            try {
                return doParse(response);
            } catch (OutOfMemoryError e) {
                VolleyLog.e("Caught OOM for %d byte image, url=%s", response.data.length, getUrl());
                return Response.error(new ParseError(e));
            }
        }
    }

    /**
     * The real guts of parseNetworkResponse. Broken out for readability.
     */
    private Response<Bitmap> doParse(NetworkResponse response) {
        byte[] data = response.data;
        BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
        Bitmap bitmap = null;
        if (mMaxWidth ==  && mMaxHeight == ) {
            decodeOptions.inPreferredConfig = mDecodeConfig;
            bitmap = BitmapFactory.decodeByteArray(data, , data.length, decodeOptions);
        } else {
            // If we have to resize this image, first get the natural bounds.
            decodeOptions.inJustDecodeBounds = true;
            BitmapFactory.decodeByteArray(data, , data.length, decodeOptions);
            int actualWidth = decodeOptions.outWidth;
            int actualHeight = decodeOptions.outHeight;

            // Then compute the dimensions we would ideally like to decode to.
            int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight,
                    actualWidth, actualHeight);
            int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth,
                    actualHeight, actualWidth);

            // Decode to the nearest power of two scaling factor.
            decodeOptions.inJustDecodeBounds = false;
            // TODO(ficus): Do we need this or is it okay since API 8 doesn't support it?
            // decodeOptions.inPreferQualityOverSpeed = PREFER_QUALITY_OVER_SPEED;
            decodeOptions.inSampleSize =
                findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
            Bitmap tempBitmap =
                BitmapFactory.decodeByteArray(data, , data.length, decodeOptions);

            // If necessary, scale down to the maximal acceptable size.
            if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth ||
                    tempBitmap.getHeight() > desiredHeight)) {
                bitmap = Bitmap.createScaledBitmap(tempBitmap,
                        desiredWidth, desiredHeight, true);
                tempBitmap.recycle();
            } else {
                bitmap = tempBitmap;
            }
        }

        if (bitmap == null) {
            return Response.error(new ParseError(response));
        } else {
            return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
        }
    }
}
           

需要添加某種資料格式的Request時,隻需要繼承自Request類,并且實作相應的方法即可。這樣通過擴充的形式來應對軟體的變化或者說使用者需求的多樣性,即避免了破壞原有系統,又保證了軟體系統的可擴充性。

優點

  • 增加穩定性
  • 可擴充性高

接口隔離原則(Interface Segregation Principle)

用戶端不應該依賴它不需要的接口;一個類對另一個類的依賴應該建立在最小的接口上。根據接口隔離原則,當一個接口太大時,我們需要将它分割成一些更細小的接口,使用該接口的用戶端僅需知道與之相關的方法即可。

可能描述起來不是很好了解,我們還是以示例來加強了解吧。

示例

我們知道,在Volley的網絡隊列中是會對請求進行排序的。Volley内部使用PriorityBlockingQueue來維護網絡請求隊列,PriorityBlockingQueue需要調用Request類的compareTo函數來進行排序。試想一下,PriorityBlockingQueue其實隻需要調用Request類的排序方法就可以了,其他的接口它根本不需要,即PriorityBlockingQueue隻需要compareTo這個接口,而這個compareTo方法就是我們上述所說的最小接口。當然compareTo這個方法并不是Volley本身定義的接口方法,而是Java中的Comparable接口,但我們這裡隻是為了學習本身,至于哪裡定義的無關緊要。

public abstract class Request<T> implements Comparable<Request<T>> {
  /**
   * 排序方法,PriorityBlockingQueue隻需要調用元素的compareTo即可進行排序
   */
  @Override
  public int compareTo(Request<T> other) {
      Priority left = this.getPriority();
      Priority right = other.getPriority();

      // High-priority requests are "lesser" so they are sorted to the front.
      // Equal priorities are sorted by sequence number to provide FIFO ordering.
      return left == right ?
              this.mSequence - other.mSequence :
              right.ordinal() - left.ordinal();
  }
}
           

PriorityBlockingQueue類相關代碼 :

public class PriorityBlockingQueue<E> extends AbstractQueue<E>
    implements BlockingQueue<E>, java.io.Serializable {

    // 代碼省略

        // 添加元素的時候進行排序
        public boolean offer(E e) {
        if (e == null)
            throw new NullPointerException();
        final ReentrantLock lock = this.lock;
        lock.lock();
        int n, cap;
        Object[] array;
        while ((n = size) >= (cap = (array = queue).length))
            tryGrow(array, cap);
        try {
            Comparator<? super E> cmp = comparator;
            // 沒有設定Comparator則使用元素本身的compareTo方法進行排序
            if (cmp == null)
                siftUpComparable(n, e, array);
            else
                siftUpUsingComparator(n, e, array, cmp);
            size = n + ;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
        return true;
    }

    private static <T> void siftUpComparable(int k, T x, Object[] array) {
        Comparable<? super T> key = (Comparable<? super T>) x;
        while (k > ) {
            int parent = (k - ) >>> ;
            Object e = array[parent];
            // 調用元素的compareTo方法進行排序
            if (key.compareTo((T) e) >= )
                break;
            array[k] = e;
            k = parent;
        }
        array[k] = key;
    }
 }
           

從PriorityBlockingQueue的代碼可知,在元素排序時,PriorityBlockingQueue隻需要知道元素是個Comparable對象即可,不需要知道這個對象是不是Request類以及這個類的其他接口。它隻需要排序,是以我隻要知道它是實作了Comparable接口的對象即可,Comparable就是它的最小接口,也是通過Comparable隔離了PriorityBlockingQueue類對Request類的其他方法的可見性。

優點

  • 降低耦合性
  • 提升代碼的可讀性
  • 隐藏實作細節

迪米特原則(Law of Demeter)

迪米特法則也稱為最少知識原則(Least Knowledge Principle),雖然名字不同,但描述的是同一個原則:一個對象應該對其他對象有最少的了解。通俗地講,一個類應該對自己需要耦合或調用的類知道得最少,這有點類似接口隔離原則中的最小接口的概念。類的内部如何實作、如何複雜都與調用者或者依賴者沒關系,調用者或者依賴者隻需要知道他需要的方法即可,其他的我一概不關心。類與類之間的關系越密切,耦合度越大,當一個類發生改變時,對另一個類的影響也越大。

迪米特法則還有一個英文解釋是: Only talk to your immedate friends( 隻與直接的朋友通信。)什麼叫做直接的朋友呢?每個對象都必然會與其他對象有耦合關系,兩個對象之間的耦合就成為朋友關系,這種關系的類型有很多,例如組合、聚合、依賴等。

示例

例如,Volley中的Response緩存接口的設計。

/**
 * An interface for a cache keyed by a String with a byte array as data.
 */
public interface Cache {
    /**
     * 擷取緩存
     */
    public Entry get(String key);

    /**
     * 添加一個緩存元素
     */
    public void put(String key, Entry entry);

    /**
     * 初始化緩存
     */
    public void initialize();

    /**
     * 辨別某個緩存過期
     */
    public void invalidate(String key, boolean fullExpire);

    /**
     * 移除緩存
     */
    public void remove(String key);

    /**
     * 清空緩存
     */
    public void clear();
}
           

Cache接口定義了緩存類需要實作的最小接口,依賴緩存類的對象隻需要知道這些接口即可。例如緩存的具體實作類DiskBasedCache,該緩存類将Response序列化到本地,這就需要操作File以及相關的類。代碼如下 :

public class DiskBasedCache implements Cache {

    /** Map of the Key, CacheHeader pairs */
    private final Map<String, CacheHeader> mEntries =
            new LinkedHashMap<String, CacheHeader>(, f, true);

    /** The root directory to use for the cache. */
    private final File mRootDirectory;

    public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) {
        mRootDirectory = rootDirectory;
        mMaxCacheSizeInBytes = maxCacheSizeInBytes;
    }

    public DiskBasedCache(File rootDirectory) {
        this(rootDirectory, DEFAULT_DISK_USAGE_BYTES);
    }

    // 代碼省略
}
           

在這裡,Volley的直接朋友就是DiskBasedCache,間接朋友就是mRootDirectory、mEntries等。Volley隻需要直接和Cache類互動即可,并不需要知道File、mEntries等對象的存在。這就是迪米特原則,盡量少的知道對象的資訊,隻與直接的朋友互動。

優點

  • 降低複雜度
  • 降低耦合度
  • 增加穩定性

總結六大原則

面向對象六大原則在開發過程中極為重要,如果能夠很好地将這些原則運用到項目中,再在一些合适的場景運用一些前人驗證過的模式,那麼開發出來的軟體在一定程度上能夠得到品質保證。其實稍微一想,這幾大原則最終就化為這麼幾個關鍵詞: 抽象、單一職責、最小化。那麼在實際開發過程中如何權衡、實踐這些原則,筆者也在不斷地學習、摸索。我想學習任何的事物莫過于實踐、經驗與領悟,在這個過程中希望能夠與大家分享知識、共同進步。

繼續閱讀