看似深奥的面向切面编程,其实很简单

640?wx_fmt=jpeg


/   今日科技快讯   /


近日有爆料称:记者“卧底”骚扰电话源头企业,发现百度等一些知名互联网企业存在泄露用户信息的行为。对此,百度官方表示,向他人出售或者提供公民个人信息属于法律禁止行为,百度坚决抵制这种行为,也绝不会开展此类业务。


/   作者简介   /


盼星星盼月亮,又盼到周五了,提前祝大家周末愉快!
本篇文章来自leobert_lan的投稿,分享了他对面向对象设计的了解,希望对大家有所帮助!同时也感谢作者贡献的精彩文章。


leobert_lan的博客地址:
https://blog.csdn.net/a774057695

/   什么是DI,什么是IoC   /


先来谈一谈什么是IoC:


控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度


1996年,Michael Mattson在一篇有关探讨面向对象框架的文章中,首先提出了IoC 这个概念。这个概念看起来苍白,还有更加形象一点的、相关的概念名词:“依赖倒置原则”、“依赖抽象原则”等。注:依赖抽象是对于”依赖倒置“的实际操作方式;依赖倒置原则(Dependence Inversion Principle)


我们说,控制反转是一种设计原则,可以用来降低系统中存在的耦合,那么我们用一个很直白的反例来理解它:
 
 


这是我写出来的耦合度最高的代码了,简直想打人,Tom也想打人:他必须定义好他这辈子可能吃到的所有的食物、他这辈子每天的食谱!那么是什么使我们如此暴躁,是Tom和各种食物之间的很强的耦合,Tom一直具有着控制权。那么要改进设计就是将Tom的控制权移交出去,这就是所谓的控制反转


那么如何去做呢?最关键的就是先实现”依赖抽象“。Tom只要知道那是食物,到点吃就完事了,至于这些食物是Tom之前想好的还是他妈妈决定的,这都不是Tom张嘴吃东西所关心的事情。
我们看一下改进后的例子: 注:限于篇幅,我不展示每一步重构,直接展示一个相对完善的阶段性成果


 
 


我们发现,Tom和具体食物之间没有直接依赖了(耦合降低),想要在系统中添加更多的食物,添加更多的eater就会变得更加简单(系统的扩展性和可维护性提升)
到这里,我们应该可以体会,IoC在面向对象编程中,是一种怎么的思想


什么是DI?


相比于IoC,一种编程思想或者说指导性原则,DI讲的是一个更加具体的东西:依赖注入。


这里话题扯开一下,在oop(面向对象编程,后续将直接使用缩写)中,我们为了让程序的扩展性更好,有单一职能、依赖优于继承等原则,带来的结果就是:多个相关职能的类的实例结合起来才能完成复杂任务。而这些类实例之间就存在着依赖关系(我要完成一件事情时,需要另外一个类的功能),而如何维护这些依赖(使用别的类功能)就是DI要处理的事情。


IOC或者DIP原则指导我们:通过面向抽象去编程,将控制权移交给依赖者、即抽象类的具体子实现类。这也意味着:获得依赖对象的过程被反转了!


在上面的例子中,最开始Tom需要控制自己吃什么,具体吃什么的时候怎么吃,这样编码是很复杂的,修改过后,Tom作为Eater对外的可观测方法是咀嚼、吞咽等动作,选择食物的控制权(这部分我们Demo中没有写,比进食过程要复杂的多,限于篇幅自动略去)移交了出去、如何吃一个具体食物的控制权移交到具体食物中。


那么此时还有一个问题需要处理:将抽象类的具体子实现类的实例注入目标中,即如何获得依赖对象。


/   DI的具体方式   /


我们会了解到这样一种说法,包括我之前的一些文章中也提到过:


  • 构造器注入
  • API注入、接口注入
  • 注解注入

这是从表象观察上得出的结论,因为比较简单,不做示例代码。


而从本质上看,所有的注入形式本质上只有两种:


  • 分析了具体业务,注入的规则(选择具体的依赖对象)直接侵入,在编码中进行描述,包含在类的某一段业务逻辑的编写中。
  • 注入过程抽象给第三方,使用者维护依赖规则。

我们重点讨论下这一块儿。


