通過代碼示例來學習面向對象六大原則

或許有的掘友們發現了,在閱讀 Android 系統底層源碼或者開源框架源碼時,發現內部大量的設計模式,如果你對設計模式不懂的話,那麼閱讀源碼真的是寸步難行。那麼這篇文章我們先來學習面向對象的六大原則,設計模式大概 23 種,後面我們一步一步來學習它。

單一職責原則

單一職責原則的英文名稱是 Single Responsibility Principle ,縮寫是 SRP 。 SRP 的定義是:就一個類而言,應該僅有一個引起變化的原因。簡單的來說,就是一個類中應該是一組相關性很高的函數、數據的封裝。單一職責的劃分界限也並不是那麼的清晰,很多時候都是靠個人經驗來給定界限,當然,最大的的問題就是對職責的定義,什麼是類的職責,以及怎麼劃分類的職責。

下面我們就以 圖片加載庫 的例子代碼來對類的職責簡單說明下,在設計一個圖片加載庫之前,我們需要先大概畫下 UML 類圖,有了 UML 圖之後寫代碼就能更加的清晰。

在這裏插入圖片描述

從上面 UML 類圖可以看出 ImageLoader 只負責加載圖片,MemoryCache 實現 IImageCache 負責往內存中存/取緩存,到這裏也許有的同學對單一職責有了一定概念了,相信看完下面的代碼,你已經對單一職責掌握的差不多了,直接上代碼

public class ImageLoader {


    /**
     * 內存緩存
     */
    private IImageCache mMemoryCache;
    
        /**
     * 圖片下載
     */
    private IDownloader mImageDownloader;

    /**
     * 線程池
     */
    private ExecutorService mExecutorService;

    /**
     * 主線程管理
     */
    private Handler mHandler = new Handler(Looper.getMainLooper());

    private static ImageLoader instance;

    public static ImageLoader getInstance() {
        if (instance == null)
            instance = new ImageLoader();

        return instance;
    }

    public ImageLoader() {
         //圖片緩存
       this. mMemoryCache = new MemoryCache();
       //圖片下載
       this.mImageDownloader = new HttpURLConnectionDownloaderImp();
        //線程池,線程數據量爲 CPU 的數量
       this.mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
    }

    /**
     * 加載圖片
     */
    public void loadImage(final String url, final ImageView imageView) {
        Bitmap bitmap = mMemoryCache.get(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }

        imageView.setTag(url);

        //如果內存緩存中沒有圖片,就開啓網絡請求去下載
        mExecutorService.submit(new Runnable() {
            @Override
            public void run() {
                Bitmap imager = mImageDownloader.downLoader(url);
                if (imager == null) return;
                if (imageView.getTag().equals(url)) {
                    displayImage(imager, imageView);
                }
                mMemoryCache.put(url,imager);
            }
        });

    }

    /**
     * 顯示圖片
     *
     * @param downBitmap
     * @param imageView
     */
    private void displayImage(final Bitmap downBitmap, final ImageView imageView) {
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                imageView.setImageBitmap(downBitmap);
            }
        });
    }

}
public class MemoryCache implements IImageCache {

    /**
     * 初始化內存緩存
     */
    private LruCache<String, Bitmap> mMemoryLru;

    public MemoryCache() {
        init();
    }

    private void init() {
        int currentMaxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        //內存緩存的大小
        int cacheSize = currentMaxMemory / 4;
        mMemoryLru = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes() * value.getHeight() / 1024;
            }
        };
    }

    @Override
    public void put(String url, Bitmap bitmap) {
        mMemoryLru.put(url,bitmap);
    }

    @Override
    public Bitmap get(String url) {
        return mMemoryLru.get(url);
    }
}

通過上面代碼可以看出 ImageLoader 負責圖片加載的邏輯,而 MemoryCache 負責緩存,這 2 個類職責分明,就像公司裏面不同部門幹不同的活一樣。但是,如果這 2 類寫在一起的話,缺點一下就出來了,不僅功能職責不分明,而且代碼也比較臃腫,耦合太重。
現在雖然代碼結構變得清晰,職責也分明瞭,但是可擴展性還需要進一步優化,下面我們就來慢慢優化吧。

