Android设计模式—面向对象六大原则

面向对象六大原则:

  1. 单一职责原则
  2. 开闭原则
  3. 里氏替换原则
  4. 依赖倒置原则
  5. 接口隔离原则
  6. 最少知识原则

单一职责原则

单一职责原则 Single Responsibility Principle,就一个类而言,应该仅有一个引起它变化的原因,简单来说,一个类中应该是一组相关性很高的函数、数据的封装。

需求:实现图片加载,并将图片缓存起来。

书上代码有LruCache、ExecutorService两个我不太熟悉的内容。看了几篇博客,就差不多了解一些了。

  1. 彻底解析Android缓存机制——LruCache
  2. Java线程池 ExecutorService
  3. 多线程ExecutorService中submit和execute区别

下面的ImageLoader耦合严重,加载图片和图片缓存的逻辑写在一个类中,没有拓展性、灵活性。随着功能增多,ImageLoader类会越来越大,代码越来越复杂,图片加载系统就越来越脆弱。

public class ImageLoader {
    //图片缓存
    LruCache<String, Bitmap> mImageCache;
    //加载图片的线程池
    //newFixedThreadPool创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
    //ui线程的handler 用来将图片加载进ImageView
    Handler mUiHandler = new Handler(Looper.getMainLooper());

    public ImageLoader() {
        initImageCache();
    }

    /**
     * 初始缓存
     */
    private void initImageCache() {
        //获取当前可用内存大小
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        //四分之一用来缓存
        final int cacheSize = maxMemory / 4;
        //设置LruCache缓存大小,重写sizeOf方法 单位要一致
        mImageCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
            }
        };
    }

    /**
     * 全过程
     * @param url 图片地址
     * @param imageView view
     */
    public void displayImage(final String url, final ImageView imageView) {
        imageView.setTag(url);
        //添加线程
        mExecutorService.submit(new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = mImageCache.get(url);
                if (bitmap == null) {
                    bitmap = downloadImage(url);
                    if (bitmap == null) {
                        return;
                    }
                    //放入缓存
                    mImageCache.put(url, bitmap);
                }
                //判断防止过程中又对imageView做了下载其他url图片的请求
                if (imageView.getTag().equals(url)) {
                    updateImageView(imageView, bitmap);
                }
            }
        });
    }

    /**
     * 图加载到view
     * @param imageView view
     * @param bitmap 图
     */
    private void updateImageView(final ImageView imageView, final Bitmap bitmap) {
        mUiHandler.post(new Runnable() {
            @Override
            public void run() {
                imageView.setImageBitmap(bitmap);
            }
        });

    }

    /**
     * 下载图片
     * @param imageUrl 图片地址
     * @return bitmap
     */
    public Bitmap downloadImage(String imageUrl) {
        Bitmap bitmap = null;
        try {
            URL url = new URL(imageUrl);
            final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            bitmap = BitmapFactory.decodeStream(connection.getInputStream());
            connection.disconnect();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }

}

把上面的ImageLoader拆分一下,各个功能独立出来,分为两个类,ImageLoader只负责图片加载逻辑,ImageCache只负责处理图片缓存的逻辑。

public class ImageLoader {
    ImageCache mImageCache = new ImageCache();
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
    Handler mUiHandler = new Handler(Looper.getMainLooper());

    public void displayImage(final String url, final ImageView imageView) {
        imageView.setTag(url);
        mExecutorService.submit(new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = mImageCache.get(url);
                if (bitmap == null) {
                    bitmap = downloadImage(url);
                    if (bitmap == null) {
                        return;
                    }
                    mImageCache.put(url, bitmap);
                }
                if (imageView.getTag().equals(url)) {
                    updateImageView(imageView, bitmap);
                }
            }
        });
    }

    private void updateImageView(final ImageView imageView, final Bitmap bitmap) {
        mUiHandler.post(new Runnable() {
            @Override
            public void run() {
                imageView.setImageBitmap(bitmap);
            }
        });

    }

    public Bitmap downloadImage(String imageUrl) {
        Bitmap bitmap = null;
        try {
            URL url = new URL(imageUrl);
            final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            bitmap = BitmapFactory.decodeStream(connection.getInputStream());
            connection.disconnect();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }

}
public class ImageCache {