从事Android的朋友们应该都接触过google出品的MVP分层架构设计的Demo,为了演示的更加清楚一些,我按照记忆中google sample的设计,移除掉jetpack的内容,现写了一个简单的例子:
基类


 
 


数据类和model部分:


 
 


一个文章详情展示页面:
注:我们仅用来做Demo说明问题,实际生产中绑定presenter的时机一般会更复杂一些。
 
 


我们可以看到,用Fragment来实现了View,定义了类EssayPresenterImpl实现了Presenter接口。这两者是如何实现依赖的注入的呢?
就是最终在Activity中的这一段业务代码:
 
 


回到前面:


分析了具体业务,注入的规则(选择具体的依赖对象)直接侵入,在编码中进行描述,包含在类的某一段业务逻辑的编写中


我们是分析了Activity呈现一个视图的流程、以及潜在的更加复杂的业务,在Activity的onCreate阶段,选择了具体的类进行实例化,并完成了相关的依赖注入。


当然,具有实际android开发经验的朋友们可能注意到:实际生产中,一个Presenter使用到不同Fragment子类的场景还可能存在(小概率)例如Essay的阅读模式和编辑模式;但是这种场景确实很少。即使出现了一些需求变化,我们只需要在Activity相应的代码片段做出调整即可,改动量是有限的、可接受的。


ok,继续从刚才的例子看,本身这个设计已经足够优秀了,根据日常实际得出了一个结论:这样的方案已经满足日常开发。让我们把注意力放到EssayRepo的注入


细心的朋友们一定注意到了,我上面定义了两个EssayRepo的实现类。假定我们的故事是这样的:请原谅我黑了一波产品经理和差劲的沟通氛围
  • 一开始产品经理拍板,这个文档编辑应用就是单机的(不要问为什么,就是没多过脑子)

于是我们哼哧哼哧的完成了一个版本,数据都是存在本地数据库的。我们为各种XXXPresenter注入了各种XXXXDao的实例,当然,有些可能是单例。


  • 项目上线后,用户对于创作交互好评如潮,但是提出反馈:没法分享和阅读他人开放的文章。产品经理一拍脑门,我怎么没想到,2.0:存储全部做服务端(不要问为什么之前做单机,就是没过脑子)

于是服务端的开始哼哧哼哧的搞。客户端开始哼哧哼哧的把之前所有注入XXXXDao的地方改成XXXXWebService,并且把之前自动存储的功能转嫁到WebService,不再存储本地DB


  • 项目上线后,IT和服务端主程开始天天盯着服务器监测,随后开始喷产品经理:”说了不信,所有功能都依靠服务器那是作死,QPS太高了“(不要问为什么产品经理之前不听,听了他也不懂,不见棺材不掉泪)

于是客户端的兄弟们又开始哼哧哼哧的修改,将注入的XXXXWebService修改成一种RepoImpl,它在内部封装了自动存储到DB,完成后提交到服务端,按照特定算法决定差异版本中实际有效的版本,并做本地和服务端同步。


  • 终于项目又上线了,IT和后端主程可以睡安稳觉了。细心的用户又提出反馈了,我对文档编辑了,提交了之后、之前打开的详情页啊列表啊啥的信息都不同步啊,写文章的时候都挺费脑子的,一不注意就以为自己漏编辑了,又去操作一遍。产品经理又拍板了:应用内数据一致性维护

于是客户端程序员赶鸭子上架,EventBus满天飞,最终赶上了工期,但几个版本后,项目已经快无法维护了,尤其是大量直接操作DB使得体验变的越来越糟糕。


  • 于是公司高薪聘请客户端架构师,试图最后锤死挣扎。

架构师放大招了,提出了本地中央仓储层,在内存对象层面实现多页面观测数据的一致性+基于数据变化的相应式设计,取代之前难以管理的EventBus事件体系、以及从DB读取数据同步页面。然后又带着大家做了一遍依赖的注入。并在总结会上提出了一个问题:“这几波依赖注入修改的爽吗?”


ok,我们虚构的故事结束了,请原谅我将故事中众多人的智商设计的很低,毕竟,设计高了就没这些事了?。


从这个故事中,我们得出结论:我们把自己忽悠了,这些侵入的代码是没有经过抽象的,他们面临修改时,会给我们制造一些麻烦,这些麻烦和这些代码的场景数成正比,正是我们的一些经验让我们没有正视这一问题,当问题没有爆发时(几率太小)他一点问题都没有,一旦爆发,对于项目的维护和稳定迭代都是巨大的挑战


