系统设计相关

206 阅读23分钟

设计模式

六大设计原则

单一职责原则

  • 一个类只负责一件事
  • CALayerUIView

开闭原则

  • 对修改关闭、对扩展开放

接口隔离原则

  • 使用多个专门的协议、而不是一个庞大臃肿的协议
  • 协议中的方法应尽量少
  • UITableviewDelegate & UITableviewDataSource

依赖倒置原则

  • 抽象不应该依赖于具体实现,具体实现可以依赖于抽象

里氏替换原则

  • 父类可以被子类无缝替换,且原有功能不受任何影响

迪米特法则

  • 一个对象应该对其他对象有尽可能少的了解
  • 高内聚、低耦合

常用设计模式

设计模式,在框架中经常使用到。比如iOS框架中大量使用原型模式、工厂模式、单例模式、观察者模式等模式,我们自己在设计代码时候也会使用设计模式。下面简单介绍下各个模式以及在iOS中的应用,具体的模式详解,笔者后续会在设计模式专栏中讲解。

对象创建

原型模式

  • 使用原型实例指定创建对象的种类,并通过复制这个原型实例创建新的对象。

  • UML图

    image-20210331161318459

  • iOS中的NSCopying和 NSMutableCopying提供了实现原型模式需要遵循的模式,客户通过调用copy或者mutableCopy来实现原型复制。

单例模式

  • 使用单例模式来创建类的唯一对象,客户端只能通过这个唯一对象访问这个类的功能。

  • UML图

    image-20210331161342808

  • iOS中很多系统类都是使用单例模式实现的。比如UIApplication、NSFileManager。

  • 示例代码:

@implementation Mooc

+ (id)sharedInstance
{
    // 静态局部变量
    static Mooc *instance = nil;
    
    // 通过dispatch_once方式 确保instance在多线程环境下只被创建一次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 创建实例
        instance = [[super allocWithZone:NULL] init];
    });
    return instance;
}

// 重写方法【必不可少】
+ (id)allocWithZone:(struct _NSZone *)zone{
    return [self sharedInstance];
}

// 重写方法【必不可少】
- (id)copyWithZone:(nullable NSZone *)zone{
    return self;
}

@end

注意点:为了防止使用者创建对象,需要从重写两个方法allocWithZonecopyWithZone:。另外instance = [[super allocWithZone:NULL] init];需要使用super方法调用防止在第一创建时循环调用。

工厂模式

工厂模式分为简单工厂、工厂方法和抽象工厂。

  • 简单工厂用一个类来创建所有产品。这个类不继承任何抽象工厂。调用方通过调用这个类来创建不同的产品。
  • 工厂方法,所有工厂继承一个抽象工厂,每个抽象工厂用来创建一个特定产品。比如工厂A用来创建产品A,工厂B用来创建产品B。调用方通过调用不同的工厂来创建不同的产品。
  • 抽象工厂是工厂方法的升级版。它跟工厂方面的区别是:通过把产品进行分类,使得一个工厂可以创建多个产品。比如工厂A可以创建产品A和产品C。

简单工厂

  • 简单工厂用一个类来创建所有产品。

  • UML 图

    image-20210331161531763

工厂方法

  • 定义创建对象的接口,让子类决定实例化哪一个类。工厂方法使得一个类的实例化延迟到其子类。

  • UML图

    image-20210331161546111

  • 使用场景 1、编译期无法预期要创建的对象的种类。 2、类有若干辅助类为其子类,想对外部隐藏这些子类的实现。

抽象工厂

  • 提供一个创建一系列相关或者互相依赖对象的接口,而无须指定他们具体的类。

  • UML图

    image-20210331161611594

  • iOS中的类簇(Class Clusters)采用抽象工厂实现的。比如NSNumber、NSString、NSArray、NSDictionary、NSData。

生成器模式

  • 将一个复杂对象的构建分步进行,使得同样的构建有不同的表现。

接口适配

适配器模式

  • 将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

  • UML图 1、类适配模式。通过继承被适配类实现适配。

    image-20210331161631712

    2、对象适配模式。通过组合被适配类实现适配。

    image-20210331161640996

  • 使用场景 1、已有类的接口于需求不匹配。 2、想要一个可复用的类,该类能够跟带有不兼容接口的其他类写作。 3、需要适配一个类的几个不同子类,可是让每一个子类去子类化一个类适配器不太现实。可以使用对象适配器(委托)来适配其父类的接口。 4、可以用iOS中的delegate和Block来实现适配器模式,他们用的是对象适配器。