    LruCache<String, Bitmap> mImageCache;

    public ImageCache() {
        initImageCache();
    }

    private void initImageCache() {
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        final int cacheSize = maxMemory / 4;
        mImageCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
            }
        };
    }

    public void put(String url, Bitmap bitmap) {
        mImageCache.put(url, bitmap);
    }

    public Bitmap get(String url) {
        return mImageCache.get(url);
    }

}

修改之后ImageLoader代码量变少了,指责也清晰了。当与缓存相关逻辑需要改变时,不需要修改ImageLoader类。图片加载逻辑需要改变时,不需要改变缓存类。

开闭原则

开闭原则Open Close Principle,软件中的对象(类、模块、函数等)应该对于拓展是开放的,但是,对于修改是封闭的。

如果因为需求的调整或其他原因需要修改原有代码,那就很有可能会向原先测试好的旧代码中引入错误,破坏原先系统。所以在需求改变时,应该先考虑使用拓展的方式实现变化,而不是修改已有代码来实现变化。

开闭原则就是,已存在的实现类对于修改是封闭的,新的实现类可以通过继承重写父类应对改变。

需求:引入SD卡缓存

之前实现只有内存缓存,现在需要引入SD卡缓存,将图片缓存到本地。

新增SD卡缓存类DiskCache

public class DiskCache {
    static String cacheDir = "sdcard/cache/"
    
    public Bitmap get (String url) {
        return BitmapFactory.decodeFile(cacheDir + url);
    }
    
    public void put(String url, Bitmap bitmap) {
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(cacheDir + url);
            bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            try {
                fileOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

修改ImageLoader类。增加了DiskCache对象,用一个isUseDiskCache来判断是否使用SD卡缓存。

public class ImageLoader {
    ImageCache mImageCache = new ImageCache();
    DiskCache mDiskCache = new DiskCache();
    boolean isUseDiskCache = false;
    ...
    public void displayImage(final String url, final ImageView imageView) {
        ...
        Bitmap bitmap = isUseDiskCache ? mDiskCache.get(url) : mImageCache.get(url);
        ...
    }
    ...
    public void setUseDiskCache(boolean useDiskCache) {
        isUseDiskCache = useDiskCache;
    }
}

这只是增加了一个sd卡缓存的需求,如果在增加双缓存、用户可自定义缓存这样的需求,ImageLoader中就会出现各种缓存对象,并且displayImage()中还会判断到底使用哪一种方式。自定义缓存还不好实现。

可以发现每一种缓存策略都有共同的get()、put(),只是实现不同,那么就可以把它们抽象出来,抽象成一个ImageCache接口。

public interface ImageCache {
    Bitmap get(String url);
    void put(String url, Bitmap bitmap);
}

再去实现这个接口,创造不同的缓存策略实现类。

内存缓存实现类MemoryCache

public class MemoryCache implements ImageCache {

    LruCache<String, Bitmap> mImageCache;

    public MemoryCache() {
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        final int cacheSize = maxMemory / 4;
        mImageCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
            }
        };
    }

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

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

}

本地缓存实现类DiskCache

public class DiskCache implements ImageCache{
    static String cacheDir = "sdcard/cache/";

    @Override
    public Bitmap get (String url) {
        return BitmapFactory.decodeFile(cacheDir + url);
    }