那么如何解决这些问题呢(如果有必要的话)?
对于注入过程做抽象,用一个第三方,它抽象并实现了依赖的注入、接受使用者对依赖规则的维护,将整个系统中做依赖注入的内容剥离。


两种方式的区别


这里有必要直接给出结论了,假定这个系统是个10万行代码的项目,有100行代码是这样处理依赖注入的。


按照第一种侵入式做法:我们对100行夹杂在业务代码中的内容进行了修改,第一步先从10万行代码中找到并修改,至少需要对这100处代码做单测,甚至需要对10万行代码的项目做完整的集成测试。


按照第二中方式做,可能需要编写300-500行代码实现这100行的内容,但是很集中,只需要在这500行代码中做修改,只需要对这500行代码做单测即可判断依赖注入正确与否。
/   IoC和DI的发展史   /


  • 1996年,Michael Mattson在一篇有关探讨面向对象框架的文章中,首先提出了IOC 的概念
  • 2004年,Martin Fowler探讨了同一个问题,提出了IoC的实现细节,Martin Fowler的经典文章:《Inversion of Control Containers and the Dependency Injection pattern》(https://www.martinfowler.com/articles/injection.html)
  • spring在实践中推广,2004年3月,Spring 1.0使用外部配置文件(xml)描述对象之间的依赖关系。
  • 2004年10月,JDK1.5支持注解(Annotation)语法。2007年3月, Google发布 Guice 1.0,使用annotation描述依赖关系。
  • 2007年11月,Spring 2.5支持使用annotation描述依赖关系
  • 为了规范和统一,JCP于2009年10月发布了JSR330(https://jcp.org/en/jsr/detail?id=330
  • 2012年square发布了开源的dagger1,用于Android平台后来google接盘,搞了dagger2.


/   深入了解,才能用好   /


区别于网上直接讲一个DI框架(例如dagger2)如何用,我们还是先来了解了解JSR-330,这样能够帮助我们更好的使用工具,毕竟这些工具是面向JSR-330写的。


其中定义了一些注解:


  • @Inject
  • @Qualifer, @Named
  • @Scope, @Singleton


Inject :Identifies injectable constructors, methods, and fields. 需要注入的标识,可用于构造器(constructors), 方法(methods)或字段(fields)


Qualifier:Identifies qualifier annotations. 限定器,多子类情况


Named:String-based qualifier. 使用字符串的限定符,


Scope:Identifies scope annotations. 标记作用域


Singleton:Identifies a type that the injector only instantiates once.


定义了一个接口:

我们前面提到过,jsr-330是jcp为了规范各种提供给java生态中使用的ioc容器而定义的协议标准,在其他生态中,是不完全一致的,但是思想是一致的.


前面说到,获取依赖对象的方式交给第三方管理,这个第三方就是IOC容器,它可以是dagger,可以是spring,可以是kodein或者其他,他的作用就是管理依赖规则以及按照依赖规则提供依赖注入。


前面提到:@Inject向IOC容器表达了此处有依赖需要注入,而Provider接口在IOC容器中被实现(或者被注册)以提供可能需要的依赖’对象实例‘;


而限定器(@Qualifer, @Named)在存在必要时,配合@Inject用于向容器说明需要”特定“的依赖、配合Provider用于向容器说明提供”特定“的实例。


而@Scope(用于自定义注解)、@Singleton(通过Scope注解的一个特定注解)是用来标记依赖对象的”生命周期“的控制的,它用于指导ioc容器如何管理这些依赖实例,是每次都新建实例(默认的)还是按照规则复用实例。


/   后记   /
这篇文章写得内容很长,但精髓的内容并不是很多,主旨在于:(1)把复杂的概念通过最直白的语言讲透彻;(2)举出例子引发思考以达到和”被动接受的概念、结论“相互印证。
推荐阅读: 自撸一个Android IM库,即时通讯很难吗?
Shortcuts,让你可以在系统的桌面上为所欲为 由Android官方团队带你学习布局编辑器


欢迎关注我的公众号 学习技术或投稿


640.png?


640?wx_fmt=jpeg 长按上图,识别图中二维码即可关注


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值