示例代码:有两个类Target是被适配对象,CoolTarget为适配对象。

Target类

#import <Foundation/Foundation.h>

@interface Target : NSObject

- (void)operation;

@end
#import "Target.h"

@implementation Target

- (void)operation
{
    // 原有的具体业务逻辑
}

@end

CoolTarget类:

#import "Target.h"

// 适配对象
@interface CoolTarget : NSObject

// 被适配对象
@property (nonatomic, strong) Target *target;

// 对原有方法包装
- (void)request;

@end
#import "CoolTarget.h"

@implementation CoolTarget

- (void)request
{
    // 额外处理
    
    [self.target operation];
    
    // 额外处理
}

@end

适配器优点:

  • 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,而无须修改原有代码。
  • 增加了类的透明性和复用性,将具体的实现封装在适配者类中,对于客户端类来说是透明的,而且提高了适配者的复用性。
  • 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”。

桥接模式

  • 将抽象部分与它的实现部分分离,使他们都可以独立地发生变化。

  • UML图

    image-20210331161729072

  • 使用场景 JSBridge的实现就用了桥接模式。我们一般把实现部分抽离出来一个单独的实现类。分别来桥接模UIWebView和WKWebView。

  • 实现img

代码示例: ClassA

#import <Foundation/Foundation.h>
#import "BaseObjectB.h"
@interface BaseObjectA : NSObject

// 桥接模式的核心实现
@property (nonatomic, strong) BaseObjectB *objB;

// 获取数据
- (void)handle;
@end
#import "BaseObjectA.h"

@implementation BaseObjectA

 /*
   组合方式:
    A1 --> B1、B2、B3         3种
    A2 --> B1、B2、B3         3种
    A3 --> B1、B2、B3         3种
  */
- (void)handle
{
    // override to subclass
    // 处理objB中的方法。
    [self.objB fetchData];
}

@end

ClassA的子类A1、A2、A3重写父类中handle方法。

#import "ObjectA1.h"

@implementation ObjectA1

- (void)handle
{
    // before 业务逻辑操作
    
    [super handle];
    
    // after 业务逻辑操作
}
@end

ClassB 实现

#import <Foundation/Foundation.h>

@interface BaseObjectB : NSObject

- (void)fetchData;

@end
#import "BaseObjectB.h"

@implementation BaseObjectB
// 默认逻辑实现
- (void)fetchData
{
    // override to subclass
}
@end

ClassB的子类进行具体的逻辑实现。

#import "ObjectB1.h"

@implementation ObjectB1

- (void)fetchData{
    // 具体的逻辑处理
}
@end

使用方代码实现

@interface BridgeDemo()
@property (nonatomic, strong) BaseObjectA *objA;
@end

@implementation BridgeDemo

/*
 根据实际业务判断使用那套具体数据
 A1 --> B1、B2、B3         3种
 A2 --> B1、B2、B3         3种
 A3 --> B1、B2、B3         3种
 */
- (void)fetch
{
    // 创建一个具体的ClassA
    _objA = [[ObjectA1 alloc] init];
    
    // 创建一个具体的ClassB
    BaseObjectB *b1 = [[ObjectB1 alloc] init];
    // 将一个具体的ClassB1 指定给抽象的ClassB
    _objA.objB = b1;
    
    // 获取数据
    [_objA handle];
}
@end

桥接模式的优点:

  • 分离抽象接口及其实现部分。
  • 桥接模式有时类似于多继承方案,但是多继承方案违背了类的单一职责原则(即一个类只有一个变化的原因),复用性比较差,而且多继承结构中类的个数非常庞大,桥接模式是比多继承方案更好的解决方法。
  • 桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。
  • 实现细节对客户透明,可以对用户隐藏实现细节。

外观

  • 为系统中的一组接口提供一个统一的接口。外观模式定义一个高层接口,让子系统更易于使用。

  • UML图

    image-20210331161743637

  • 使用场景 如果使用一个子系统的某个功能,需要调用多个类才能实现,这个时候可以用外观模式。通过提供一个接口让客户调用,这样可以隐藏实现,降低客户使用门槛。