開閉原則

開閉原則英文全稱是 Open Close Principle,縮寫 OCP ,它是 Java 世界裏最基礎的設計原則,它指導我們如何建立一個穩定的、靈活的系統。
開閉原則的定義是:軟件中的對象 (類、模塊、函數等) 應該對於擴展是開放的,但是,對於修改是封閉的 這就是開放-關閉原則。

上一小節的 ImageLoader 職責單一,結構清晰,應該算是一個不錯的開始了,但是 Android 中應用內存是有限制的,當應用重新啓動,那麼原有的緩存就不在了。現在我們加上本地磁盤緩存,爲了遵從開閉原則的思想,我又對 ImageLoader 重新設計了。

在這裏插入圖片描述

public class ImageLoader {
    private String TAG = getClass().getSimpleName();


    /**
     * 默認內存緩存
     */
    private IImageCache mMemoryCache;

    /**
     * 線程池
     */
    private ExecutorService mExecutorService;

    /**
     * 主線程管理
     */
    private Handler mHandler = new Handler(Looper.getMainLooper());

    private static ImageLoader instance;

    public static ImageLoader getInstance() {
        if (instance == null)
            instance = new ImageLoader();

        return instance;
    }

    public ImageLoader() {
        mMemoryCache = new MemoryCache();
        //線程池,線程數據量爲 CPU 的數量
        mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
    }

    /**
     * 用戶配置緩存策略
     *
     * @param imageCache
     */
    public void setImageCache(IImageCache imageCache) {
        this.mMemoryCache = imageCache;
    }

    /**
     * 加載圖片
     */
    public void loadImage(final String url, final ImageView imageView) {
       .....

    }

    /**
     * 顯示圖片
     *
     * @param downBitmap
     * @param imageView
     */
    private void displayImage(final Bitmap downBitmap, final ImageView imageView) {
     .....

    }

}
//磁盤緩存
public class DiskCache implements IImageCache {

    private DiskLruCache mDiskLruCache;

    private static final int MAX_SIZE = 10 * 1024 * 1024;//10MB
    //IO緩存流大小
    private static final int IO_BUFFER_SIZE = 8 * 1024;

    //緩存個數
    private static final int DISK_CACHE_INDEX = 0;

