从CocoaLumberjack的高效易扩展谈为什么要学习设计模式

原文链接: http://blog.cocosdever.com/2019/07/25/talking-about-design-patterns-from-cocoaLumberjack

文档更新说明

  • 最后更新 2019年07月25日
  • 首次更新 2019年07月25日

前言

在编程领域里, 听的多做得少的就是设计模式. 很多程序员都听说过设计模式, 但是却很少自己手动实现一些真正意义上的设计模式, 这几天刚好在复习设计模式, 然后今天又看了iOS上流行的以灵活,扩展性高著称的开源日志框架CocoaLumberjack的源码, 有感而发, 下面我想好好谈谈心得, 一步一步揭开这个高度灵活的框架是如何设计的.

CocoaLumberjack的使用

先来简单看一下CocoaLumberjack的使用. CocoaLumberjack为用户提供了几种日志模式, 有控制台, 系统日志, 沙盒日志, 此外还有日志不可或缺的格式化功能Formatter. 下面例子以最复杂的沙盒日志为例:

// 自己需要定义这个变量, 确定实际上需要的日志级别.
static const DDLogLevel ddLogLevel = DDLogLevelDebug;

// 先创建一个文件日志, 定义好相关配置信息
DDFileLogger *fileLogger = [[DDFileLogger alloc] init]; // File Logger
fileLogger.logFormatter = [[DDDispatchQueueLogFormatter alloc] init];
    
fileLogger.rollingFrequency = 60 * 60 * 24; // 24 hour rolling
fileLogger.logFileManager.maximumNumberOfLogFiles = 7;

// 添加一个logger
[DDLog addLogger:fileLogger];

// 可以同时加入多个, 比如再加入一个控制台logger, 这样文件和控制台都会有日志信息输出
DDTTYLogger *log2 = [DDTTYLogger sharedInstance];
log2.logFormatter = [[CCLogFormatter alloc] init];
[DDLog addLogger:log2];

// 直接使用内置的宏即可开始记录日志
DDLogDebug(@"Debug");

上面简单的代码,就完成了日志格式的定制, 日志的输出定制, 日志的沙盒文件配置等功能, 灵活设置. 此外还演示了用户自行扩展的格式化类, 用于自行格式化输出内容, 像这样灵活的设计, 确实很值得赞赏, 下面一起来看看源码, 这是如何实现.

CocoaLumberjack源码分析

展开宏之后得到下面的真实执行入口, 下面主要分析沙盒日志源码

[DDLog
         log:NO
       level:ddLogLevel
        flag:DDLogFlagDebug
     context:0
        file:__FILE__
    function:__FUNCTION__
        line:__LINE__
         tag:nil
      format:@"Debug"
];

上面这个类方法, 一层一层调用下去, 直到下面代码:

// DDLog.m

// 将参数封装成
[self.sharedInstance log:asynchronous message:message level:level flag:flag context:context file:file function:function line:line tag:tag];

这是遇到的第一个设计模式, 也是最常见的, 单例, 没什么好说的, 接着往下执行:

// DDLog.m
// 程序开始调用下面方法, 进入初始化完成后的阶段

- (void)queueLogMessage:(DDLogMessage *)logMessage asynchronously:(BOOL)asyncFlag {
     ///省略其他代码
    [self lt_log:logMessage];
}

程序执行了lt_log方法, 把消息传进去, 接着往下执行:

// DDLog.m

- (void)lt_log:(DDLogMessage *)logMessage {
    ///省略其他代码
    for (DDLoggerNode *loggerNode in self._loggers) {
        // skip the loggers that shouldn't write this message based on the log level
        // 从当前的DDLog实例里取出所有存入的logger节点, 里面就有我传入的logger对象
        if (!(logMessage->_flag & loggerNode->_level)) {
            continue;
        }
    
        dispatch_group_async(_loggingGroup, loggerNode->_loggerQueue, ^{ @autoreleasepool {
            [loggerNode->_logger logMessage:logMessage];
        } });
    }
}

上面代码, 注意需要注意的是 _logger的类型是id <DDLogger>, 这就是我要讲的第二种编程模式, 针对抽象编程, 也就是代码只关心对象实现了那些接口, 只要实现了规定的接口, 下面就可以安全调用接口即可, 不关心对象具体的类型. 继续往下执行:

// DDFileLogger.m

// 这里分析的Logger是沙盒日志
- (void)logMessage:(DDLogMessage *)logMessage {
    /// 省略其他代码
    // 这里可以看到框架开始使用我传入的Formatter对象了
    if (_logFormatter != nil) {
        message = [_logFormatter formatLogMessage:logMessage];
        isFormatted = message != logMessage->_message;
    }
    [self lt_logData:[message dataUsingEncoding:NSUTF8StringEncoding]];
}

