设计模式中的7种设计原则

2021年9月17日 9点热度 0条评论 来源: pengboboer

前言

最近准备好好回顾一下设计模式的知识,提高一下自己的代码质量,然后做个笔记。记录一下。

1、定义

代码设计的经验总结

2、作用

  • 减少重复工作、提高代码复用率
  • 提高代码的可维护性、可扩展性
  • 代码更加优雅、更容易使人理解

3、高内聚低耦合

  • 内聚性:模块内各个元素之间相互结合的紧密程度的度量,内聚性越强,独立性越高。
  • 耦合性:模块间关联程度的度量,也就是说模块之间的各种关系,耦合性越强,独立性越差。通俗的来讲,你我都是当官的,咱俩之间尽量少有太密切的联系,不然谁出事了(模块被修改),另一个也会被连累。

4、类之间的关系:

  • 关联关系:其实就是咱俩有关系,比如说:一个Activity中声明了一个TextView,那么他俩指定是关联关系咯。关联关系还分好多种:双向关联、单向关联、自关联、多重性关联、聚合关系、组合关系。
  • 聚合关系:汽车和发动机的关系,发动机是汽车的一部分,但是我也可以独自存在,UML图中用空心棱形来表示,一般通过构造方法或者set方法传参的方法注入到整体对象中。
  • 组合关系:头和嘴的关系,头没了,嘴也就没了。整体对象控制着成员对象的生命周期,UML图中用实心棱形来表示,一般直接通过在构造方法中new一个实例来注入到整体对象中。
  • 依赖关系:是一种使用关系,一般通过方法中的参数传入另一个类的对象,比如说Driver类和Car类,void drive(Car car),驾驶依赖汽车,那么Driver类就依赖Car类,在UML中是依赖类用一个虚线的箭头指向被依赖的类。
  • 泛化关系:那么就是一个继承的关系,很好理解,在UML中子类用一个空心三角形来指向父类。
  • 实现关系:那么就是一个实现接口的关系,也很好理解,在UML中实现接口的类用一个虚线的空心三角形来指向接口。

5、七大设计原则

  • 单一职责:一个类只负责一个功能中的相应职责,这个类应该只有一个引起它变化的原因。通俗的来讲,一个老师最好只教一种科目,不要又教语文又教数学又教英语(一个类不能太“累”)。一个修理工,如果把各种工具,扳子,改锥,钳子等等工具都带在身上,是不是太累了(一个类不能太“累”),那么如果想要更换扳子,钳子的型号,那么还得从身上都拿下来换一下(拥有不止一个让他变化的原因),那么其他的修理工也想用扳子和钳子,但是又不能复用,只能自己再买一个新的带身上,结果每个修理工身上都带有一套工具(重复代码),如果每个修理工都用这些工具,那么没必要每人都带工具,把工具放到一个工具箱(拆分成单一的类),谁用的时候取一下就可以了(代码复用)。
  • 开闭原则:一个实体(类、函数、模块)等,应该对外扩展开放,对内修改关闭。也就是说在添加新功能时,不需要去修改以前的代码,直接加功能就行。通俗的来讲,一座城市,肯定是随着时间的推移而发展的,那么我要在这座城市盖个游乐场(扩展),不应该对体育场进行整改(修改),你盖就盖你的呗,去野地去盖,不要去修改其他已经存在的东西。(对外扩展开放,对内修改关闭)。
  • 里氏代换原则:子类必须替换掉它们的父类型,将一个基类对象替换成一个子类对象,程序没有任何的异常。通俗的来讲,我喜欢动物,我一定喜欢狗;我喜欢狗并不代表我喜欢所有的动物。通常我们用父类的对象来进行定义,再在运行的时候再用子类对象进行替换,比如说:List<String> list = new ArrayList<>();包括我们在开发当中定义一个BaseActivity,我们在基类中写好基本的方法,那么添加一个新的Activity新的功能只需要继承BaseActivity即可。
  • 依赖倒转原则:细节应该依赖于抽象,而抽象不应该依赖于细节。其实还是说了一个抽象类的事情,举个例子来说,小明阅读漫画,需要在read方法参数中传入漫画,那么小明又想阅读小说,那么没必要再创建一个阅读小说的方法,让漫画和小说都继承书籍,然后read方法的参数是这个父类,那么无论我们传入的是漫画还是小说,都能实现阅读。
  • 接口隔离原则:使用多个专门功能的接口,而不是使用单一的总接口。其实这个是很好理解的,得符合单一职责嘛,如果接口中方法太多,那么我如果有需要实现这个接口中的某几个方法,就必须实现接口中所有的方法,会出现大量的空方法。
  • 合成复用原则:尽量使用组合,而不是继承。比如说小明要玩球,不应该继承球类的play方法,而是去调用它,而且应该是还有好多种球类,比如篮球,足球,排球等去继承球类。合成复用原则其实我们在大部分编程过程中都会遵守的。
  • 迪米特原则:一个模块或对象应尽量少的和其他实体类发生相互作用,使得模块相对独立,降低耦合性。通俗的来说,我找小明借钱,小明的钱是找小红借的,那么这个借钱的事儿啊,我只接触我的朋友小明,小明和小红怎样,我不管,跟我没关系,到时候出什么事情了,我也只需要跟小明解决,而不是我们三个人都有关系。