    @Override
    public void put(String url, Bitmap bitmap) {
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(cacheDir + url);
            bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            try {
                fileOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

双缓存实现类DoubleCache

public class DoubleCache implements ImageCache {
    
    ImageCache mMemoryCache = new MemoryCache();
    ImageCache mDiskCache = new DiskCache();
    
    @Override
    public Bitmap get(String url) {
        Bitmap bitmap = mMemoryCache.get(url);
        if (bitmap == null) {
            bitmap = mDiskCache.get(url);
        }
        return bitmap;
    }

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

这样就可以去修改ImageLoader类了。只需要一个接口对象,使用不同的缓存策略时使用setImageCache()传入不同缓存的实现类。接口统一了缓存和获取缓存的方法,所以这里只用调用抽象方法就可以了。

public class ImageLoader {
    ImageCache mImageCache = new MemoryCache();
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
    Handler mUiHandler = new Handler(Looper.getMainLooper());

    //设置不同缓存对象
    public void setImageCache(ImageCache imageCache) {
        mImageCache = imageCache;
    }

    public void displayImage(final String url, final ImageView imageView) {
        Bitmap bitmap = mImageCache.get(url);
        if (bitmap != null) {//是否可以从缓存中拿到图片
            updateImageView(imageView, bitmap);
            return;
        }
        //调用下载
        submitLoadRequest(url, imageView);

    }
    
    //发起下载请求
    private void submitLoadRequest(final String url, final ImageView imageView) {
        imageView.setTag(url);
        mExecutorService.submit(new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = downloadImage(url);
                if (bitmap == null) {
                    return;
                }
                if (imageView.getTag().equals(url)) {
                    updateImageView(imageView, bitmap);
                }
                mImageCache.put(url, bitmap);
            }
        });
    }

    //设置图片
    private void updateImageView(final ImageView imageView, final Bitmap bitmap) {
        mUiHandler.post(new Runnable() {
            @Override
            public void run() {
                imageView.setImageBitmap(bitmap);
            }
        });

    }

    //下载逻辑
    public Bitmap downloadImage(String imageUrl) {
        Bitmap bitmap = null;
        try {
            URL url = new URL(imageUrl);
            final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            bitmap = BitmapFactory.decodeStream(connection.getInputStream());
            connection.disconnect();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }

}

再来看如何使用

final ImageLoader imageLoader = new ImageLoader();
imageLoader.setImageCache(new DoubleCache());//使用双缓存
imageLoader.setImageCache(new ImageCache() {//使用自定义缓存
    @Override
    public Bitmap get(String url) {
        //具体实现
        return null;
    }

    @Override
    public void put(String url, Bitmap bitmap) {
        //具体实现
    }
});
imageLoader.displayImage(url, imageView);

这样一修改代码,如果之后还需要实现其他缓存策略,就不需要去修改ImageLoader了,现在的ImageLoder非常请。更不需要对原先的ImageCache或者其他缓存实现类做修改,只需要去实现ImageCache这个接口,加入新逻辑,就可以实现新需求了。

这就是开闭原则,哪怕有千变万化的缓存策略,也不需要去改变原有代码,而是实现接口,通过公用的setImageCache()注入ImageLoader。就遵循了尽量通过拓展的方式来实现变化,而不是通过修改已有代码来实现,保证了可拓展性。

里氏替换原则

里氏替换原则Liskov Substitution Principle,定义所有引用基类的地方必须能透明地使用其子类对象。

只要是父类能出现的地方子类都可以出现,而且替换为子类也不会产生任何错误和异常。里氏替换依赖于继承,继承是有两面性的:

优点:

  • 代码重用,减少创建类的成本。
  • 提高代码可拓展性。

缺点:

  • 侵入性,必须拥有父类所有属性和方法。
  • 可能造成子类代码冗余、灵活性降低。

在上述的图片加载代码修改中,其实就存在里氏替换原则,所有ImageCache出现的地方,都可以用MemoryCache、DiskCache、DoubleCache来替换,并且不会有错误。

里氏替换原则就建议通过建立抽象,通过抽象建立规范,具体的实现在运行时替换掉抽象,保证系统的拓展性、灵活性。里氏替换原则保证了拓展性,从而达到开闭原则的对拓展开放、对修改关闭的效果,两个原则都强调面向对象的一个重要特性——抽象。

依赖倒置原则

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

关键点:

  • 高层模块不用依赖底层模块,两者都应该依赖于抽象。
  • 抽象不应该依赖于细节。
  • 细节应该依赖抽象。

解释:

  • 高层模块:调用端
  • 低层模块:具体实现类
  • 抽象:接口或抽象类
  • 细节:实现类

如果类与类直接依赖于细节,那么当需求变化时,就意味着两个类都需要修改。就像最初的ImageLoader,ImageLoader直接依赖于ImageCache这个实现内存缓存的细节类,当需求增加需要实现既内存缓存又本地缓存时,就需要同时修改两个类了。而修改之后,ImageCache变为缓存的抽象接口,ImageLoader依赖于抽象的ImageCache,保证了依赖倒置原则,对应来看就是:

  • ImageLoader和Cache实现类不直接依赖,而是两者都依赖于抽象的ImageCache接口。
  • ImageCache不依赖于MemoryCache、DiskCache、DoubleCache。
  • MemoryCache、DiskCache、DoubleCache依赖于ImageCache。

接口隔离原则

接口隔离原则Interface Segregation Princeple,类间的依赖关系应该建立在最小的接口上。

接口隔离原则将庞大的接口拆分成更小的更具体的接口,客户端只需要知道他们感兴趣的方法。接口隔离原则的目的是使系统解耦,从而更容易重构、更改和重新部署。

在DiskCache类中的put方法:

@Override
    public void put(String url, Bitmap bitmap) {
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(cacheDir + url);
            bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            try {
                fileOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

fileOuputStream在使用完后需要调用close()关闭资源,但是调用这个close()就必须嵌套一个try catch,降低了代码的可读性。

事实上close()方法属于Closeable接口的,很多类都实现了这个接口,FileOutputStream就属于其中之一。那么我们完全可以利用这个接口写一个工具类。

public class CloseUtil {
    private CloseUtil() {}
    public static void closeQuietly(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

这时,我们的put()就可以变为如下。

@Override
    public void put(String url, Bitmap bitmap) {
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(cacheDir + url);
            bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            CloseUtil.closeQuietly(fileOutputStream);
        }
    }

直接调用CloseUtil的closeQuietly()就可以了。

在使用过程中,fileOutputStream替换掉了closeQuietly()参数的Closeable接口,closeQuietly()依赖于抽象的Closeable接口而不是具体的FileOutputStream,也是遵循了依赖倒置原则。

CloseUtil的的closeQuietly()设计的最初目的是为了关闭fileOutputStream,它的关注点并不在FileOutputStream的整个设计上,而是只关注可关闭这一特点,也就是close()这一个方法。Closeable接口将流的关闭这一方法抽象出来,使得closeQuietly()只需要依赖Closeable这一个接口,而不需要去依赖整个FileOutputStream,从而降低了接口的使用难度,这就是接口隔离原则,使一个类依赖的接口尽可能的小。

回想前面ImageLoader的设计,它只需要知道缓存对象有可存可取的接口就可以了,其他的实现都是对ImageLoader隐藏的。

接口隔离原则用最小化接口隔离了实现类的细节,促使我们将庞大的接口拆分到更细粒度的接口当中去,使我们的系统具有更低的耦合性、更高的灵活性。

最少知识原则

最少知识原则Least Knowledge Principle,一个对象应该对其他对象有最少的了解。

一个类应该对自己需要耦合或调用的类知道得最少,类内部如何实现与调用者或者依赖者没关系,调用者或者依赖着只需要知道它需要的方法即可。类鱼类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。

想要实现租客通过中介那里租房的逻辑,设计以下三个类。

public class Room {
    private float area;
    private float price;

    public Room(float area, float price) {
        this.area = area;
        this.price = price;
    }

    public float getArea() {
        return area;
    }

    public float getPrice() {
        return price;
    }
    
    @Override
    public String toString() {
        return "Room{" +
                "area=" + area +
                ", price=" + price +
                '}';
    }
    
}
public class Mediator {
    private List<Room> mRooms = new ArrayList<>();

    public Mediator() {
        for (int i = 0; i < 5; i++) {
            mRooms.add(new Room(20 + i, (20 + i) * 200));
        }
    }

    public List<Room> getRooms() {
        return mRooms;
    }

}
public class Tenant {
    private static final String TAG = "Tenant";
    public void rentRoom(float area, float price, Mediator mediator) {
        List<Room> rooms = mediator.getRooms();
        for (Room room : rooms) {
            if (isSuitable(area, price, room)) {
                Log.d(TAG, "rentRoom: " + room.toString());
                break;
            }
        }
    }

    private boolean isSuitable(float area, float price, Room room) {
        return room.getArea() >= area && room.getPrice() <= price;
    }

}

可以看到现在的设计中,三个类两两之间都存在依赖关系,Mediator依赖Room,Tenant依赖Mediator和Room,这样一来,如果需要改变Room类,Mediator和Tenant也都要跟着改变。
但是按照现实逻辑来说,租客是不需要依赖Room的,他只需要将自己的需求标准给中介就可以了,并不需要依赖Room并知道其中的成员细节。于是可以将代码改成这样。

public class Mediator {
    private List<Room> mRooms = new ArrayList<>();

    public Mediator() {
        for (int i = 0; i < 5; i++) {
            mRooms.add(new Room(20 + i, (20 + i) * 200));
        }
    }
    
    private boolean isSuitable(float area, float price, Room room) {
        return room.getArea() >= area && room.getPrice() <= price;
    }

    public Room rentOut(float area, float price) {
        for (Room room : mRooms) {
            if (isSuitable(area, price, room)) {
                return room;
            }
        }
        return null;
    }
    
}
public class Tenant {
    private static final String TAG = "Tenant";
    public void rentRoom(float area, float price, Mediator mediator) {
        Log.d(TAG, "rentRoom: " + mediator.rentOut(area, price).toString());
    }

}

将筛选房的逻辑交给了中介,租客只需要向中介传达标准就可以了,并不依赖Room的内部成员,将Tenant和Room解耦了。

在ImageLoader示例中,用户并不会知道MemoryCache的内部实现,不知道内部使用了LruCache算法,只知道有ImageCache中的get()、put(),这样的话如果我们想要将LruCache替换为别的去实现内存缓存,用户也不会感觉到,和缓存相关的其他代码也不需要去改变。

最少知识原则让类暴露最少的内容给其他类,使得系统具有更低的耦合性和更好的扩展性。

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

推荐阅读更多精彩内容

  • 原文链接:Explaining UX Design To Your Team 原文作者:Rosie Allabar...
    ShusQ阅读 681评论 0 1
  • 作者简介 内容介绍 前言 第1章 超级的有机体 1.1 夜游城市生命馆 1.2 社会性昆虫的神奇世界 1.3 听听...
    杨友三阅读 280评论 0 1
  • 大盆友:妈妈,你为什么天天都让我学习,学习到底有什么好?! 我:不学习你想和妈妈一样为了生计奔波吗? 大盆友:那你...
    周舟_2983阅读 200评论 0 0
  • ​初学者去健身房的经验总结 如果这是你第一次去健身房,那么这篇文章必须要读一读,这里总结了第一次去健身房你应该了解...
    轻椒女子健身阅读 793评论 1 4
  • 三月,你从烟雨里来,带着一身暮春的微凉。 三月,我打烟雨里走过,伞下的天空,看不见你清瘦的容颜。 三月的烟雨里,我...
    夜里飞行的猫阅读 813评论 0 2