(零)设计模式六大基本原则

本文根据一个简单的例子(图片加载),分析下设计模式的六大基本原则,让你对设计模式的基础了然于胸。
前人栽树,后人乘凉,本文章的参考了《设计模式之禅》和《Android源码设计模式》,也算是个人的一个总结分析。
文章中的代码示例采用Kotlin来实现,只要你懂Java,都能理解。

1. 单一职责原则

单一职责原则的英文缩写为:SRP,Single Responsibility Principle.

定义为:就一个类而言,应该仅有一个引起它变化的原因。简单的来讲,一个类中应该是一组相关性很高的函数、数据的封装。

1.1 ImageLoader

我们来手写一个最简单的图片加载器。(Kotin实现)
虽然简单,但是它还是包含了一个简单的内存缓存。


package core.zs.pattern

import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.support.v4.util.LruCache
import android.widget.ImageView
import java.net.HttpURLConnection
import java.net.URL
import java.util.concurrent.Executors

/**
 * @function 图片加载管理器
 * @author ZhangShuai.
 * @created 2018/4/25.
 */
class ImageLoader {

    private lateinit var mImageCache: LruCache<String, Bitmap>

    //定义线程池来下载图片
    private val mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())

    init {
        initImageCache()
    }


    /**
     * 显示图片
     * @param url 图片url
     * @param imgView 待显示图片的ImageView
     */
    fun displayImage(url: String, imgView: ImageView) {
        imgView.tag = url
        mExecutorService.submit {
            val bitmap = downloadImage(url) ?: return@submit
            if (imgView.tag == url) {
                imgView.setImageBitmap(bitmap)
            }
            mImageCache.put(url, bitmap)
        }
    }

   private fun initImageCache() {
        // 计算可以使用的最大内存
        val maxMemory = Runtime.getRuntime().maxMemory() / 1024
        // 取四分之一作为内存缓存大小
        val cacheSize = (maxMemory / 4).toInt()
        mImageCache = object : LruCache<String, Bitmap>(cacheSize) {
            override fun sizeOf(key: String, bitmap: Bitmap): Int {
                return bitmap.rowBytes * bitmap.height / 1024
            }
        }
    }


    /**
     * 下载图片。
     * @param url 图片url
     * @return 解析的图片
     */
    private fun downloadImage(url: String): Bitmap? {
        var bitmap: Bitmap? = null
        try {
            val url = URL(url)
            val conn = url.openConnection() as HttpURLConnection
            bitmap = BitmapFactory.decodeStream(conn.inputStream)
            conn.disconnect()
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return bitmap
    }
}

我们来分析上面的加载器,是否满足SRP原则?

图片加载器不仅包含了图片的显示和加载,还包含了缓存的处理和设置,很明显它是不符合SRP的原则。

怎么处理?

将图片的缓存实现单独拆分出来。

1.2 改进后的代码

ImageCache:将改造前ImageLoader中内存缓存的内容抽取。
ImageCache负责内存缓存功能的实现,在ImageLoader中使用。

class ImageCache {
    private lateinit var mImageCache: LruCache<String, Bitmap>

    init {
        initImageCache()
    }

    private fun initImageCache() {
        // 计算可以使用的最大内存
        val maxMemory = Runtime.getRuntime().maxMemory() / 1024
        // 取四分之一作为内存缓存大小
        val cacheSize = (maxMemory / 4).toInt()
        mImageCache = object : LruCache<String, Bitmap>(cacheSize) {
            override fun sizeOf(key: String, bitmap: Bitmap): Int {
                return bitmap.rowBytes * bitmap.height / 1024
            }
        }
    }

    fun put(url: String, bitmap: Bitmap) {
        mImageCache.put(url, bitmap)
    }

    fun get(url: String): Bitmap? {
        return mImageCache.get(url)
    }
}

ImageLoader


