[Common] 架构设计的小姿势们

目录:

  1. DI - Dependency Injection
  2. AOP(Aspect Oriented Programming)面向切面编程
  3. 用二进制Option代表进行到哪一步
  4. DI前世今生的插曲
  5. ReMVVM
  6. Service Locator Pattern (和DI很类似)
  7. DDD领域驱动设计

1. DI - Dependency Injection

Spring的两个核心内容为控制反转(Ioc)和面向切面(AOP),依赖注入(DI)是控制反转(Ioc)的一种方式。

可参考造车过程:https://www.cnblogs.com/tinaluo/p/8353886.html

我理解的DI其实是当我们在class A中可能需要class B的时候,我们就自己在class A中new了或者使用了B的单例,这个创建过程是没有人管理的,非常的随意,假设当B的初始化方法需要修改的时候,所有创建B的class都需要修改。

但是其实A只要用B就可以了,它不需要关心B究竟怎么被初始化的,也就是不关心B的实现(表现上就是A不需要import B class)。

故而通过DI来实现在A和B中间加了一个容器,我们可以向容器注册B类对象,当A需要B的时候就向容器索要,容器就会将我们之前注册好的B给A。

当我们想要改B的初始化过程的时候,其实只要将注册给容器的对象改一下就可以了,甚至可以换一个B的实现类。


java中依赖注入的三种方式:
可参考https://blog.csdn.net/liu_shi_jun/article/details/79727838

  • 构造器注入
  • setter注入
  • 接口注入

应用实例

今天遇到了一个情景,和厉害小哥哥讨论了一下觉得这种就应该用DI来实现,具体是酱紫的:

view controller会持有很多小插件的视图view,然后也会有这些小插件的单例管理者,管理者会持有小插件们的对象。

如果小插件对象例如聊天插件,在接收到服务器的消息以后,要把别人发来的消息放到消息view上面,就需要操作他的插件视图,但这个视图在vc里面,这要怎么办呢。

如果将各个小插件回调给他们的管理者,由管理者再向上回调vc里面的方法,那么管理者要配备超多的方法,为了响应各种小插件的操作,而且每增加一个小插件,可能都要改管理者,这是非常不好的。

所以最好还是由插件可以直接获取到他们的视图view,而vc也要能方便地获取view。

这个时候可以考虑用一个类做视图管理,这个类可以通过DI依赖注入获取,也就是我们只要在开始的时候注册一下,在vc以及小插件里面都可以通过向DI容器请求来获取这个对象。

然后由这个视图管理类来持有各种插件view,这样其实很多插件都可以通过DI获取这个视图管理实例。DI的一个好处就是当这个东南是会被很多地方用到的时候,可以比较方便的解耦。这里正好会有很多plugin使共用图层管理,就非常合适抽出一个图层管理类。


2. AOP(Aspect Oriented Programming)面向切面编程

OOP的特点在于它可以很好的将系统横向分为很多个模块(比如通讯录模块,聊天模块,发现模块),每个子模块可以横向的衍生出更多的模块,用于更好的区分业务逻辑。而AOP其实相当于是纵向的分割系统模块,将每个模块里的公共部分提取出来(即那些与业务逻辑不相关的部分,如日志,用户行为等等),与业务逻辑相分离开来,从而降低代码的耦合度。

我们什么情况下可能会用到AOP呢?比如如果需要在每个控制器的viewDidLoad里面都需要添加统计代码,或者每个类都需要添加日志代码。其实上面的需求很容易想到在每个控制器里面都写一遍,这样的话会有很多重复的代码而且不好维护。另外也可以用继承,但是用继承无形中增加了类的强耦合,所以都不是最好的办法。

AOP主要是被使用在日志记录,性能统计,安全控制,事务处理,异常处理几个方面。

一种方式是通过swizzle方法调配实现AOP,另外一起来看下一个iOS的AOP库吧

※ Aspects

git地址:https://github.com/steipete/Aspects

我们捕捉viewWillAppear的这个方法,把option设为AspectPositionAfter,block就会在viewWillAppear执行之后再执行:

[self aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id info){
  NSLog(@"hook_viewWillAppear");
} error:nil];

而且不光能对对象使用,也可以直接对类使用

[ViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id info){
  NSLog(@"hook_viewWillAppear");
} error:nil];

这样所有这个类的对象在执行viewWillAppear的时候,都会触发block。

当然,对类的hook和对对象的hook是不一样的,类应该是整体起效,而对象是独立起效。

整体这个库的实现原理就是利用runtime swizzle/add method + 消息转发~ 具体原理可以参考:https://www.jianshu.com/p/6dfa47763cd9

※ 应用场景

