设计模式笔记01——设计原则

说明:有人私底下问我MVP并不是BaseActivity,其实我想说的是本文并不是谈MVP,至于本文提到的MVP初体验重点在于MVP的思想而不是具体实现。而且到目前为止没有谁说MVP就一定是什么样的实现,而且在我的项目中比较推荐的是View层需要抽取一个BaseView用于Presenter的固定持有,还有很多业务View(接口隔离原则),用于具体业务方法传入作为Presenter的临时参数。当然我们项目使用的是RxJava和retrofit,在BaseActivity中实现的BaseView,主要为了管理RxJava的CompositeSubscription。依据实际情况考虑需不需要实现BaseView,也就是说Presenter是否一定需要持有一个固定的View,至少我认为也不是那么必然性,让View作为接口处于游离状态,就像是一个积木一样,需要的时候具体类实现它便是而且不局限于Activity和Fragment。

大纲

代码写了不少,从来没有进行过整理和记录,时间久了总是会忘了,而且日常的所知总是零零散散的,很难成为一个体系和个人日常行为自然反应。其实我一直很相信一个人如果面对每一个事情,无论是难办的,还是非常简单的都能够很自然的潜意识给出一个方案来。首先无论最佳,至少有自己的经验和想法,当然在不断学习中不断强化这种潜意识的判断,那样的人生我才会感觉到是一种传奇。
言归正传,本文主要记录一下我个人理解的程序设计六大原则,在整理的时候也在网上看了很多相关文章作为参考。

程序设计六项原则

首先我们了解一下六项原则是哪些原则:

  • 单一职责原则
  • 里氏替换原则
  • 依赖倒置原则
  • 接口隔离原则
  • 迪米特法则
  • 开闭原则

这六项原则或多或少多听过,我们在敲代码的时候当然也不至于面面俱到,原因不是原则可有可无,主要是这六项原则中有一些可能相互有所出入,需要我们结合实际灵活运用。例如过于追求接口隔离原则的最后就是一场灾难,尤其对于大型项目而言。

单一职责原则

SRP(Single Responsibility Principle)
There should never be more than one reason for a class to change
上面的英文不难理解,字面上就是影响一个类改变的因素不能超过一个。也就是说:我们在编写代码过程中对于一个类的实现,尽最大可能只为一件事情或一个对象而服务,而不能让它同时为多个对象或事情提供方法。

确实不太好说明清楚,主要是很多事情还是一句实际情况区分,还是距离说明吧

案例

实现一个登陆页面,在登陆页面中有两个输入框分别是用户名、密码的输入以及一个按钮进行登陆请求,将输入的内容传递给服务器,服务器返回结果之后页面给予用户提示或者其他交互响应。

这个场景并不少见,而且功能很简单没什么好顾虑的,直接上手就写,例如Android下:
1、直接编写layout布局文件之后;
2、在LoginActivity中直接获取控件
3、设置按钮监听,在监听方法中获取输入内容,请求后台数据
4、判断请求结果做出反馈
以上实现均在LoginActivity中实现,乃至一些人(早起我也是这么干的)直接在onCreate中完成所有任务就完事了。

分析

上面的方案没有错误,当前需求很简单代码量也不会太多,而且的确没什么技术含量,业务流程很清晰。但是问题来了,目前第三方登录算是APP的基本功能了,产品要求加入第三方功能,后台服务器新增第三方认证接口。需要我们进行迭代维护升级新需求!

这时候你看到新增一个接口请求,新增几个第三方登录按钮,然后继续在onCreate中完成,然后测试没问题,会有一看代码难看的自己都忍不住摇摇头。更不说后面产品需要加入用户名、手机号码、电子邮箱同时进行登录,需要客户端进行验证三者调用不同接口,那你就更加抓狂了。

程序设计原则最大的魅力就是让我们的事情做起来更加简单,这里不仅仅是完成眼前的需求,更是为了让我们更好的分析和维护未来的改动

MVP初体验

MVP是2017年被提及最多的一种模式了,我也不知道说程序设计模式呢,还是说代码设计模式,似乎都对有似乎有点不对。MVP自从出来之后就没一个固定的模板,包括我自己都没有按照特定范例来进行,因地制宜而已。
接着上面的案例,我们接着分析:

1、在Android中Activity主要用来进行页面的展示,用户交互,那么我们可以进行相关布局以及事件的监听;
2、用户登录需要请求网络,获得登录结果数据,但由于业务不确定性,或者说后期可维护性吧,我们将接口请求和Activity分别放在不同类中实现LoginModel;
3、那么Activity中的数据如何传递给LoginModel呢?或者说是否能够传入LoginModel,这时候我们可以让Activity自己在响应事件中自行判断,从而调用Activity中LoginModel对象方法(LoginActivity持有LoginModel对象),但是可能导致Activity代码量偏多,加上Activity本身一些事件和生命周期的维护代码,显得Activity更加浮肿。这时候我们就会将这部分业务代码独立抽取出来,创建一个 LoginPresenter类,然后LoginPresenter分别持有LoginActivity和LoginModel的引用