class ImageLoader {
    //定义线程池来下载图片
    private val mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())

    // 原来那一部分的initImageCache不见了,我们只是引用了缓存的内容
    private  var mImageCache: ImageCache = ImageCache()


    /**
     * 显示图片
     * @param url 图片url
     * @param imgView 待显示图片的ImageView
     */
    fun displayImage(url: String, imgView: ImageView) {
        val bitmap = mImageCache.get(url)
        if (bitmap != null) {
            imgView.setImageBitmap(bitmap)
            return
        }
        imgView.tag = url
        mExecutorService.submit {
            val bitmap = downloadImage(url) ?: return@submit
            if (imgView.tag == url) {
                imgView.setImageBitmap(bitmap)
            }
            mImageCache.put(url, bitmap)
        }
    }


    /**
     * 下载图片。
     * @param url 图片url
     * @return 解析的图片
     */
    private fun downloadImage(url: String): Bitmap? {
        var bitmap: Bitmap? = null
        try {
            val url = URL(url)
            val conn = url.openConnection() as HttpURLConnection
            bitmap = BitmapFactory.decodeStream(conn.inputStream)
            conn.disconnect()
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return bitmap
    }

}

对比改造前和改造后的代码,改造后的代码明显逻辑上更加清楚,基本符合SRP。


2. 开闭原则

开闭原则的英文缩写为:OCP,Open Close Principle.

定义为:软件中的对象(类、模块、方法等)应该对于扩展是开放的,而对于修改是关闭的。

软件开发中最不会变化的,就是变化本身。(你是不是也深受其害)

程序一旦开发完成,程序中一个类的实现应该只因错误而被修改,新的或者改变的特性应该通过新建不同的类来实现,新建的类可以通过继承的方式来重用原来类的代码。

2.1 增加SD卡缓存

现在了来了新的需求,我们除了能使用内存缓存外,还要能使用SD卡缓存。

首先我们来实现SD卡缓存类。

class DiskCache {
    private val cacheDir = "sdcard/cache"
    fun put(url: String, bitmap: Bitmap) {
        var fous: FileOutputStream? = null
        try {
            fous = FileOutputStream(cacheDir + url)
            bitmap.compress(Bitmap.CompressFormat.PNG, 100, fous)
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            try {
                fous?.close()
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
    }
    fun get(url: String): Bitmap? {
        return BitmapFactory.decodeFile(cacheDir + url)
    }
}

再看下ImageLoader如何使用DiskCache,省略了非关键代码。


class ImageLoader {
  
    private var mImageCache: ImageCache = ImageCache()

    private val mDiskCache: DiskCache = DiskCache()
    // 是否使用SD卡缓存
    private var isUseDiskCache = false

    // 设置是否使用SD卡缓存
    fun useDiskCache(useDiskCache: Boolean) {
        isUseDiskCache = useDiskCache
    }


    /**
     * 显示图片
     * @param url 图片url
     * @param imgView 待显示图片的ImageView
     */
    fun displayImage(url: String, imgView: ImageView) {
        // 判断是否使用SD卡缓存
        val bitmap = if (isUseDiskCache) mDiskCache.get(url) else mImageCache.get(url)
        if (bitmap != null) {
            imgView.setImageBitmap(bitmap)
            return
        }

        imgView.tag = url
        mExecutorService.submit {
            val bitmap = downloadImage(url) ?: return@submit
            if (imgView.tag == url) {
                imgView.setImageBitmap(bitmap)
            }
            mImageCache.put(url, bitmap)
            mDiskCache.put(url, bitmap)
        }
    }

}

看起来实现起来还是很简单,修改的代码量也不是很大。

我们明显使用缓存的方式存在问题的,在实际开发中,我们可是既使用内存,又使用SD卡缓存的。

2.2 双缓存

首先我们来实现SD卡缓存类:DoubleCache.

class DoubleCache {

    private val mMemoryCache = ImageCache()

    private val mDiskCache = DiskCache()

    fun put(url: String, bitmap: Bitmap) {
        mMemoryCache.put(url, bitmap)
        mDiskCache.put(url, bitmap)
    }

    fun get(url: String): Bitmap? = mMemoryCache.get(url) ?: mDiskCache.get(url)
}

ImageLoader


class ImageLoader {
    //定义线程池来下载图片
    private val mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())

    private var mImageCache: ImageCache = ImageCache()
    private val mDiskCache: DiskCache = DiskCache()
    private val mDoubleCache: DoubleCache = DoubleCache()

    // 是否使用SD卡缓存
    private var isUseDiskCache = false
    // 是否使用双缓存
    private var isUseDoubleCache = false

    fun useDiskCache(useDiskCache: Boolean) {
        isUseDiskCache = useDiskCache
    }

    fun useDoubleCache(useDoubleCache: Boolean) {
        isUseDoubleCache = useDoubleCache
    }


    /**
     * 显示图片
     * @param url 图片url
     * @param imgView 待显示图片的ImageView
     */
    fun displayImage(url: String, imgView: ImageView) {
        // 根据设置选择合适的缓存
        val bitmap = when {
            isUseDoubleCache -> mDoubleCache.get(url)
            isUseDiskCache -> mDiskCache.get(url)
            else -> mImageCache.get(url)
        }


        if (bitmap != null) {
            imgView.setImageBitmap(bitmap)
            return
        }

        imgView.tag = url
        mExecutorService.submit {
            val bitmap = downloadImage(url) ?: return@submit
            if (imgView.tag == url) {
                imgView.setImageBitmap(bitmap)
            }
            mImageCache.put(url, bitmap)
            mDiskCache.put(url, bitmap)
        }
    }

}

2.3 分析

我们上面的修改或者设计符合OCP原则吗?

很明显不符合,每次我们需要修改缓存实现时,都需要修改ImageLoader,这就违背了对扩展开放对修改封闭的原则,而且修改原来的代码,很可能会引入新的Bug,破坏原来的实现。

如何修改