对象解耦

中介模式

  • 用一个对象来封装一系列对象的交互方式。中介者使各个对象不需要显示地相互引用,做到高内聚、低耦合。

  • UML图

    image-20210331161753297

  • 使用场景 1、CTMediator使用了中介者模式来进行模块解耦。 2、MVC模式中的VC就是中介者。

观察者模式

  • 定义对象间的一种一对多的依赖关系,当一个对象的状态发生变化时,所有依赖于它的对象都得到通知并自动更新。

  • UML图

    image-20210331161805396

  • 使用场景 1、观察者模式能实现对象之间的彻底解耦。 2、iOS中的观察者中心和KVO就是观察中中心的实现。

抽象集合

组合模式

  • 将对象组合成树形结构以表示"部分-整体"的层次结构。组合使得用户对单个对象和组合对象的使用具有一致性。
  • UML图 image-20210331162429141
  • 使用场景 iOS中的UIView就是组合模式的实现。通过UI控件组合形成UI树,呈现界面给用户。

迭代器模式

  • 提供一种方法顺序访问一个集合对象的各个元素,而不需要暴露该对象的内部表示。

  • UML图

    image-20210331161826240

    image-20210331161835439

    迭代器分为外部迭代器和内部迭代器。外部迭代器需要用户手动创建迭代器,内部迭代器不需要用户手动创建,由组合对象提供枚举方法。

  • 使用场景 1、iOS中NSEnumerator提供了外部迭代器的实现。 2、iOS中集合类方法enumerateObjectsUsingBlock提供了内部迭代器的实现。 3、iOS中的快速枚举也是迭代器的实现,需要实现NSFastEnumeration。

行为扩展

访问者

  • 表示一个作用于某对象结构中的各元素的操作。它让我们可以在不改变各元素的前提下定义作用于这些元素的新操作。

  • UML图

    image-20210331161848095

  • 使用场景 1、遍历组合对象。 2、扩展复杂类对象的行为。

装饰

  • 动态地给一个对象添加一些额外的职责。就扩展功能来说,装饰模式相比生成子类更为灵活

  • UML图

    image-20210331161857614

  • 使用场景 1、在没有类源码的基础上扩展类。 2、在不改变原有类行为的基础上扩展类。 3、iOS中的类别可以用来实现装饰模式。

责任链

  • 使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间发生耦合。此模式将这些对象连成一条链,并沿着这条链条传递下去,直到有一个对象处理它为止。

  • UML图

    image-20210331161909157

  • 使用场景 iOS中的事件响应链实现了责任链模式。

  • 实用

    img

    • 代码示例:
    @class BusinessObject;
    typedef void(^CompletionBlock)(BOOL handled);
    typedef void(^ResultBlock)(BusinessObject *handler, BOOL handled);
    
    @interface BusinessObject : NSObject
    
    // 下一个响应者(响应链构成的关键)
    @property (nonatomic, strong) BusinessObject *nextBusiness;
    // 响应者的处理方法
    - (void)handle:(ResultBlock)result;
    
    // 各个业务在该方法当中做实际业务处理
    - (void)handleBusiness:(CompletionBlock)completion;
    @end
    
    @implementation BusinessObject
    
    // 责任链入口方法
    - (void)handle:(ResultBlock)result
    {
        CompletionBlock completion = ^(BOOL handled){
            // 当前业务处理掉了,上抛结果
            if (handled) {
                result(self, handled);
            }
            else{
                // 沿着责任链,指派给下一个业务处理
                if (self.nextBusiness) {
                    [self.nextBusiness handle:result];
                }
                else{
                    // 没有业务处理, 上抛
                    result(nil, NO);
                }
            }
        };
        
        // 当前业务进行处理
        [self handleBusiness:completion];
    }
    
    - (void)handleBusiness:(CompletionBlock)completion
    {
        /*
         业务逻辑处理
         如网络请求、本地照片查询等
         */
    }
    
    @end
    

算法封装