我这周遇到的状况是,几个VC有个共同的特征,就是他们持有很多plugin,然后这些plugin会在同样的delegate里面做同样的事情,例如在delegate的didLoad里面就要加载plugin。

现在的实现是让几个VC都持有一个PluginManager的实例,而这个实例持有了这些plugin,如果用AOP只要在delegate的几个方法的before切入plugin的处理就可以啦~


3. 用二进制Option代表进行到哪一步

需求是酱紫的,加入我写好作业这个事儿是需要先写数学,在写语文,最后写英语,然后才能退出写作业流程。

如果我中间不小心退出了,之后仍旧要继续之前的过程,例如我之前到了语文,回来还要开始写语文、英语、退出。

于是如果都用completion block一层套一层的话,有多少种可能性就要写几种,例如如果进行到数学,就要数学finish后语文finish后英语;如果进行到语文,就要语文finish后英语,如果进行到英语,就要写英语;如果都写完了就退出吧。

if (state == mathNotFinish) {
  [self writeMathWithCompletion:^(Bool finished){
    if (!finished) {
      return;
    }
    
    [self writeChineseWithCompletion:^(Bool finished){
      if (!finished) {
        return;
      }

      [self writeEnglishWithCompletion:^(Bool finished){
        if (!finished) {
          return;
        }
        
        // all done
       }];
    }];
  }];
} else if (state == ChineseFinish) {
// 照着上面来一遍。。。
} else {
……
}

如果像上面这么嵌套+枚举可能性我自己都想打自己了。。代码实在太丑了,所以可以通过Option来实现对过程的管理,也就是到了哪一步

然后continue的时候根据当前状态进行下一步,有点儿类似数据库升级:

typedef NS_OPTIONS(NSUInteger, StageOption) {
    StageOptionMathDone = 1 << 0,
    StageOptionChineseDone = 1 << 1,
    StageOptionEngDone = 1 << 2,
};

- (void)doHomework {
    if (self.workStage & StageOptionEngDone) {
        return;
    }
    
    if (self.workStage & StageOptionChineseDone) {
        [self writeEnglishWithCompletion:^(Bool finished){
            if (!finished) {
                return;
            }
            
            self.workStage = self.workStage | StageOptionEngDone;
            [self doHomework];
        }];
        return;
    }
    
    if (self.workStage & StageOptionMathDone) {
        [self writeChineseWithCompletion:^(Bool finished){
            if (!finished) {
                return;
            }
            
            self.workStage = self.workStage | StageOptionChineseDone;
            [self doHomework];
        }];
        return;
    }

    [self writeMathWithCompletion:^(Bool finished){
        if (!finished) {
            return;
        }
        
        self.workStage = self.workStage | StageOptionChineseDone;
        [self doHomework];
    }];
}

4. DI前世今生的插曲