不知道大家可明白了,上面的案例和最后的分析。可能文字有点啰嗦,但是我想说的是在十几项过程中我们为了方便维护和扩展性,我们可以分别按照MVP进行划分三层结构,LoginModel主要负责用户数据获取,也就是网络请求,LoginActivity主要负责维护Activity生命周期和用户交互,LoginPresenter主要负责业务场景的整理,例如校验输入是否为手机号码、电子邮箱等从而调用LoginModel对应的方法

总结

  • 类的复杂性降低,实现什么职责都有清晰明确的定义
  • 可读性提高、复杂性降低
  • 可维护性提高
  • 变更引起的风险性降低

好东西我们愿意接受,但是也要适当承受一些问题,例如:我们会发现类的数量变多了,那么在个人项目中可能会在一定程度上增加工作量。因此抛开设计模式,就日常代码编程而言的话我个人更加认为这个单一原则更多的强调和体现的是一种编程习惯

里氏替换原则

对于面向对象而言,这个肯定不会陌生,我最早接触这个还是档期学C#时,在面向对象中有一个很重要的概念,那就是继承。在继承中子类通过extends继承父类之后,父类所有的方法和属性都可以由子类继承得到。这里其实有一个问题那就是private声明的属性是否也会被继承呢?其实我的理解是继承过程中有显性和隐性之分,我一直将private声明的认为是对于子类而言的隐性继承,不然为什么对于特定方法可以获取到相关属性信息呢?当然如果对于通过方法获取到信息你理解成是父类的封装性也对。因此这个话题我觉得怎么说也都没啥大问题。

概念

  • 父类声明,子类实例化
  • 父类出现的地方,子类都可以出现,但是反过来就不行了
  • 方法的重写,重载。

也就是我们在需要传入或者使用一个子类的时候尽量用父类代替子类进行声明,或者入参。原因不言而喻是为了方法和业务的扩展性。

案例

接着上面的例子,用户登录按钮点击进行网络请求过程中我们需要提出对话框提示用户等待登录请求结果,并且所有含有网络请求,耗时操作都需要弹出对话框提示“正在进行中…………”。

实现

我们可以创建一个BaseActivity,然后在BaseActivity中实现showProssGresss(String str),然后让LoginActivity继承BaseActivity。在LoginPresenter中直接处理相关逻辑

那么在这过程中,我们改动很小,而且只需要处理LoginPresenter中业务代码,同时对于其他需要实现相同需求的只需要是BaseActivity的子类就行了

在项目中,采用里氏替换原则时,尽量避免子类的“个性”,一旦子类有“个性”,这个子类和父类之间的关系就很难调和了。把子类当做父类使用将会抹杀子类的“个性”,如果把子类直接作为业务使用,那么将会使代码之间的耦合关系扑朔迷离!
关于这个主意内容,还是需要依据实际情况而言来衡量,例如上面的BaseActivity其实我们很难严格约束子类,因为这本就是一个个性化的扩展。当然如果让BaseActivity在继承自一个SuperBaseActivity那我也没话说

总结

这个没什么说的了,其实就是一个继承关系,我们不但可以继承父类,也可以通过接口方式定义约束实现类的方式来进行,在声明中我们使用对应接口声明,实现类实例化那么就可以完成上面类似的需求。
在MVP中对于V层更多的是通过抽象接口进行传入P层,以方便P中多有多样化的V层,同时使P层最大化利用,以在一定程度上降低代码量。

依赖倒置原则

其实这一部分没什么内容的,我们结合上面的案例,先提出一个问题。
不知道大家有没有用过EventBus(没有了解的可以参考EventBus官方),当我们实现了一个EventBusActivity,只要需要接收EventBus事件的Activity继承EventBusActivity之后就会自动注册和注销EventBus的事件监听,这时候在上面的LoginActivity需要接收EventBus事件,那么怎么办呢?(当然就地注册一下也不是什么难事)。

概念

依赖倒置原则(Dependence Inversion Principle DIP)
High level modules should not depend upon low level modules ,Both should depend upon abstractions ,Abstractions should not depend upon details,Details should depend upon abstractions

我的英语确实不怎么样,字面我的翻译是:高级模块部分不能依赖低级模块,两者都应该依赖于抽象的定义,抽象部分不能依赖于具体实现,具体实现模块需要依赖于抽象部分。我们还是只是过一遍吧,着实也说不清楚,但是说白了就是怎么解决上面的那个接收EventBus事件的问题。

解决问题

EventBusActivity我们提取出来抽象成为一个接口为IEventBus,

public interface IEventBus {
}

然后在BaseActivity中对应的方法进行判断是否实现IEventBus,如果实现了就进行注册EventBus
onCreate中

if (this instanceof IEventBus) {
EventBus.getDefault().register(this);
}

onDestroy中

if (this instanceof IEventBus) {
            EventBus.getDefault().unregister(this);
        }