总结

对一些定义大体做了一下介绍,加深了一下记忆,包括七大设计原则也是很费劲的想一些生活中的例子来理解,可能有的地方的比喻不是那么的形象,但是我觉得还是能加深理解,这些基本的定义在以后的学习中通过应用实践我们再逐步的更深入的去理解。

 

分割线----------------------------

借用Android源码设计模式解析与实战一书中的内容,再理解一下这几个原则

单一职责

实现图片加载,并且将图片缓存起来:

public class ImageLoader {
    LruCache<String, Bitmap> imageCache;
    ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
    
    public ImageLoader() {
        initImageCache();
    }

    private void initImageCache() {
        // todo 对imageCache进行初始化操作
    }
    
    public void displayName(final String url, final ImageView imageView) {
        // todo 如果有缓存先取缓存 return
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = downloadImage((url));
                // todo imageView显示图片
                // todo 将图片加入缓存
            }
        });
    }
    
    public Bitmap downloadImage(String imageUrl) {
        Bitmap bitmap = null;
        // todo 通过网络下载bitmap
        return bitmap;
    }
}

我感觉大部分人简单考虑后,都能写出如上的代码,功能是实现了,没问题,但是设计上是有毛病的,整个ImageLoader耦合太严重了,没法扩展,ImageLoader不光做了初始化LruCache的工作,还要去做加载图片的工作,也不满足单一职责。就像代码整洁之道讲的,如果有些函数想要共享某些变量,那么可以让这几个函数拥有一个自己的类,换句话说,我们应该去创建一个类去包含这几个函数

那么就有了重构后的新版本:

public class ImageLoader {
    ImageCache cache = new ImageCache();
    ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    public void displayName(final String url, final ImageView imageView) {
        // todo 如果有缓存先取缓存 return
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = downloadImage((url));
                // todo imageView显示图片
                // todo 将图片加入缓存
            }
        });
    }

    public Bitmap downloadImage(String imageUrl) {
        Bitmap bitmap = null;
        // todo 通过网络下载bitmap
        return bitmap;
    }
}



public class ImageCache {
    LruCache<String, Bitmap> cache;

    public ImageCache() {
        initImageCache();
    }

    private void initImageCache() {
        // todo 对imageCache进行初始化操作
    }

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

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

这样重构后好像有些清晰了,我们将LruCache单独抽出为一个新的类,在新类中进行初始化工作。我们要明白,一个类中应该是一组相关性很高的数据和函数的一个封装。其实从这个例子我们可以收获很多,作为一个Android开发者来说,图片加载更贴近于我们的工作,更像是真正的实战,所以我们会对这个代码的关注度也很高。如果只是从实现功能上来说的话,我们很容易写出耦合很重的代码,如何考虑去优化代码结构,是我们需要不断的去探索,去思考的

开闭原则

我们需求改动了,现在需要添加sd卡缓存和双缓存:

public class ImageLoader {
    ImageCache imageCache = new ImageCache();
    DiskCache diskCache = new DiskCache();
    DoubleCache doubleCache = new DoubleCache();
    boolean isUseDiskCache;
    boolean isUseDoubleCache;
    ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    public void setUseDiskCache(boolean useDiskCache) {
        isUseDiskCache = useDiskCache;
    }

    public void setUseDoubleCache(boolean useDoubleCache) {
        isUseDoubleCache = useDoubleCache;
    }

    public void displayName(final String url, final ImageView imageView) {
        if (isUseDoubleCache) {
            // todo 取 doubleCache获取的bitmap
        } else if (isUseDiskCache) {
            // todo 取 diskCache获取的bitmap
        } else {
            // todo 取 imageCache获取的bitmap
        }
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = downloadImage((url));
                // todo imageView显示图片
                // todo 将图片加入缓存
            }
        });
    }

    public Bitmap downloadImage(String imageUrl) {
        Bitmap bitmap = null;
        // todo 通过网络下载bitmap
        return bitmap;
    }
}