凌哥上周讲了一下DI的来源,就顺了一遍知识点,所以这里再整理一下为啥有DI~ (我整天都在拾人牙慧啊......

最开始的时候,如果我们用别的class,就自己alloc init一个,直接用就可以了~ but如果我们用的class改了init方法,需要传入参数了,那么代码里面所有用到这个class的地方都要改init方法。那么这个时候我们就想,提供一个工厂方法不就好了,把init都放在一个地方,别的地方用就直接调用factory的create方法~

但这个时候会出现一个新的问题,如果你create A的时候需要create B,create B的时候又需要create A,那么就形成了一个环,会不断循环调用导致死循环。这个时候就出现了DI,Dependency Injection,他比工厂好在可以解决环形依赖的问题

关于DI我之前有写过好几篇啦~ 可以参考:https://www.jianshu.com/p/4e90327551be (这里面有个part是DI的scope,也就是生成对象的时候是用的同一个还是每次新建,这里weak和graph的区别主要是weak是如果有强引用每次找DI要拿到的都是同一个,然而graph是一次解环也就是一次init保证拿到的是同一个)

DI还有一个好处就是可以方便单元测试,因为你可以随意改变对象的生成,也就可以生成一个需要测试的类所依赖的class给被测试的对象

  • 这里就有一个问题了,为啥要单元测试?
    单元测试其实会自动把每个类的各个方法的各种case的期望输出和实际输出对比,如果都是对的说明这个class没有问题。当我们的工程越来越大的时候,每改一行代码都有可能造成其他一个看起来没关联的class行为错误,如果有单元测试,每次build的时候会自动跑一遍所有的case,如果有问题就可以很快发现了。也就是说单元测试可以防止改代码影响到之前的代码,确保app不会因为新增代码出bug

  • 关于引入三方库的问题:
    引入大一点的三方库的时候(例如AFNetworking)最好自己再包一下,防止之后需要换,可以很方便的直接换掉底层实现。


5. ReMVVM

这个是前几天和大神爬山聊到的~

Redux + MVVM = ReMVVM
Redux和Flux相似,都是前端的框架,ReSwift那个库就是实现这个的

关于MVVM可以参考:https://www.jianshu.com/p/f1d0f7f01130

关于ReMVVM可以参考:https://www.ctolib.com/dgrzeszczak-ReMVVM.html

这个我准备单独写一篇学习一下~ 大家感兴趣可以康康~


6. Service Locator Pattern (和DI很类似)

服务定位模式(Service Locator Pattern)是一种软件开发中的设计模式,通过应用强大的抽象层,可对涉及尝试获取一个服务的过程进行封装。

定位服务

如果现在想要解耦classA和两个service,做到无需动A里面的代码就能改实现,可以加入中间人locator:

service locator

Service Locator 模式并不描述如何实例化服务,它更多的是绑定一个已经存在的对象和这个对象的标识符,让外部可以通过标识符获取对象,所以其实它不是很关心你是如何初始化的,更像一个查找表。
其描述了一种注册和定位服务的方式。通常情况下,Service Locator 模式与工厂模式(Factory Pattern)和依赖注入模式(Dependency Injection Pattern)等结合使用。

实际上这Service Locator 和 DI 都提供了基本的解耦合能力——无论使用哪个模式,应用程序代码都不依赖于服务接口的具体实现。两者之间最重要的区别在于:这个“具体实现”以什么方式提供给应用程序代码。

使用Service Locator 模式时,应用程序代码直接向服务定位器发送一个消息,明确要求服务的实现;使用Dependency Injection 模式时,应用程序代码不发出显式的请求,服务的实现自然会出现在应用程序代码中,这也就是所谓“控制反转”

控制反转是框架的共同特征,但它也要求你付出一定的代价:它会增加理解的难度,并且给调试带来一定的困难。所以,整体来说,除非必要,否则我会尽量避免使用它。这并不意味着控制反转不好,只是我认为在很多时候使用一个更为直观的方案(例如Service Locator 模式)会比较合适。

一个关键的区别在于:使用Service Locator 模式时,服务的使用者必须依赖于服务定位器。定位器可以隐藏使用者对服务具体实现的依赖,但你必须首先看到定位器本身。所以,问题的答案就很明朗了:选择Service Locator 还是Dependency Injection,取决于“对定位器的依赖”是否会给你带来麻烦。

这里引入了一个问题,就是DI其实不仅仅是一个容器,你想要什么找他要,他去负责解依赖以及创建(工厂和service locator都没有weak之类的那种DI对依赖的处理,并隐藏了类依赖项),而是一种思想,就是一个类可以不依赖别的类被创建出来,其他属性可以通过注入来解决,而工厂和locator其实没有这种思想

工厂和service locator的由来和区别可以参考:https://www.cnblogs.com/Free-Thinker/p/4173256.html,感觉主要是service locator可以隐藏参数和实现,不像工厂如果改动了参数全局都要变化,和DI的思想类似。


7. DDD领域驱动设计

参考美团的设计:https://tech.meituan.com/2017/12/22/ddd-in-practice.html

我们创建微服务时,需要创建一个高内聚、低耦合的微服务。而DDD中的限界上下文则完美匹配微服务要求,可以将该限界上下文理解为一个微服务进程。

上述是从更直观的角度来描述两者的相似处。

在系统复杂之后,我们都需要用分治来拆解问题。一般有两种方式,技术维度和业务维度。技术维度是类似MVC这样,业务维度则是指按业务领域来划分系统。

微服务架构更强调从业务维度去做分治来应对系统复杂度,而DDD也是同样的着重业务视角。 如果两者在追求的目标(业务维度)达到了上下文的统一,那么在具体做法上有什么联系和不同呢?

我们将架构设计活动精简为以下三个层面:

  • 业务架构——根据业务需求设计业务模块及其关系
  • 系统架构——设计系统和子系统的模块
  • 技术架构——决定采用的技术及框架

以上三种活动在实际开发中是有先后顺序的,但不一定孰先孰后。在我们解决常规套路问题时,我们会很自然地往熟悉的分层架构套(先确定系统架构),或者用PHP开发很快(先确定技术架构),在业务不复杂时,这样是合理的。

跳过业务架构设计出来的架构关注点不在业务响应上,可能就是个大泥球,在面临需求迭代或响应市场变化时就很痛苦。

DDD的核心诉求就是将业务架构映射到系统架构上,在响应业务变化调整业务架构时,也随之变化系统架构。而微服务追求业务层面的复用,设计出来的系统架构和业务一致;在技术架构上则系统模块之间充分解耦,可以自由地选择合适的技术架构,去中心化地治理技术和数据。

DDD与微服务

你可能感兴趣的:([Common] 架构设计的小姿势们)