    public DiskCache(Context context) {
        try {
            File cacheDir = CacheUtils.getDiskCacheDir(context, "bitmapCache");
            if (!cacheDir.exists()) {
                cacheDir.mkdirs();
            }
            mDiskLruCache = DiskLruCache.open(cacheDir, ImageLoaderUtils.getAppVersion(context), 1, MAX_SIZE);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    @Override
    public void put(String url, Bitmap bitmap) {
        OutputStream outputStream = null;
        DiskLruCache.Snapshot snapshot = null;
        BufferedOutputStream out = null;
        BufferedInputStream in = null;
        String key = ImageLoaderUtils.hashKeyForDisk(url);
        try {
            snapshot = mDiskLruCache.get(key);
            if (snapshot != null) {
                DiskLruCache.Editor editor = mDiskLruCache.edit(key);
                if (editor != null) {
                    outputStream = editor.newOutputStream(DISK_CACHE_INDEX);

                    InputStream inputStream = ImageLoaderUtils.bitmap2InputStream(bitmap, 50);

                    in = new BufferedInputStream(inputStream, IO_BUFFER_SIZE);
                    out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
                    int b;
                    while ((b = in.read()) != -1) {
                        out.write(b);
                    }
                    editor.commit();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (snapshot != null) {
                    snapshot.close();
                }
                if (out != null) {
                    out.close();
                }
                if (in != null) {
                    in.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }

    @Override
    public Bitmap get(String url) {
        //通過key值在緩存中找到對應的Bitmap
        Bitmap bitmap = null;
        String key = ImageLoaderUtils.hashKeyForDisk(url);
        try {
            DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
            if (snapshot == null) return null;
            //得到文件輸入流
            InputStream fileInputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
            if (fileInputStream != null)
                bitmap = BitmapFactory.decodeStream(fileInputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }


}
public class DoubleCache implements IImageCache {

    private String TAG = getClass().getSimpleName();

    /**
     * 內存緩存
     */
    private IImageCache mMemoryCache;

    /**
     * 磁盤緩存
     */
    private IImageCache mDiskCache;

    public DoubleCache(Context context) {
        this.mMemoryCache = new MemoryCache();
        this.mDiskCache = new DiskCache(context);
    }

    @Override
    public void put(String key, Bitmap bitmap) {
        mMemoryCache.put(key, bitmap);
        mDiskCache.put(key, bitmap);
    }

    @Override
    public Bitmap get(String url) {
        Bitmap bitmap = mMemoryCache.get(url);
        if (bitmap != null) {
            Log.i(TAG,"使用內存緩存");
            return bitmap;
        }
        Log.i(TAG,"使用磁盤緩存");
        return mDiskCache.get(url);
    }
}
public interface IImageCache {

    /**
     * 存圖片
     */
    void put(String url, Bitmap bitmap);

    /**
     * 獲取圖片
     */
    Bitmap get(String url);
}

IImageCache 接口簡單定義了 存儲/獲取 兩個函數,緩存的 url 就是圖片網絡地址,值就是緩存的圖片,經過這次重構我們擴展了內存/磁盤緩存,細心的同學可能注意到了, ImageLoader 類中增加了一個 setImageCache (IImageCache cache) 函數,用戶可以通過該函數來設置緩存,也就是通常說的依賴注入。下面看看怎麼配置:

public void config() {
        //使用雙緩存
  ImageLoader.getInstance().setImageCache(new DoubleCache(getApplicationContext()));

        //用戶自定義
  ImageLoader.getInstance().setImageCache(new IImageCache() {
            @Override
    public void put(String url, Bitmap bitmap) {
                
    }

            @Override
    public Bitmap get(String url) {
     return null;
     }
        });
    }

在上述代碼中,通過 setImageCache() 方法注入不同的緩存實現,這樣不僅能夠使 ImageLoader 更簡單,健壯,也使得 ImageLoader 的可擴展性,靈活性能高,MemoryCache 、DiskCache 、DoubleCache 緩存圖片的具體實現完全一樣,但是,他們的一個特點是都實現了 ImageCache 接口,並且通過 setImageCache() 注入到 IImageCache 中,這樣就實現了千變萬化的緩存策略,且擴展不會導致內部的修改,哈哈,這就是我們之前所說的開閉原則。

里氏替換原則

里氏替換原則英文全稱是 Liskov Substitution Principle , 縮寫是 LSP。LSP 的第一種定義是:**如果對每一個類型爲 S 的對象 O1, 都有類型爲 T 的對象 O2, 使得以 T 定義的所有程序 P 在所有的對象 O1都替換成 O2 時,程序 P 的行爲沒有發生變化,那麼類型 S 是類型 T 的子類型。**上面這種描述確實有點不好理解,我們再來看第二種里氏替換原則定義:所有引用基類的地方必須能透明地使用其子類的對象。

我們知道,面嚮對象語言的三大特點是 繼承,封裝,多態,里氏替換原則就是依賴於 繼承,多態這兩大特性。里氏替換原則通俗來說的話就是,只要父類能出現的地方子類就可以出現,而且替換爲子類也不會產生任何錯誤或異常,使用者可能根本不用知道是父類還是子類,但是反過來就不行了,有子類出現的地方,父類就不一定能適應,說了這麼多,其實最終總結就兩個字:抽象。

爲了我們能夠深入理解直接看下面代碼示例吧:

在這裏插入圖片描述

//框口類
public class Window{
  public void show(View view){
    view.draw();
  }
}

//建立視圖對象,測量視圖的寬高爲公用代碼,繪製實現交給具體的子類
pubic abstract class View{
  public abstract void draw();
  public vid measure(int width,int height){
    //測量視圖大小
  }
}

public class ImageView extends View{
  draw{
    //繪製圖片
  }
}

... extends View{
  ...
}

上述示例代碼中, Window 依賴於 View , 而 View 定義了一個視圖抽象, measure 是各個子類共享的方法,子類通過重寫 View 的draw 方法實現具有各自特色的功能,在這裏,這個功能就是繪製自身的內容,在任何繼承 View 類的子類都可以傳遞給 show 函數,這就是所說的里氏替換。
里氏替換原則的核心原理是抽象,抽象又依賴於繼承這個特性,在 OOP 當中,繼承的優缺點都相當明顯,優點:

  1. 代碼複用,減少創建類的成本,每個子類都擁有父類的方法和屬性;
  2. 子類於父類基本相似,但又與父類有所區別;
  3. 提高代碼的可擴展性;

繼承的缺點:

  1. 繼承是侵入性的,只要繼承就必須擁有父類的所有屬性和方法;
  2. 可能造成子類代碼冗餘,靈活性降低,因爲子類必須擁有父類的屬性和方法。

事務都是都利和弊,須合理利用。

繼續拿上面的 ImageLoader 緩存策略來說明裏氏替換原則,用戶只需要指定具體的緩存對象就可以通過 ImageCache 的 setImageCache() 函數就可以替換 ImageLoader 的緩存策略,這就使得 ImageLoader 的緩存系統有了無限的可能性,也保證了可擴展性。

開閉和里氏往往是生世相依,不離不棄,通過里氏替換來達到程序的擴展,對修改的關閉效果。然而,這兩個原則都同時強調了一個 OOP 的重要性 - 抽象,因此,在開發過程中,運用抽象是走向代碼優化的重要一步。

依賴倒置原則

依賴倒置原則英文全稱是 Dependence Inversion Principle, 簡寫 DIP 。依賴倒置原則指代了一種特定的解耦形式,使得高層次的模塊不依賴於底層次模塊的實現細節的目的,依賴模塊被顛倒了。這個概念有點不好理解,這到底是什麼意思勒?

依賴倒置有幾個關鍵點:

  1. 高層模塊不應該依賴底層模塊,兩者都應該依賴起抽象;
  2. 抽象不應該依賴細節;
  3. 細節應該依賴抽象;

在 Java 語言中,抽象就是接口或抽象類,兩者都是不能直接被實例化的;細節就是實現類,其特點就是可以直接實例化,也就是可以加上一個 new 關鍵字產生一個對象。高層模塊就是調用端,底層模塊就是具體實現類。依賴倒置原則在 Java 語言中的表現就是: 模塊間的依賴通過抽象發生,實現類之間不發生直接的依賴關係,其依賴關係是通過接口或抽象類產生的 ,這又是一個將理論抽象化的實例,其實一句話可以概括:面向接口編程,或者說是面向抽象編程,面向接口編程是面向對象精髓之一,也就是上面兩節強調的抽象。

這裏我們還是以 ImageLoader 來說明,先看下面代碼:

public class ImageLoader {
    private String TAG = getClass().getSimpleName();


    /**
     * 默認內存緩存(直接依賴於細節,而不是抽象)
     */
    private MemoryCache mMemoryCache;

    /**
     * 線程池
     */
    private ExecutorService mExecutorService;

    /**
     * 主線程管理
     */
    private Handler mHandler = new Handler(Looper.getMainLooper());

    private static ImageLoader instance;

    public static ImageLoader getInstance() {
        if (instance == null)
            instance = new ImageLoader();

        return instance;
    }

    public ImageLoader() {
        mMemoryCache = new MemoryCache();
        //線程池,線程數據量爲 CPU 的數量
        mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
    }

    /**
     * 用戶配置緩存策略
     *
     * @param imageCache
     */
    public void setImageCache(MemoryCache imageCache) {
        this.mMemoryCache = imageCache;
    }

...
}

上面代碼 ImageLoader 直接依賴於細節 MemoryCache ,如果框架升級需有多級緩存也就是內存 + SD 卡緩存策略,那麼就又需要改 ImageLoader 中的代碼,如下:

public class ImageLoader {
    private String TAG = getClass().getSimpleName();


    /**
     * 默認內存緩存(直接依賴於細節,而不是抽象)
     */
    private DoubleCache mMemoryCache;

    /**
     * 線程池
     */
    private ExecutorService mExecutorService;

    /**
     * 主線程管理
     */
    private Handler mHandler = new Handler(Looper.getMainLooper());

    private static ImageLoader instance;

    public static ImageLoader getInstance() {
        if (instance == null)
            instance = new ImageLoader();

        return instance;
    }

    public ImageLoader() {
        mMemoryCache = new DoubleCache();
        //線程池,線程數據量爲 CPU 的數量
        mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
    }

    /**
     * 用戶配置緩存策略
     *
     * @param imageCache
     */
    public void setImageCache(DoubleCache imageCache) {
        this.mMemoryCache = imageCache;
    }

...
}

在 ImageLoader 中我們把默認內存緩存改成了雙緩存,這樣不僅違背了沒有開閉原則,也沒有依賴於抽象,所以下面的代碼纔是正確的:

public class ImageLoader {
    private String TAG = getClass().getSimpleName();
    /**
     * 默認內存緩存 默認依賴於抽象
     */
    private IImageCache mMemoryCache;

    private static ImageLoader instance;

    public static ImageLoader getInstance() {
        if (instance == null)
            instance = new ImageLoader();

        return instance;
    }

    public ImageLoader() {
     ...
    }

    /**
     * 用戶配置緩存策略 注入抽象類
     *
     * @param imageCache
     */
    public void setImageCache(IImageCache imageCache) {
        this.mMemoryCache = imageCache;
    }
}

在這裏實現類沒有發生直接的依賴,而是通過抽象發生的依賴。滿足了依賴倒置基本原則,想要讓程序更爲靈活,那麼抽象就是邁出靈活的第一步。

接口隔離原則

接口隔離原則英文全稱是 InterfaceSegregation Principles, 縮寫 ISP 。接口隔離原則的目的是系統解耦,從而容易重構、更改和重新部署。說白了就是讓客服端依賴的接口儘可能地小,這樣說可能還有點抽象,還是以一個示例說明一下

未優化的接口

public class DiskCache implements IImageCache {

    private DiskLruCache mDiskLruCache;

    private static final int MAX_SIZE = 10 * 1024 * 1024;//10MB
    //IO緩存流大小
    private static final int IO_BUFFER_SIZE = 8 * 1024;

    //緩存個數
    private static final int DISK_CACHE_INDEX = 0;

   
    @Override
    public void put(String url, Bitmap bitmap) {
       .....
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (snapshot != null) {
                    snapshot.close();
                }
                if (out != null) {
                    out.close();
                }
                if (in != null) {
                    in.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }

        }
    }
}

可以看見上面一段代碼雖然功能達到了要求,但是各種 try…catch 嵌套,不經影響代碼美觀,而且可讀性差。我們可以看 Cloaseable 這個類的實現差不多 160 多個實現類,如果每個類都 close 那不的瘋了,我們直接抽取一個 CloseUtils 如下:

public class CloaseUtils {

    public static void close(Closeable... closeable) {
        if (closeable != null) {
            try {
                if (closeable.length == 1){
                    closeable[0].close();
                    return;
                }
                for (int i = 0; i < closeable.length; i++) {
                    closeable[i].close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

支持同時關閉一個,或多個實現類的 close。

改造之後的代碼:

public class DiskCache implements IImageCache {

    private DiskLruCache mDiskLruCache;

    private static final int MAX_SIZE = 10 * 1024 * 1024;//10MB
    //IO緩存流大小
    private static final int IO_BUFFER_SIZE = 8 * 1024;

    //緩存個數
    private static final int DISK_CACHE_INDEX = 0;

   
    @Override
    public void put(String url, Bitmap bitmap) {
       .....
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
      		 CloaseUtils.close(snapshot,out,in);
        }
    }
}

是不是清爽多了,一行代碼解決了剛剛差不多 10 行代碼的邏輯。而且這裏基本原理就是依賴於 Closeable 抽象,而不是具體實現類(這不就是我們剛剛纔說了的依賴倒置原則嘛),並且建立在最小化依賴原則的基礎上,它只需要知道這個對象是否關閉,其它一概不關心,也就是這裏的接口隔離原則。

迪米特原則

迪米特原則英文的全稱爲 Law of Demeter , 縮寫是 LOD , 也稱爲最少知識原則。雖然名字不同,但描述的是同一個原則:一個對象應該對其他對象有最少的的瞭解。通俗的將,一個類應該對自己需要耦合或調用的類知道的最少,類的內部如何實現與調用者或者依賴者沒有關係,調用者或者依賴着只需要知道它需要的方法即可,其他的可一概不用管。類與類之間關係密切,耦合度就越大,當一個類發生改變時,對另一個類的影響也越大。

下面以一個租房例子說明:

在這裏插入圖片描述

/**房子*/
public class Room {
  //面積
  public float area;
  //價錢
  public float price;
  
  public Room(float area,float price){
    this.area = area;
    this.price = price;
  }
}
/**中介*/
public class Mediator{
  List <Room> mRooms = new ArrayList<Room>();
  
  public Mediator(){
    for(i = 0; i < 5 ; i ++){
      mRoom.add(new Room(14 + i,(14 + i) * 150));
    }
  }
  
  public List<Room> getAllRooms(){
    return mRooms;
  }
}
/**租客**/
public class Tenant {
  public void rentRoom(float roomArea,float roomPrice,Mediator mediator){
    List<Room> rooms = mediator.getAllRooms();
  	for(Room room : rooms){
      if(isSuitable(roomArea,roomPrice,room)){
        Log.i(TAG,"租到房子了");
      bread;
      }
    }
  }
  
  //租金要小於等於指定的值,面積要大於等於指定的值
  public boolean isSuitable(float roomArea,float roomPrice,Room room){
    return room.price <= roomPrice && room.area >= roomArea;
  }
}

上面的代碼中可以看到,Tenant 不僅依賴了 Mediator 類,還需要頻繁得於 Room 類打交道。租客只是找一個房子而已,如果把這些功能都放在 Tenant 類裏面,那中介都沒有存在感了吧?耦合太重了,我們只需要跟中介通信就行了,繼續重構代碼;

在這裏插入圖片描述

//中介
public class Mediator{
  List<Room> mRooms = new ArrayList<Room>();
  
  /**構造房子**/
  public Mediator(){
    for(i = 0; i < 5 ; i ++){
      mRoom.add(new Room(14 + i,(14 + i) * 150));
    }
  }
  
  public Room rentOut(float area,float price){
    for(Room room : mRooms){
      if(isSuitable(area,price,room)){
        return room;
      }
    }
    return null;
  }
public boolean isSuitable(float area,float price ,Room room){
  return room.price <= price && room.area >= area
	}
}
//租客
public class Tenant{
  /**是否租到房子了*/
  public Room rentRoom(float roomArea,float roomPrice,Mediator mediator){
    return mediator.rentOut(roomArea,roomPrice);
  }
}

總結

從六大原則中我們得出了重要的結論,就是一定要有抽象的思維,面向抽象或面向接口編程。在應用開發過程中,最難的不是完成開發工作,而是後續的維護和迭代工作是否擁有可變性,擴展性,在不破壞系統的穩定性前提下依然保持 二高一低原則(高擴展,高內聚,低耦合) 在經歷多個版本的迭代項目依然保持清晰,靈活,穩定的系統架構。當然這是我們一個比較理想的情況,但是我們需要往這個方向去實現努力,就相當於接口(想法)出來了,我們要去實現(接口實現類)它,遵循面向對象六大原則就是我們走向靈活軟件之路所邁出的第一步,加油!

代碼地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章