上面的代码,很大概率是会被我们写出来的,每多加一种类型,我们出于本能,会下意识的添加boolean变量,添加if/else语句,虽然我们可以让代码去正常的运作,但是让整个代码变的凌乱不堪,使看到它的人感到疯狂,改动它的人感到恶心。我们一旦想要去扩展,只能不断的去修改ImageLoader类,然后可能会带来新的bug。真的是太糟糕了,根本就没有遵循开闭原则

我们应该去重构一下:

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

class MemoryCache implements ImageCache {
    ... 
}

class DiskCache implements ImageCache {
    ...
}

class DoubleCache implements ImageCache {
    ...
}


public class ImageLoader {
    ImageCache cache;
    ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    public void setImageCache(ImageCache cache) {
        this.cache = cache;
    }

    public void displayName(final String url, final ImageView imageView) {
        Bitmap bitmap = cache.get(url);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
            return;
        }
        requestUrl(url, imageView);
    }

    private void requestUrl(final String url, ImageView imageView) {
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = downloadImage((url));
                // todo imageView显示图片
                // todo 将图片加入缓存
            }
        });
    }

    private Bitmap downloadImage(String imageUrl) {
        Bitmap bitmap = null;
        // todo 通过网络下载bitmap
        return bitmap;
    }
}

我们通过ImageCache接口去实现了这三种缓存,然后通过setImageCache去注入,即使我们再去扩展新的缓存,只需要去实现ImageCache接口即可,这不就是我们要遵循的开闭原则吗?

其实这个图片加载类只是一个个例,我们可能看过很多设计模式的书,看到过无数次提到开闭原则的地方,但是每次都是知道这么回事,却不知道如何应用,其实结合我的经验,我可以很轻松的说几个实战的例子:

现在需要实现视频、图文、文档、作业、考题五个列表,列表内容相同,功能也几乎类似,而且将来扩展的功能大概率还是相同的。我们有什么办法去实现呢?

  1. 你可以选择去写5个Activity,复制相同代码过去,只是请求数据接口名可能不一样,topbar的标题栏名字不一样。某一天,需求改了,需要在列表上面加一个筛选功能,那么你再去挨个修改这5个activity,添加5次筛选功能。某一天这个列表页又加了某个功能,我们再去改,然后。。。
  2. 我们可以选择去写成一个activity,然后标题栏的内容,请求数据的接口名,使用if/else来判断。尤其是当时的需求只有两种类型的时候,我们很多时候会为了图方便,用boolean类型的变量,if/else就来处理了,再加一种类型,那就再来一个else语句吧,一旦需求增加,代码就会比较丑陋。而且最有可能出现的是,比如视频和图文界面某一个控件要显示,其他三个不显示;视频和考题有某个功能,而其他三个没有。这样代码中的各个地方可能不全是5个if/else,可能这个地方是5个,另一个地方是3个,而且代码中还充斥着某些类似if (type == xx1 || type == xx2)的判断,各种混乱的逻辑交错在一起,可能稍微修改某个地方,就牵动了另一个地方,随着功能越来越复杂,当你再打开这个Activity的时候,除了感觉到恶心就是恶心。
  3. 其实最好的选择就是使用多态,把这些差别封装到一个类中,比如我视频类中的变量title就是视频,requestListUrl就是视频列表的接口地址,isShowFilterView就是要不要显示筛选条,我们通过继承能将各种复杂的东西独立化,封装到各自的类中。某次加一个需求,把视频列表的某个地方改一下,我们完全不需要动图文、考题等类的代码。再新增一个类型,我们也显得游刃有余,只要拿到新类型的列表接口,我想我们在很短的时间内,就可以又完成一个xxx列表的界面。
  • 在平时的开发中,我们经常会遇到这种有关多态的问题,但是往往我们都用if/else来处理了。再比如说详情页的来源问题:有可能详情页是从主页列表跳转过来的,也有可能是从个人中心的“我的xxx”列表跳转过来,还有可能是从评论通知当中的一个查看详情按钮跳转而来,又或是从多个不同的地方跳转而来,一开始可能只有一个差别,可能就是标题不同而已,但是随着需求的不断变化,可能会有几处甚至十几处不同。很多人写代码,大部分都会先从两个类别的if/else开始写起,不知不觉的变成了3个4个if/else,然后十几处都得写这样3到4个if/else,到最后修改的时候真的是非常的痛苦。

在写代码的过程中,我觉得我们会逐渐的加深对开闭原则的理解