然后只需要让LoginActivity声明实现IEventBus即可

总结

这个原则主要强调我们编码过程中对于关系控制,更多的使用抽象应用,减少实体类的直接依赖。抽象接口对于类而言更加方便管理和维护。

接口隔离原则

对于接口隔离原则和单一原则很类似。我们可以理解成我们所定义的接口中,所有接口尽量只是针对一个功能或操作进行约束和定义的。

定义

客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上

不同于单一职责原则,却类似于单一职责原则,单一职责原则要求的是接口和类的定义着重在实现的某种功能。接口隔离则是要求接口的定义避免复杂,浮肿(也就是方法过多),最理想状态是一个接口约束一个方法!

提出问题

这里我就提出一个场景,还是接着上面的案例来进行。在登录页面中,用户点击登录之后我们需要依据后台返回的结果弹出不同的提示内容,同时如果是第三方登录我们需要在当前页面提示当前账号没有绑定手机号码,让用户选择去绑定手机号码或者去绑定现有其他账号,如果登录失败我们会依据后台返回参数现实图片验证码,用户下一次点击登录时需要输入正确图片验证码才能再次响应按钮请求

分析问题

在这个过程中由于现实对话框的关系我们默认让LoginActivity继承自BaseActivity,在LoginPresenter中控制对话框的显示与隐藏,但是这次显示选择绑定操作和图片验证码的操作并非具有一定的公共性,并不适合在BaseActivity中实现,而且图片验证码的操作也并不是对话框,在一定程度上需要布局的支持才可以。

这时候我们可能就不太容易操作了,于是乎LoginPresenter似乎也只是持有BaseActivity对象,即便实际是LoginActivity实现也无法直接调用LoginActivity的showImageNotifyCode方法。那怎么办呢?
在实际项目中相比也并不缺乏这种场景,我的解决办法是抽象成接口LoginNotifyListener,由LoginPresenter中的login方法提供一个入参为LoginNotifyListener。那么问题来了LoginNotifyListener怎么定义呢?
可能有人会直接定义两个方法就是了,分别为choiceBindOption和showImageNotifyCode。细心的朋友发现了,其实图片验证码的需求并不是只有这里可能用到,主要用于控制服务器的访问平率,例如注册,获取验证码等等什么的场合都可能用到,拿到那时候我们还得分别创建不同的接口么?当然答案肯定是否定的!
这时候大家就会深刻的明白了接口隔离原则的用处了接口隔离只为了让我们在关系引用过程中使我们更加明确我们需要做什么
我们可以声明两个接口分别为BindAccountOptionListener和ImageNotifyCodeListener,方法分别是choiceBindOption和showImageNotifyCode,然后让LoginNotifyListener同时继承自这两个接口,最后让LoginActivity实现LoginNotifyListener接口即可

总结

上面的案例不知道大家在项目中是否会遇到,但是我们认为应该是比较常见的吧。对于接口隔离原则而言我们不必太过于强调,但是在实际也无需求的时候只要遵循公用性即可,因为一味地强调接口隔离原则会导致接口数量非常多。

迪米特法则

一个对象应该对其他的对象保持最少的了解

这个没什么特别想说的,因为这个就像是前面的一个强调

迪米特法则更像是前面的里氏替换原则和依赖倒置原则的一个总结和升华
面向接口编程更加优于面向实现编程,接口编程依据里氏替换原则更加符合迪米特法则

开闭原则

一个软件实体如类、模块和函数应该对扩展开放,对修改关闭

分析

这是一个设计的基础原则,但却又是定义最为模糊的一个原则,对扩展开放,对修改关闭。具体如何操作那就是见仁见智了
面向抽象编程,抽象构建整个系统框架,实现扩展具体逻辑细节。虽说如此,但是框架一旦改变是否就存在重构呢?
这是一个设计原则的综合,当前面的五项原则得到了很好地运用,那时候恐怕就是开闭原则了吧,

总结

现在我们针对六项原则进行一下总结,首先我们强调一下,六项原则在实际项目中并非是完全必须遵守的,关键在于实际的运用。

前面也提到过,过于强调单一原则和接口隔离原则,必然导致类和接口的剧增,对于一些团队而言并不是什么乐观的事情。(几乎所有程序员都想吐槽的,时间紧任务重外加催的烦导致心情乱)
所以这六项原则更像是日常编程的一个规范,准确的说是程序设计的六项规范,了解设计模式的或多或少对照上面的应该都可以对号入座几个。

在实际项目情况做的恰如其分是每个程序员都想的,但是对于框架,项目程序设计者而言在产品发展和项目设计稳定性两者之间如何取舍,应该慎重。
我个人的观点是复杂项目循序渐进迭代,既有利于产品本身的市场尝试,又不会急切的固定自己的发展方向,毕竟任何想法都需要市场来肯定才是王道。当然关键就是我们可以更好的把控项目设计风险,争取做到小风险小改动小情绪

    原文作者:风雨沉安
    原文地址: https://www.jianshu.com/p/bba153df5a60
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