模板模式

  • 定义一个操作中算法的骨架,而将一些步骤延迟到子类中。模版方法可以重定义算法的某些特定步骤而不改变改算法的结构。

  • UML图

    image-20210331161919507

  • 使用场景 1、子类的共同行为提取出来放到基类中,以避免代码重复。差异方法定位为模版方法,子类通过重写模版方法来实现差异化。 2、iOS中drawRect为模版方法。NSArray和NSDictionary定义为模版类。

策略模式

  • 定义一系列算法,把它们一个个封装起来,并且使它们可相互替代。本模式使得算法可独立改变,而不影响调用方。

  • UML图

    image-20210331161928507

  • 使用场景 1、使用大量条件语句来定义行为,可以把条件分支移到策略类中。

命令模式

  • 将请求封装成对象,以支持对请求排队、记录请求请求日志,已经支持撤销。

  • UML图

    image-20210331161938326

  • 使用场景 1、NSInvocationk实现了命令模式。 2、NSUndoManager实现了命令模式。 3、Targrt-Action可以实现了命令模式。

  • 实现

  • 代码实例:一个命令对象和一个命令管理者。

    Command

    @class Command;
    typedef void(^CommandCompletionCallBack)(Command* cmd);
    
    @interface Command : NSObject
    @property (nonatomic, copy) CommandCompletionCallBack completion; // 执行回调
    
    - (void)execute; // 执行
    - (void)cancel; // 取消
    
    - (void)done; // 完成
    
    @end
    
    #import "Command.h"
    #import "CommandManager.h"
    @implementation Command
    
    - (void)execute{
        
        //override to subclass;
        
        [self done];
    }
    
    - (void)cancel{
        
        self.completion = nil;
    }
    
    - (void)done
    {
        dispatch_async(dispatch_get_main_queue(), ^{
            
            if (_completion) {
                _completion(self);
            }
            
            //释放
            self.completion = nil;
            // 在数组中移除
            [[CommandManager sharedInstance].arrayCommands removeObject:self];
        });
    }
    
    @end
    

    CommandManager

    #import <Foundation/Foundation.h>
    #import "Command.h"
    @interface CommandManager : NSObject
    // 命令管理容器
    @property (nonatomic, strong) NSMutableArray <Command*> *arrayCommands;
    
    // 命令管理者以单例方式呈现
    + (instancetype)sharedInstance;
    
    // 执行命令
    + (void)executeCommand:(Command *)cmd completion:(CommandCompletionCallBack)completion;
    
    // 取消命令
    + (void)cancelCommand:(Command *)cmd;
    
    @end
    
    #import "CommandManager.h"
    
    @implementation CommandManager
    
    // 命令管理者以单例方式呈现
    + (instancetype)sharedInstance
    {
        static CommandManager *instance = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            instance = [[super allocWithZone:NULL] init];
        });
        return instance;
    }
    
    // 【必不可少】
    + (id)allocWithZone:(struct _NSZone *)zone{
        return [self sharedInstance];
    }
    
    // 【必不可少】
    - (id)copyWithZone:(nullable NSZone *)zone{
        return self;
    }
    
    // 初始化方法
    - (id)init
    {
        self = [super init];
        if (self) {
            // 初始化命令容器
            _arrayCommands = [NSMutableArray array];
        }
        return self;
    }
    
    + (void)executeCommand:(Command *)cmd completion:(CommandCompletionCallBack)completion
    {
        if (cmd) {
            // 如果命令正在执行不做处理,否则添加并执行命令
            if (![self _isExecutingCommand:cmd]) {
                // 添加到命令容器当中
                [[[self sharedInstance] arrayCommands] addObject:cmd];
                // 设置命令执行完成的回调
                cmd.completion = completion;
                //执行命令
                [cmd execute];
            }
        }
    }
    
    // 取消命令
    + (void)cancelCommand:(Command *)cmd
    {
        if (cmd) {
            // 从命令容器当中移除
            [[[self sharedInstance] arrayCommands] removeObject:cmd];
            // 取消命令执行
            [cmd cancel];
        }
    }
    
    // 判断当前命令是否正在执行
    + (BOOL)_isExecutingCommand:(Command *)cmd
    {
        if (cmd) {
            NSArray *cmds = [[self sharedInstance] arrayCommands];
            for (Command *aCmd in cmds) {
                // 当前命令正在执行
                if (cmd == aCmd) {
                    return YES;
                }
            }
        }
        return NO;
    }
    @end
    

    命令模式的优点

    • 降低系统的耦合度。

    • 新的命令可以很容易地加入到系统中。

    • 可以比较容易地设计一个命令队列和宏命令(组合命令)。

    • 可以方便地实现对请求的Undo和Redo。