里氏替换原则

其实上面例子中的setImageCache()方法,ImageCache作为基类,而MemoryCache、DiskCache、DoubleCache作为子类,它们之间的关系,已经说明了这一点,我在这里就不在多阐述了。只要记住,将一个基类对象替换成一个子类对象,程序应该没有任何的异常

依赖倒置原则

总体来说就是:模块间的依赖通过抽象产生,实现类之间不发生依赖关系,其依赖关系是通过接口或者抽象类产生的。

具体的体现也可以用上面的例子来说明,如果ImageLoader直接依赖于MemoryCache这个具体的实现类,就相当于依赖了细节,当我们需要添加新的缓存类的时候,就不得不去修改ImageLoader类。所以ImageLoader应该去依赖于ImageCache这个抽象类。同理,上面我提到的列表Activity应该依赖于视频、图文、文档、作业、考题等的基类。其实对于第三方API的打包,也是可用这个原则来理解的,比如之前的项目使用Glide框架,我们的业务代码依赖的是打包的后的ImageUtils,ImageUtils中的方法是用Glide来实现,而不是直接去依赖Glide。我们不应该让Glide过分的去侵入业务代码

接口隔离原则

说白了就是让客户端依赖的接口尽可能的小,这个不多说了,应该是很好理解的

迪米特原则

这个也叫最少知道原则,一个对象应该对其他对象有最少的了解

还是用上面的ImageLoader来说,ImageLoader中使用ImageCache存取bitmap对象,只需要知道ImageCache可以存和取就OK,具体是怎么干的,ImageLoader根本不关心。MemoryCache使用了Jake Wharton的DiskLruCache框架来实现,我们不需要知道。好比我们使用了某一个第三方SDK来解决我们的问题,其实我们不关心它是怎么实现的,他只要解决了我们的问题就OK了。往大了夸张了说,我们平时用手机,只是希望它能解决我们的问题,方便我们的生活,我们才不关心手机芯片是怎么生产出来的!

作者举了一个例子,分别写了一套好的代码和不好的代码,来形容租户和中介的关系,租户告诉中介想要的房子大小和价格,中介直接给到租户就可以,具体中介怎么找的,租户并不关心,而不是说拿了一堆房源出来,让租户自己去对比

public void rentRoom(Mediator mediator) {
    List<Room> rooms = mediator.getAllRoom();
    for (Room room : rooms) {
        if (isSuitable(room)) {
            // 租到房子啦
        } 
    }
}

应该是:

public void rentRoom(Mediator mediator) {
    mediator.getRentOut(roomArea, roomPrice);
    // 租到房子啦
}

那么在我们Android开发当中经常碰到的问题,我举几个常见的说一下:

就拿Activity跳转来说吧,我们需要Intent去实现跳转,有时还会传递几个参数,传参还得需要几个String类型的常量,说不定还得对数据进行一下处理,其实我们只是想要去跳转到某一个界面,也不太想知道是如何传参,如何跳转。那么我们完全可以用一个静态方法来搞定,我只是知道我点了这个按钮,我想跳转而已,给我一个start方法,比我自己去写Intent跳转要爽的多,比如:

Intent intent = new Intent();
intent.putExtra(XXX, id);   
...   
startActivity(intent);

不如:

LoginActivity.start(id);


// LoginActivity中:
public static void start(Context context, String id) {
    if (TextUtils.isEmpty(id)) {
        ...
        return;
    }
    Intent intent = new Intent();
    ...
    intent.putExtra(XXX, id);
    context.startActivity(intent);
}

再比如说,我们在网络接口请求到一个列表的数据,但是本地需要去做一个整合,比如说把日期转换为可以直接显示的格式,或是我们需要通过一些字段去生成一个对象,很多时候网络接口请求下来之后,我们就直接在回调对这些东西开始各种处理,一堆乱七八糟的代码,处理完之后放入列表中去。其实我们想要的是最终展示在list当中的数据,其实具体是怎么整合的,并不太关心,我们完全可以在model层,把数据处理好,再返回给比如说presenter或者viewmodel等。而不是说等到去presenter或viewmodel中甚至是activity中去整合数据,我们只是需要最终的数据,然后把它们展示到list当中去

总结

别看这么点东西,其实写了我好几个小时,但是重新梳理一遍下来,还是收获不少。每一次去重新理解设计模式,都会有不同的感觉。很微妙,先到这,下次回来再写新的收获

    原文作者:pengboboer
    原文地址: https://blog.csdn.net/pengbo6665631/article/details/80788592
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系管理员进行删除。