  1. 我们应该将缓存的实现设计为一个接口(ImageCache),而不是一个具体的类。

  2. ImageLoader持有一个ImageCahe的实例,而不关心的具体实现是谁。

2.4 改进

ImageCache

interface ImageCache {
    fun put(url: String, bitmap: Bitmap)
    fun get(url: String): Bitmap?
}

MemoryCache&DiskCache&DoubleCache


class MemoryCache : ImageCache {
    private lateinit var mImageCache: LruCache<String, Bitmap>

    init {
        initImageCache()
    }

    private fun initImageCache() {
        // 计算可以使用的最大内存
        val maxMemory = Runtime.getRuntime().maxMemory() / 1024
        // 取四分之一作为内存缓存大小
        val cacheSize = (maxMemory / 4).toInt()
        mImageCache = object : LruCache<String, Bitmap>(cacheSize) {
            override fun sizeOf(key: String, bitmap: Bitmap): Int {
                return bitmap.rowBytes * bitmap.height / 1024
            }
        }
    }

    override fun put(url: String, bitmap: Bitmap) {
        mImageCache.put(url, bitmap)
    }

    override fun get(url: String): Bitmap? {
        return mImageCache.get(url)
    }
}


class DiskCache : ImageCache {
    private val cacheDir = "sdcard/cache"

