面向对象六大设计原则

作者: 慕涵盛华 | 来源:发表于2017-11-07 13:54 被阅读42次
    目录

    最新在阅读《Android源码设计模式解析与实战》一书,我觉得写的很清晰,每一个知识点都有示例,通过示例更加容易理解。书中的知识点有些都接触过,有的没有接触过,总之,通过阅读这本书来梳理一下知识点,可能有些东西在项目中一直在使用,然并不能笼统,清理的说明理解它。本文主要是记录阅读这本书的知识点和自己的一些理解。一来整理知识点,二来方便以后查看,快速定位。

    单一职责原则 :优化代码第一步

    单一职责原则(英文简称:SRP):对于一个类而言,应该仅有一个引起它变化的原因。这个有点抽象,因为该原则的划分界面并不是那么清晰,很多时候靠个人经验来区分。简单来说就是一个类只负责一个功能,比如加减乘除应分别对应一个类,而不是把四个功能放在一个类中,这样在只要有一个功能变化都需要更改这个类。

    下面以实现一个图片加载器(ImageLoader)来说明:

    public class ImageLoader {
    
        //图片内存缓存
        private LruCache<String,Bitmap> mImageCache;
        //线程池,线程数量为CPU的数量
        private ExecutorService mExecutorService = Executors.newFixedThreadPool(
                Runtime.getRuntime().availableProcessors()
        );
    
        public ImageLoader(){
            initImageLoader();
        }
    
        //初始化
        private void initImageLoader() {
            //计算最大的可使用内存
            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 displayImage(final String url, final ImageView imageView){
            Bitmap bitmap = mImageCache.get(url);
            if(bitmap != null){
                imageView.setImageBitmap(bitmap);
                return;
            }
            imageView.setTag(url);
            mExecutorService.submit(new Runnable() {
                @Override
                public void run() {
                    Bitmap bitmap = downloadImage(url);
                    if(bitmap == null){
                        return;
                    }
                    if (imageView.getTag().equals(url)) {
                        imageView.setImageBitmap(bitmap);
                    }
                    mImageCache.put(url,bitmap);
                }
            });
    
        }
    }
    

    我们一般都会这样这样简单的实现一个图片加载工具类,这样写功能虽然实现了,但是代码是有问题的,代码耦合严重,随着ImageLoader功能越来越多,这个类会越来越大,代码越来越复杂。按照单一职责原则,我们应该把ImageLoader拆分一下,把各个功能独立出来。

    ImageLoader修改代码如下:

    public class ImageLoader {
        //图片缓存
        ImageCache mImageCache = new ImageCache();
        //线程池,线程数量为CPU的数量
        private ExecutorService mExecutorService = Executors.newFixedThreadPool(
                Runtime.getRuntime().availableProcessors()
        );
        
    
        public void displayImage(final String url, final ImageView imageView){
             .............
        }
    }
    
    public class ImageCache {
    
        //图片内存缓存
        private 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 Bitmap get(String url){
            return mImageCache.get(url);
        }
    
        public void put(String url,Bitmap bitmap){
            mImageCache.put(url,bitmap);
        }
    }
    

    上述代码将ImageLoader一分为二,ImageLoader只负责图片加载的逻辑,ImageCache负责缓存策略,这样,ImageLoader的代码变少了,职责也清晰了,并且如果缓存策略改变了的话,只需要修改ImageCache而不需要在修改ImageLoader了。从这个例子可以更加清晰的理解什么是单一职责原则,如何去划分一个类的职责,每个人的看法不同,这需要根据个人的经验,业务逻辑而定。

    开闭原则:让程序更稳定,更灵活

    开闭原则(英文缩写为OCP): 软件中的对象(类,函数,模块等)对于扩展是开放的,但是对于修改是封闭的。在软件的生命周期内,因为变化,升级和维护的原因需要对原代码修改时,可能会将错误引入已经经过测试的旧代码,破坏原有的系统。因为当需求变化时,我们应尽可能的通过扩展来实现,而不是修改原来的
    代码。

    在实际的开发过程中,只通过继承的方式来升级,维护原有的系统只是一个理想化的状态,修改原代码,扩展代码往往是同时存在的。我们应尽可能的影响原代码。避免引入的错误造成系统的破坏。

    还是上面的那个例子,虽然通过内存缓存解决了每次都从网络下载图片的问题,但是Android内存有限,并且当应用重启后内存缓存会丢失。我们需要修改一下,增加SD卡缓存,代码如下:

    public class ImageLoader {
    
        //内存缓存
        ImageCache mImageCache = new ImageCache();
        //SD卡缓存
        DiskCache mDiskCache = new DiskCache();
        //线程池,线程数量为CPU的数量
        private ExecutorService mExecutorService = Executors.newFixedThreadPool(
                Runtime.getRuntime().availableProcessors()
        );
    
        public void displayImage(final String url, final ImageView imageView){
            //先从内存缓存中读取,如果没有再从SD卡中读取
            Bitmap bitmap = mImageCache.get(url);
            if(bitmap == null){
                bitmap = mDiskCache.get(url);
            }
            if(bitmap != null){
                imageView.setImageBitmap(bitmap);
                return;
            }
          //从网络下载图片
           ..........
        }
    }
    
    public class DiskCache {
        private final 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 {
                if(fileOutputStream != null){
                    try {
                        fileOutputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    

    上述代码我们增加了SD卡缓存,我们在显示图片的时候先判断内存缓存中是否存在如果不存在就在SD卡中找,否则再从网络下载,这样就会有一个问题,每增加一个新的缓存方法,我们都需要修改原来的代码,这样可能引入Bug,而且会使原来的代码越来越复杂,还有用户也不能自定义缓存方法。我们具体使用哪一种缓存方法是通过if条件判断的,条件太多,是很容易写错的。而且代码会越来越臃肿,并且可扩展性差。可扩展性是框架的重要特性之一。

    根据开闭原则,当软件需求改变的时候,我们应该通过扩展的方式实现,而不是修改自己的代码。对上述代码进行优化:

    public class ImageLoader {
    
        //默认缓存方式为内存缓存
        ImageCache mImageCache = new MemoryCache();
    
        //线程池,线程数量为CPU的数量
        private ExecutorService mExecutorService = Executors.newFixedThreadPool(
                Runtime.getRuntime().availableProcessors()
        );
       
        //设置缓存方式
        public void setImageCache(ImageCache cache){
            mImageCache = cache;
        }
    
        public void displayImage(final String url, final ImageView imageView){
            //先从缓存中读取
            Bitmap bitmap = mImageCache.get(url);
            if(bitmap != null){
                imageView.setImageBitmap(bitmap);
                return;
            }
         //网络下载图片
          ............
        }
    }
    
    public interface ImageCache {
        Bitmap get(String url);
        void put(String url, Bitmap bitmap);
    }
    

    通过上述代码我们可以看出,ImageLoader增加了一个方法setImageCache,我们可以通过该方法设置缓存方式,这就是我们常说的依赖注入。当然我们还可以自定义自己的缓存方式,只需要实现ImageCache这个接口即可。然后再调用setImageCache这个方法来设置。而不需要修改ImageLoader的代码。这样当缓存需求改变的时候我们可以通过扩展的方式来实现而不是修改的方法,这就是所说的开闭原则。同时是ImageLoader的代码更简洁,扩展性和灵活性也更高。

    里氏替换原则:构建扩展性更好的系统

    里氏替换原则(英文缩写为LSP):所有引用基类的地方都必须能够透明的使用其子类的对象。我们知道面向对象有三大特性:封装,继承和多态,里氏替换原则就是依赖继承和多态这两大原则,里氏替换原则简单来说就是:只要是父类引用出现的地方都可以替换成其子类的对象,并且不会产生任何的错误和异常。

    下面以Android中的Window和View的关系的例子来理解里氏替换原则:

    //窗口类
    public class Window {
        public void show(View child){
            child.draw();
        }
    }
    

    建立视图抽象类,测量视图的宽高为公共代码,绘制交给具体的子类去实现

    public abstract class View {
        public abstract void draw();
        public void measure(int width,int height){
            //测量视图大小
            ...........
        }
    }
    
    
    //文本类具体实现
    public class TextView extends View {
        @Override
        public void draw() {
            //绘制文本
            ...........
        }
    }
    
    //按钮类具体实现
    public class Button extends View {
        @Override
        public void draw() {
            //绘制按钮
            .............
        }
    }
    

    上述示例中,Window依赖于View,View定义了一个视图抽象,measure是各个子类共享的方法,子类通过重写View的draw方法来实现具体各自特色的内容。任何继承View的子类都可以设置给show方法,这就是所说的里氏替换原则,通过里式替换,就可以自定义各种各样的,千变万化的View,然后传递给Window,Window负责组织View,并将View显示到屏幕上。

    上面ImageLoader的例子也体现了里氏替换原则,可以通过setImageCache方法来设置各种各样的缓存方式,如果 setImageCache中的cache对象不能被子类替换,那么又怎么能设置各种各样的缓存方式呢?

    依赖倒置原则:让项目拥有变化的能力

    依赖倒置原则(英文缩写为DIP)指代了一种特定的解耦方式,使得高层次的模块不依赖于低层次模块的实现细节的目的,依赖模块被颠倒了。这个概念更加的抽象,该怎么理解呢?

    依赖倒置原则有几个关键的点:

    • 1.高层模块不应该依赖底层模块,两者都应该依赖其抽象。
    • 2.抽象不应该依赖细节。
    • 3.细节应该依赖抽象。

    在Java语言中,抽象就是接口或者抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或者继承抽象类而产生的类就是细节,可以直接实例化;高层模块就是调用端;底层模块就是实现端。

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

    如果类与类之间直接依赖于细节,那么它们之间就有直接的耦合,当需求变化的时候,意味着要同时修改依赖者的代码。这就限制了系统的可扩展性。

    public class ImageLoader {
        //直接依赖于细节
        MemoryCache mImageCache = new MemoryCache();
        ...................
    }
    

    ImageLoader直接依赖于MemoryCache,MemoryCache是一个具体的实现,这就导致ImageLoader直接依赖于细节,当MemoryCache不能满足而被其他缓存实现替换时,就必须需要修改ImageLoader的代码。

    public interface ImageCache {
        Bitmap get(String url);
        void put(String url, Bitmap bitmap);
    }
    
    public class ImageLoader {
        //依赖于抽象,并且有一个默认的实现
        ImageCache mImageCache = new MemoryCache();
        ......................
    

    在这里我们建立了ImageCache抽象,并且让ImageLoader直接依赖于抽象而不是具体的细节,当需求变化时,只需要实现ImageCache或者继承已有的类来完成相应的缓存功能。然后再将具体的实现注入到ImageLoader中,保证了系统的高扩展性。这就是依赖倒置原则。

    接口隔离原则:让系统拥有更高的灵活性

    接口隔离原则(英文缩写为LSP):客户端不应该依赖它不需要的接口。另外一种定义是:类间的依赖关系应该建立在最小的接口上。接口隔离原则将庞大,臃肿的接口拆分成更小更具体的接口,这样客户端只需要知道它感兴趣的方法。接口隔离的目的是解开耦合,从而容易重构更改和重新部署。

    接口隔离原则说白了就是让依赖的接口尽可能的小,看一下上个例子实现SD卡缓存的代码:

     /* 将图片添加到缓存中 */
     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 {
             if(fileOutputStream != null){
                 try {
                     fileOutputStream.close();
                 } catch (IOException e) {
                     e.printStackTrace();
                 }
             }
         }
      }
    

    我们看到这段代码的可读性非常的差,各种try...catch都是非常简单的代码,但是会严重影响代码的可读性,并且多层级的大括号很容易将代码写到错误的层级中。那么如何解决这样的问题呢?Java中有一个Closeable接口,该接口标识了一个可关闭的对象,它只有一个close方法。实现该接口的类有很多,FileOutputStream也实现了该接口,当程序有有多个可关闭的对象时,如果都像上述代码那样在finally中去关闭,就非常的麻烦了。

    我们可以抽取一个工具类来专门去关闭需要关闭的对象。

    public class CloseUtils {
    
        /**
         * 关闭Closeable对象
         * @param closeable
         */
        public static void closeQuietly(Closeable closeable){
            if(null != closeable){
                try {
                    closeable.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    使用工具类替换上述的代码

    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 {
            CloseUtils.closeQuietly(fileOutputStream);
        }
    }
    

    这样代码就简洁多了,并且CloseUtils可以用到多个可以关闭对象的地方,保证了代码的重用性,它依赖于Closeable抽象而不是具体的实现,并且建立在最小的依赖原则之上,他只需要知道对象是否可关闭,其他的一概不关心,这就是接口隔离。如果现在只需要关闭一个对象时,它却暴露了其他的接口方法,比如OutputStream的write方法,这使得更多的细节暴露在客户端面前,还增加了使用难度。而通过Closeable接口将可关闭的对象抽象起来,这样客户端只需要依赖Closeable就可将其他的细节隐藏起来,客户端只需要知道这个对象可关闭即可。

    在上述的ImageLoader中,只需要知道该缓存对象有读取和缓存的接口即可,其他的一概不管,这样缓存的具体实现是对ImageLoader隐藏的。这就是用最小化接口隔离了实现类的细节。

    Robert C Martin在21世纪早期将单一职责,开闭原则,里氏替换,接口隔离和依赖倒置5个原则定义为SOLID原则,作为面向对象编程的5个基本原则。当这些原则在一起使用时,它使得一个软件系统更清晰,更简单,最大程度的拥抱变化。

    迪米特原则:更好的可扩展性

    迪米特原则(英文缩写为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 {
        private List<Room> mRooms = new ArrayList<>();
    
        public Mediator(){
    
            for (int i = 0; i < 5; i++) {
                mRooms.add(new Room(14 + i,(14 + i) * 150));
            }
        }
    
        public List<Room> getRooms(){
            return mRooms;
        }
    }
    
    public class Tenant {
        private float roomArea;
        private float roomPrice;
        private static final float diffArea = 0.0001f;
        private static final float diffPrice = 100.0001f;
    
        public void rentRoom(Mediator mediator){
            List<Room> rooms = mediator.getRooms();
            for (Room room : rooms) {
                if(isSuitable(room)){
                    System.out.print("租到房子了" + room.toString());
                    break;
                }
            }
        }
    
        private boolean isSuitable(Room room){
            return Math.abs(room.price - roomPrice) < diffPrice
                    && Math.abs(room.area - roomArea) < diffArea;
        }
    }
    

    从上面的代码看出,Tenant不仅依赖Mediator,还需要频繁的与Room打交道,租户类只需要通过中介找到一间符合要求的房子即可。如果把这些检索都放在Tenant中,就弱化了中介的作用,而且导致TenantRoom耦合度较高。当Room变化的时候,Tenant也必须跟着变化,而且Tenant还和Mediator耦合,这样关系就显得有些混乱了。

    我们需要根据迪米特原则进行解耦。

    public class Mediator {
        private List<Room> mRooms = new ArrayList<>();
    
        public Mediator(){
    
            for (int i = 0; i < 5; i++) {
                mRooms.add(new Room(14 + i,(14 + i) * 150));
            }
        }
    
        public Room rentOut(float price,float area){
            for (Room room : mRooms) {
                if (isSuitable(price,area,room)) {
                    return room;
                }
            }
            return null;
        }
    
        private boolean isSuitable(float price,float area,Room room){
            return Math.abs(room.price - price) < Tenant.diffPrice
                    && Math.abs(room.area - area) < Tenant.diffArea;
        }
    }
    
    public class Tenant {
        
        private float roomArea;
        private float roomPrice;
        public static final float diffArea = 0.0001f;
        public static final float diffPrice = 100.0001f;
    
        public void rentRoom(Mediator mediator) {
            Room room = mediator.rentOut(roomPrice, roomArea);
            if(null != room){
                System.out.print("租到房子了" + room.toString());
            }
        }
    
    }
    

    我们将对Room的操作移到了Mediator中,这本来就是Mediator的职责,根据租户的条件检索符合的房子,并且将房子返回给用户即可。这样租户就不需要知道有关Room的细节,比如和房东签合同,房产证的真伪等。只需要关注和我们相关的即可。

    总结

    在应用开发过程中,我们不仅要完成应用的开发工作,还需要在后续的升级,维护中让应用系统能够拥抱变化。拥抱变化意味着在满足需求且不破坏系统稳定的前提下保持高扩展性,高内聚,低耦合,在经历了各个版本变更之后依然保持清晰,灵活,稳定的系统架构。那么遵守面向对象的六大原则是我们迈向的第一步。

    关注微信公众号获取更多相关资源

    Android小先生

    相关文章

      网友评论

        本文标题:面向对象六大设计原则

        本文链接:https://www.haomeiwen.com/subject/gwplmxtx.html