上面代码, 需要注意的地方是_logFormatter这个变量的类型是id <DDLogFormatter>, 和上一步提到的原理一样, 这里其实也是第三种设计模式: 桥接模式(有点像策略模式, 后面解释).
框架最后执行到了lt_logData:方法后, 才开始真正写日志.通过上面的分析, 可以知道我传入的Logger和Formatter是如何起作用的, 也可以看出来框架底层采用接口编程这个套路, 兼容了传入的任何适合的Logger和Formatter, 让用户可以灵活扩展自己的日志需求.

桥接模式

写到这里我其实也不管100%告诉你CocoaLumberjack主要利用桥接模式实现可扩展高度灵活的功能, 因为有好几种设计模式都是类似这样的, 我也不是写论文似的分析, 姑且就叫桥接模式吧.
  实际上桥接模式和策略模式是有点像, 策略模式更多的应该是表示行为算法, 本身实现了某个策略的类被聚合进主类中时, 更多的是提供简单的无状态算法功能. 而CocoaLumberjack框架可以看到, 像Formatter和Logger这些类本身是包含比较复杂的逻辑和内部对象属性, 所以我觉得更适合把他们叫做桥接模式.
  说了这么多, 那什么是桥接模式呢? 具体定义可以自行谷歌搜索一下, 他长得就是文章上面分析的那样, 这里我引用一下菜鸟教程里面的关于桥接模式的表述, 我觉得很合适.
  学习了桥接模式的设计方法之后, 再看CocoaLumberjack框架的源码, 就会发现, 好像是这么回事, 很精彩. 看懂了源码之后, 下面就来自行定制一下输出格式, 这整个过程几乎不需要上网查任何资料, 自给自足即可.

扩展CocoaLumberjack

通过上面分析可知, 只要我的类实现了DDLogFormatter协议, 就可以作为一个合法的Formatter对象做为DDLog的配置. 下面看看扩展的Formatter源码:


//
//  CCLogFormatter.h
//  OCSimpleView
//
//  自定义格式化信息, 用于CocoaLumberjack框架
//  Created by Cocos on 2019/7/25.
//  Copyright © 2019 Cocos. All rights reserved.
//

#import <Foundation/Foundation.h>
#import <CocoaLumberjack/CocoaLumberjack.h>

NS_ASSUME_NONNULL_BEGIN

@interface CCLogFormatter : NSObject <DDLogFormatter>

/**
 *  Default initializer
 */
- (instancetype)init;

/**
 *  Designated initializer, requires a date formatter
 */
- (instancetype)initWithDateFormatter:(NSDateFormatter * __nullable)dateFormatter NS_DESIGNATED_INITIALIZER;

@end

NS_ASSUME_NONNULL_END

//
//  CCLogFormatter.m
//  OCSimpleView
//
//  Created by Cocos on 2019/7/25.
//  Copyright © 2019 Cocos. All rights reserved.
//

#import "CCLogFormatter.h"


@interface CCLogFormatter () {
    NSDateFormatter *_dateFormatter;
}

@end

@implementation CCLogFormatter


- (instancetype)init {
    return [self initWithDateFormatter:nil];
}

- (instancetype)initWithDateFormatter:(NSDateFormatter * __nullable)aDateFormatter {
    if ((self = [super init])) {
        if (aDateFormatter) {
            _dateFormatter = aDateFormatter;
        } else {
            _dateFormatter = [[NSDateFormatter alloc] init];
            [_dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4]; // 10.4+ style
            [_dateFormatter setDateFormat:@"yyyy/MM/dd HH:mm:ss:SSS"];
        }
    }
    
    return self;
}

- (NSString * _Nullable)formatLogMessage:(nonnull DDLogMessage *)logMessage {
    NSString *dateAndTime = [_dateFormatter stringFromDate:(logMessage->_timestamp)];
    return [[NSString alloc] initWithFormat:@"[%@]\n%@:\n[%@]%@", logMessage.file, logMessage.function, dateAndTime, logMessage.message];
}

@end

其实还是挺简单的, 我实现了formatLogMessage:这个方法, 其他的代码直接从DDLogFileFormatterDefault类抄过来, 当然这里直接让CCLogFormatter继承DDLogFileFormatterDefault的话这里初始化代码就可以省略了. 在CCLogFormatter里我让这个格式从原来:

2019/07/25 14:33:36:239  Debug

换成了如下格式:

[~/Xcode/Study/OCSimpleView/OCSimpleView/ViewController.m]
-[ViewController loggerFunc]:
[2019/07/25 14:58:07:358]Debug

这样扩展就结束了. 其他更多复杂的功能, 百变不离其宗, 都是这个套路.

总结

这篇文章主要是通过对热门框架CocoaLumberjack源码的分析, 找出其中的设计模式, 进而了解设计模式的重要性, 学会灵活扩展框架的实现方法之一. 还有很多设计模式, 传说一共有30多种, 实在是太多了, 学不完, 但是保持一颗年轻的心, 活到老学到老, 我觉得是最重要的 : )

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,233评论 6 495
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,357评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,831评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,313评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,417评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,470评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,482评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,265评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,708评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,997评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,176评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,503评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,150评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,391评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,034评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,063评论 2 352

推荐阅读更多精彩内容