    override fun put(url: String, bitmap: Bitmap) {
        var fous: FileOutputStream? = null
        try {
            fous = FileOutputStream(cacheDir + url)
            bitmap.compress(Bitmap.CompressFormat.PNG, 100, fous)
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            try {
                fous?.close()
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
    }

    override fun get(url: String): Bitmap? {
        return BitmapFactory.decodeFile(cacheDir + url)
    }
}


class DoubleCache : ImageCache {
    private val mMemoryCache = MemoryCache()
    private val mDiskCache = DiskCache()
    override fun put(url: String, bitmap: Bitmap) {
        mMemoryCache.put(url, bitmap)
        mDiskCache.put(url, bitmap)
    }

    override fun get(url: String): Bitmap? = mMemoryCache.get(url) ?: mDiskCache.get(url)
}



ImageLoader


class ImageLoader {
    //定义线程池来下载图片
    private val mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
    // 并不具体的实现,只依赖抽象
    private var mImageCache: ImageCache  = MemoryCache()

    // 注入我们需要的ImageCache
    // imgCache可以为自定义对象,只要实现ImageCache接口即可
    fun setImageCache(imgCache: ImageCache) {
        this.mImageCache = imgCache
    }


    /**
     * 显示图片
     * @param url 图片url
     * @param imgView 待显示图片的ImageView
     */
    fun displayImage(url: String, imgView: ImageView) {
        val bitmap = mImageCache?.get(url)
        if (bitmap != null) {
            imgView.setImageBitmap(bitmap)
            return
        }

        imgView.tag = url
        mExecutorService.submit {
            val bitmap = downloadImage(url) ?: return@submit
            if (imgView.tag == url) {
                imgView.setImageBitmap(bitmap)
            }
            mImageCache?.put(url, bitmap)
        }
    }


    /**
     * 下载图片。
     * @param url 图片url
     * @return 解析的图片
     */
    private fun downloadImage(url: String): Bitmap? {
        var bitmap: Bitmap? = null
        try {
            val url = URL(url)
            val conn = url.openConnection() as HttpURLConnection
            bitmap = BitmapFactory.decodeStream(conn.inputStream)
            conn.disconnect()
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return bitmap
    }
}

通过setImageCache注入不同的缓存实现,使得ImageLoader更加健壮和简单,也提高了ImageLoader的可扩展性、灵活性。


3. 里氏替换原则

里氏替换原则的英文缩写为:LSP,Liskov Substitution Principle.

定义为:所有使用基类的地方,必须能透明的使用其子类的对象。

通俗的来讲,只要父类出现的地方,子类都可以出现。

3.1 OOP三大原则

面向对象的三大基本原则:封装、继承、多态。

3.2 继承

LSP的核心是抽象,抽象又依赖于继承这个特性。

继承的优点:
(1)代码重用,减少创建类的成本,每个子类都拥有父类的属性和方法。
(2)多态,子类与父类基本相似,但又与父类有所区别。
(3)提高代码的扩展性。

继承的缺点:
(1)继承是侵入性的,子类必须拥有父类的属性和方法。
(2)可能造成代码冗余,灵活性降低。

3.3 分析

在上一个章节中,我们定义了ImageCahe接口,在setImageCache(imgCache: ImageCache)接口,我们既可以传入MemoryCache,也可以传入DoubleCache,这也是里氏替换原则的体现。

试想如果setImageCache接口我们只能传入ImagaCache对象,而不能传入其子类,那么每次拓展我们就必须修改ImageLoader,增加一堆isUseDoubleCache、isUseDiskCache值类的变量,那么就毫无扩展可言了。


4. 依赖倒置原则

依赖倒置原则的英文缩写为:DIP,Dependence Inversion Principle.

依赖倒置原则指代了一种解耦形式,使得高层模块不依赖于低层模块的实现细节,依赖模块被倒置了。

DIP主要有以下几个关键点:
(1)高层模块不应该依赖于低层模块,两者都应该依赖于抽象。
(2)抽象不应该依赖于细节。
(3)细节应该依赖于抽象。

在Java中抽象指的就是接口、抽象类,细节就是具体的实现类。

依赖倒置原则在Java中的表现就是:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或者抽象类产生的。

4.1 分析

在[2.2]章节的代码中,ImageLoader就直接依赖于MemoryCache\DoubleCache\DiskCache这几个实现类,如果ImageCache需要一个新缓存实现,那么我们就必须修改ImageLoader的实现。

在后面的改进中,我们修改了缓存的实现,将缓存单独抽取为一个接口(ImageCache),并在ImageLoader中增加了setImageCache方法。
每当用于需要不同缓存实现时,都可以直接通过setImageCache进行依赖注入,保证了系统的灵活性。

4.2 简单回顾


class ImageLoader {
    // 图片缓存类,依赖于抽象的ImageCache接口,提供一个默认的实现
    private var mImageCache: ImageCache = MemoryCache()

    // 设置缓存策略,依赖于抽象
    // 依赖注入的方法
    fun setImageCache(imgCache: ImageCache) {
        this.mImageCache = imgCache
    }

}


5. 接口隔离原则

英文缩写为:ISP,Interface Segregation Principle.

定义:客户端不应该依赖它不需要的接口。

另一种定义:类之间的依赖关系应该建立在最小的接口上。

5.1 讨厌的关闭

首先来看一段代码:

override fun put(url: String, bitmap: Bitmap) {
        var fous: FileOutputStream? = null
        try {
            fous = FileOutputStream(cacheDir + url)
            bitmap.compress(Bitmap.CompressFormat.PNG, 100, fous)
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            try {
                fous?.close()
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
    }

相信每个开发者都写过这样的代码,这样的代码(finally包括的那一段)既丑陋又冗余,那么我们是否可以简化它呢?

答案当然是可以的。

5.2 分析

我们首先来看下FileOutputStream的继承关系:

public class FileOutputStream extends OutputStream{}

public abstract class OutputStream implements Closeable, Flushable{}

public interface Closeable extends AutoCloseable {
    public void close() throws IOException;
}

FileInputStream的继承关系:

public class FileInputStream extends InputStream{}

public abstract class InputStream implements Closeable {}

通过分析我们知道,几乎所有的xxxOutputStream和xxxInputStream类都实现了Closeable,Closeable接口只有一个方法close,而这也是我们改造的原理所在。

5.3 关闭Closeable

我们实现了一个工具类,用于关闭流。

object IOCloseUtil {

    /**
     * 关闭流。
     * @param closeable 可关闭对象
     */
    fun closeQuietly(closeable: Closeable?) {
        try {
            closeable?.close()
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }

}

5.4 分析

试想IOCloseUtil的closeQuietly接口中,如果我们的参数对象是FileOutputStream类型,那么它就暴露了其他的方法(如write等),这样就使得更多的细节暴露在客户端的代码。

而通过Closeable接口将可关闭对象进行抽象,这样就可以对客户端隐藏其他接口信息,这就是使用ISP原则隔离了实现类的细节。


6. 迪米特原则

英文缩写为:LOD,Law of Demeter ,也叫最少知识原则.

定义:一个对象应该对其他对象有最少的了解。

一个类应该对自己需要耦合或者调用类知道的最少,类的内部如何实现如何实现与调用者或者依赖者没关系。
类与类之间关系越密切,耦合越严重,当一个类发生改变是,对另一个类的影响也越大。

6.1 租房

我们以租房为例来讲解该原则,我们在租房时不可避免的出现几个角色:房子、租房者、中介、房东。

奉上代码:

//房子有租金和面积大小两个属性
data class Room(val area: Float, val price: Float)

class Mediator {
    // 中介掌握着所有的房源信息
    val rooms = mutableListOf<Room>()

    init {
        (0..10).forEach {
            rooms.add(Room(10 + it.toFloat(), (10 + it) * 150.toFloat()))
        }
    }
}

/**
 * 租房者。
 * 要求:15平方米,价格不能大于2000元。
 */
class Tenant(var roomArea: Float = 15f, var roomPrice: Float = 2000f) {

    fun rentRoom(mediator: Mediator) {
        val rooms = mediator.rooms
        rooms.filter { room ->
            room.area == roomArea
                    && room.price <= roomPrice
        }.forEach { println(it) }
    }
}

在上面的代码中,Tenant不仅要依赖于Mediator,还要与Room打交道,导致Tenant耦合度过高。

我们正常的需求是,Tenant只需向Mediator表述需求,Mediator负责根据要求来寻找合适的Room。

6.2 改进

//房子有租金和面积大小两个属性
data class Room(val area: Float, val price: Float)

class Mediator {
    // 中介掌握着所有的房源信息
    val rooms = mutableListOf<Room>()

    init {
        (0..10).forEach {
            rooms.add(Room(10 + it.toFloat(), (10 + it) * 150.toFloat()))
        }
    }


    fun rentRoom(roomArea: Float, roomPrice: Float): List<Room> {
        return rooms.filter { room ->
            room.area == roomArea
                    && room.price <= roomPrice
        }
    }

}

/**
 * 租房者。
 * 要求:15平方米,价格不能大于2000元。
 */
class Tenant(var roomArea: Float = 15f, var roomPrice: Float = 2000f) {

    fun rentRoom(mediator: Mediator) {
        val suitableRooms = mediator.rentRoom(roomArea, roomPrice)
        suitableRooms.forEach { println(it) }
    }
}


只与直接朋友进行通信,这个几个字,就将我们从严重的耦合中解放了出来。


7. 总结

在学习设计模式之前,我们一般都需要了解下六大基本原则,这是设计模式的核心指导思想,在实际的开发中我们在写代码前也应该运用这些原则进行设计和指导我们。

祝各位工作愉快。

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

推荐阅读更多精彩内容