性能与对象访问

享元

  • 运用共享技术有效地支持大量细粒度的对象。

  • UML图

    image-20210331161947781

  • 使用场景 线程池、内存池、使用了享元模式。

代理

  • 为其他对象提供一种代理以控制对这个对象的访问。

  • UML图

    image-20210331161956839

  • 使用场景 1、NSProxy实现了代理模式 2、 通过懒加载实现代理模式。

对象状态

备忘录

  • 在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象外保存这个状态,以便后续恢复成这个状态。

  • UML图![image-20210331162258432](/Users/mac/Library/Application Support/typora-user-images/image-20210331162258432.png)

  • 使用场景 1、文档的归档可以采用备忘录模式。 2、iOS中的Archive采用了备忘录模式。通过NSCoding实现对象的归档和反归档。。 3、plist存储、coredata也采用了备忘录模式

框架

MVC

MVC定义

MVC模式(Model–View–Controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。 在iOS中,MVC通信模式如下所示:

image-20210331163046320

  • VC持有Model和View,Model和View不直接交互。
  • 当用户点击View时,通知VC,需要更新Model时由VC来更新Model。
  • 当Model发生变化时候,利用KVO等技术通知VC,由VC来更新View。

Massive ViewController

如果我们把所有的业务逻辑都写在VC里面,不做好拆分,很容易造成VC臃肿。这个时候需要为VC瘦身,而MVVM就是为了解决VC过于庞大引入的设计模式。

怎么为VC瘦身

1、一个干净的VC应该只做如下事:

  • 在初始化时,构造相应的 View 和 Model。
  • 监听 Model 层的事件,将 Model 层的数据传递到 View 层。
  • 监听 View 层的事件,并且将 View 层的事件转发到 Model 层。
  • 其他的事情可以由若干个Manager来完成。

2、那么在iOS中,我们有那些常用的瘦身手段呢?

  • 网络层。定义网络请求类 ,一个网络请求代表一个类。网络请求类负责发网络请求以及把响应数据解析为model,model以及response通过block方式回调给调用方。这里的调用方大部分时候是VC。
  • 数据存储层。定义数据存储Manager,用来加载数据和缓存数据。
  • 定义数据转换层。负责数据转换,比如Model跟View Model的转换。
  • 可以通过类别的方式给VC做好功能的划分,比如定义TableView分类用来处理UITableViewDelegate && UITableViewDataSource代理方法;定义Network分类用来处理网络逻辑。

推荐大家每个业务模块至少建立Model、View、VC、Network、Cache五个文件夹来分类代码,并且遵循上面原则来给VC瘦身。

MVVM

MVVM定义

image-20210331163205394

  • View、VC可以持有View Model,反之不行;View Model可以持有Model,反之不行。 View Model相当于MVC中的VC作用,用来连接View、VC 和Model。
  • 当Model更新时,通知View Model,View Model再通知VC或者View,来更新View;
  • 当用户点击View时候,通知View Model,View Model通知Model来更新Model。

RN的数据流思想

img

  • 系统UIView更新机制的思想
  • FaceBook的开源框架AsyncDisplayKit关于预排版的设计思想

应用

实现MVVM的关键是如何做数据通知,在iOS中可以用KVO来实现。业内一般用ReactiveCocoa来实现数据绑定及通知。

选择MVC还是MVVM?

无论是MVC还是MVVM,我们的本质都是想对VC进行瘦身。 用MVVM的话,分层更加清晰,不过要引入ReactiveCocoa,ReactiveCocoa比较重,学习成本比较高,最重要的是用的是block,调试起来比较麻烦,目前业内用得不是特别多。 笔者推荐用MVC,按照上面介绍的VC瘦身方案来使用,这样轻量点。

  • 系统UIView更新机制的思想
  • FaceBook的开源框架AsyncDisplayKit关于预排版的设计思想

组件化

客户端整体架构

img

组件化背景

当项目因为各种需求,越来越来时,如果此时的各个模块之间是互相调用,即你中有我,我中有你这种情况时,会造成高耦合的情况。一旦我们需要对某一块代码进行修改时,就会牵一发而动全身,导致项目难以维护。

其问题主要体现在以下几个方面:

  • 修改某个功能时,同时需要修改其他模块的代码,因为在其他模块中有该模块的引用。可以理解为高耦合导致代码修改困难
  • 模块对外接口不明确,甚至暴露了本不该暴露的私有接口,修改时费时费力。可以理解为接口不固定导致的接口混乱
  • 高耦合代码产生的后果就是会影响团队其他成员的开发,产生代码冲突
  • 当模块需要重用到其他项目时,难以单独抽离
  • 模块间耦合的忌口导致接口和依赖关系混乱,无法进行单元测试

所以为了解决以上问题,我们需要采用更规范的方式来降低模块间的耦合度,这就是组件化,也可以理解为模块化

但是,这里还需要说明一点,因为组件化也是需要一定成本的,需要花费时间设计接口、分离代码等,所以并不是所有的项目都需要组件化。如果你的项目有以下这些特征就不需要组件化

  • 项目较小,模块间交互简单,耦合少
  • 项目没有被多个外部模块引用,只是一个单独的小模块
  • 模块不需要重用,代码也很少被修改
  • 团队规模很小
  • 不需要编写单元测试

如果你的有以下特性,说明你就必须要考虑进行组件化了:

  • 模块逻辑复杂,多个模块之间频繁互相引用
  • 项目规模逐渐变大,修改代码变的越来越困难(这里可以理解为:修改一处代码,需要同时修改其他多个地方)
  • 团队人数变多,提交的代码经常和其他成员冲突
  • 项目编译耗时较大
  • 模块的单元测试经常由于其他模块的修改而失败

组件化作用

  • 模块间解耦
  • 模块重用
  • 提高团队协作开发效率
  • 单元测试

方案

组件化方案的8条指标

一个项目经过组件化后如何来评判,主要有以下几个标准

  • 模块之间没有耦合,模块内部的修改不会应该其他模块
  • 模块可以单独编译
  • 模块间数据传递明确
  • 模块可以随时被另一个提供了相同功能的模块替换
  • 模块对外接口清晰且易维护
  • 当模块接口改变时,此模块的外部代码能够被高效重构
  • 尽量用最少的修改和代码,让现有的项目实现模块化
  • 持OC和Swift,以及混编

组件化方案 常用的组件化方案主要有两种:

  • 本地组件化:主要是通过在工程中创建library,利用cocoapodsworkspec进行本地管理,不需要将项目上传git,而是直接在本项目中以framework的方法进行调用
  • cocoapods组件化:主要是利用cocoapods来进行模块的远程管理,需要将项目上传git(需要注意:这里的组件化模块分为公有库私有库,对公司而言,一般是私有库)image-20210402103128860

MGJRouter

MGJRouter通过注册url的方式来实现方法注册和调用 1、组件提供方通过registerURLPattern注册方法。

[MGJRouter registerURLPattern:@"mgj://category/travel" toHandler:^(NSDictionary *routerParameters) {
    NSLog(@"routerParameters[MGJRouterParameterUserInfo]:%@", routerParameters[MGJRouterParameterUserInfo]);
    // @{@"user_id": @1900}
}];

2、调用方通过openURL调用组件提供的方法。

[MGJRouter openURL:@"mgj://category/travel" withUserInfo:@{@"user_id": @1900} completion:nil];

缺点

组件每提供一个方法,需要提供一个url,组件多的话url肯定会很多,需要有一个地方统一管理,比较难维护。 参数传入不能直接传model,而是需要传字典,如果方法实现方修改一个字段的类型但没有通知调用方,调用方无法直接知道,有可能导致崩溃。 通过字典传参不直观,调用方需要知道字段的名字才能获取字段值,如果字段名不定义为宏,到处拷贝字段名造成难以维护。

CTMediator

CTMediator通过CTMediator的类别来实现方法调用。 1、组件提供方实现Target、Action。

@interface Target_A : NSObject

	(UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params;

@end

(UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params
{
  // 因为action是从属于ModuleA的,所以action直接可以使用ModuleA里的所有声明
  	DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
  	viewController.valueLabel.text = params[@"key"];
  	return viewController;
}

2、组件提供方实现CTMediator类别暴露接口给使用方。

@interface CTMediator (CTMediatorModuleAActions)

	(UIViewController *)CTMediator_viewControllerForDetail;

@end

(UIViewController *)CTMediator_viewControllerForDetail
{
    UIViewController *viewController = [self performTarget:kCTMediatorTargetA action:kCTMediatorActionNativFetchDetailViewController
                                                    params:@{@"key":@"value"}
                                         shouldCacheTarget:NO
                                        ];
    if ([viewController isKindOfClass:[UIViewController class]]) {
        // view controller 交付出去之后,可以由外界选择是push还是present
        return viewController;
    } else {
        // 这里处理异常场景,具体如何处理取决于产品
        return [[UIViewController alloc] init];
    }
}

3、组件调用方通过引入CTMediator (CTMediatorModuleAActions)来调用组件的接口

#import "CTMediator +CTMediatorModuleAActions.h"
UIViewController* vc = [[CTMediator sharedInstance] CTMediator_viewControllerForDetail];

优缺点

此种方案的优点是通过Targrt-Action实现了组件之间的解耦,通过暴露方法给组件使用方,避免了url直接传递字典带来的问题。 缺点是:

CTMediator类别实现由于需要通过performTarget方式来实现,需要写一堆方法名、方法参数名字字符串,影响阅读,难以维护; 没有组件管理器概念。组件之间的互相调用都是通过直接引用CTMediator类别来实现,没有实现真正的解耦;并且类别暴露了方法的具体实现

BeeHive

BeeHive通过url来实现页面路由,通过Protocol来实现方法调用。

一、组件提供方注册service

[[BeeHive shareInstance] registerService:@protocol(HomeServiceProtocol) service:[BHViewController class]];

二、组件调用方调用service

id< HomeServiceProtocol > homeVc = [[BeeHive shareInstance] createService:@protocol(HomeServiceProtocol)];

// use homeVc do invocation

笔者强烈推荐使用BeeHive这种方式来做组件化,基于Protocol(面向接口)来实现组件化有如下优点:

  • 通过提供接口给调用方,向调用方隐藏了实现,后面如果实现改变了,对调用方也是透明的。
  • 能让组件提供方清晰地提供接口声明给使用方。
  • 能充分利用编辑器特性,比如如果接口删除了一个参数,能通过编译器编不过来告诉调用方接口发生了变化。

组件管理与发布

可以采用私有pod库来管理。笔者采用的是私有远程pod+本地pod结合来管理的

框架设计

图片缓存

  • 怎样设计一个图片缓存框架?

img

  • 图片通过什么方式进行读写,过程是怎样的?

    • 以图片URL的单向Hash值作为Key

    img

  • 内存的设计上需要考虑哪些问题?

    • 存储size

    img

    • 淘汰策略

      • 以队列先进先出的方式淘汰

      img

      • LRU算法(如30分钟之内是否使用过)

      img

  • 磁盘设计需要考虑哪些问题?

    • 存储方式
    • 大小限制(如100MB)
    • 淘汰策略(如某一图存储事件距今已超过7天)
  • 网络部分的设计需要考虑哪些问题?

    • 图片请求最大并发量
    • 请求超时策略
    • 请求优先级
  • 对于不同格式的图片,解码采用什么方式来做?

    • 应用策略模式对不同图片格式进行解码
    • 在磁盘读取后,网络请求返回后,进行图片解码处理。
  • 线程处理

img

阅读时长统计

  • 怎样设计一个时长统计框架?

img

  • 为何要有不同类型的记录器,你的考虑是什么?
    • 基于不同分类场景提供的关于记录的封装、适配。
  • 记录的数据会由于某种原因丢失,你是怎样处理的?
    • 定时写磁盘
    • 限定内存缓存条数(如10跳),超过该条数,即写磁盘
  • 关于延时上传的具体场景有哪些?
    • 前后台切换
    • 从无望了到有网的变化
    • 通用轻量接口捎带
  • 上传时机是怎样把我的?
    • 立即上传
    • 延时上传
    • 定时上传