Objective-C实现链式编程语法(DSL)

2023-11-08

您越着急开始写代码,代码就会花费越长的时间。 - Carlson, University of Wisconsin

前言

熟悉Objective-C这一门编程语言的人都知道,Objective-C中方法的调用都是通过中括号[]实现的。比如[self.view addSubview:xxxView];如果想要在一个对象上连续调用多个方法,就要使用多组中括号嵌套(当然要保证每个方法都能把该对象作为返回值return)。比如[[[UILabel alloc] init] setText:@”xxx”];。

这对于有其他编程语言经验的开发者而言,Objective-C无异于就是众多语言中的一朵奇葩。因为其他多数的高级语言方法调用都是以点语法.的形式实现的。好在Objective-C在iOS4.0之后推出了block这个语法(相当于其他语言中的匿名函数)。我们可以利用block的来实现Objective-C方法的链式调用。像这种用于特定领域的表达方式,我们叫做 DSL (Domain Specific Language),本文就介绍一下如何让Objective-C实现链式调用,其最终调用方式如下:

DSLObject *obj = DSLObject.new.name(@"ws").age(27).address(@"beijing");

很明显,相比较传统的Objective-C的方法调用方式,使用点语法进行方法调用更加简洁连贯、一气呵成。

不难看出,这种点语法连续调用的方式,需要保证每次调用都能返回对象本身,这样链式调用才得以继续,并且在必要的时候还可以传入参数,比如上例中的“ws”、“27”、“beijing”。

而至于为什么使用block来实现DSL链式调用语法?正是因为block完全符合构造链式调用的要求:既可以接收参数,又可以有返回值。

不喜欢读文章的可以直接看代码

链式调用的实现

现在要给系统原生的类增扩展链式调用语法。比如给UIView的frame、backgroundColor增加链式调用,目前能想到的有以下两种实现方式。

第一种方式是使用category给UIView类扩展一些方法,每个方法的返回值都是一个block,block的参数是要给UIView对象的属性设置的值(比如frame),block的返回值是一个UIView对象。block接收到传入的参数后,会对view对象的响应属性进行赋值,然后把view对象作为返回值返回。开发者想使用链式调用,必须要调用category中的方法。

第二种方式是为我们要支持链式调用的系统类(比如UIView类)增加一个中间类(比如叫做DSLViewMaker),DSLViewMaker对象内部持有一个UIView对象,然后DSLViewMaker会声明并实现一些和UIView同名的方法。和方式一一样,每个方法的返回值也是一个block,block的参数是要给UIView对象的属性设置的值,block的返回值是这个UIView对象**。然后在合适的时候把这个view对象返回给调用者。

下面针对于两种实现方式分别说明。

category方式实现

/// category 头文件
@interface UIView (DSL)
- (UIView* (^)(CGRect))DSL_frame;
- (UIView* (^)(UIColor *))DSL_backgroundColor;
@end
/// category 实现文件
#import "UIView+DSL.h"
#define weak_Self __weak typeof(self) weakSelf = self
#define strong_Self __strong typeof((weakSelf)) strongSelf = (weakSelf)

@implementation UIView (DSL)
- (UIView *(^)(CGRect))DSL_frame {
    weak_Self;
    return ^UIView* (CGRect frame) {
        strong_Self;
        strongSelf.frame = frame;
        return strongSelf;
    };
}

- (UIView *(^)(UIColor *))DSL_backgroundColor {
    weak_Self;
    return ^UIView* (UIColor *backgroundColor) {
        strong_Self;
        strongSelf.backgroundColor = backgroundColor;
        return strongSelf;
    };
}
@end
/// 客户端调用
/// 客户端调用category指定的带有“DSL_”前缀的方法
UIView *view = UIView.new.DSL_frame(CGRectMake(0, 0, 100, 250)).DSL_backgroundColor([UIColor orangeColor]);

那么问题来了,现在要给UIImageView的一些方法和属性增加DSL的链式调用语法。因为UIImageView继承自UIView,这就代表UIImageView还要拥有UIView的DSL_frame方法和DSL_backgroundColor方法。经过简单的实现,大致如下:

/// UIImageView category的头文件
@interface UIImageView (DSL)

- (UIImageView* (^)(UIImage *))DSL_image;
- (UIImageView* (^)(UIImage *))DSL_HighlightedImage;
- (UIImageView* (^)(BOOL))DSL_UserInteractionEnabled;
- (UIImageView* (^)(BOOL))DSL_highlighted;
- (UIImageView* (^)(NSArray <UIImage *> *))DSL_AnimationImages;
- (UIImageView* (^)(NSArray <UIImage *> *))DSL_HighlightedAnimationImages;
- (UIImageView* (^)(NSTimeInterval))DSL_AnimationDuration;
- (UIImageView* (^)(NSInteger))SDL_AnimationRepeatCount;
- (UIImageView* (^)(UIColor *))DSL_TintColor;

@end
#import "UIImageView+DSL.h"
#define weak_Self __weak typeof(self) weakSelf = self
#define strong_Self __strong typeof((weakSelf)) strongSelf = (weakSelf)

@implementation UIImageView (DSL)
- (UIImageView* (^)(UIImage *))DSL_image {
    weak_Self;
    return ^UIImageView *(UIImage *image) {
        strong_Self;
        strongSelf.image = image;
        return strongSelf;
    };
}
- (UIImageView* (^)(UIImage *))DSL_HighlightedImage {
    weak_Self;
    return ^UIImageView *(UIImage *highlightedImage) {
        strong_Self;
        strongSelf.highlightedImage = highlightedImage;
        return self;
    };
}
- (UIImageView* (^)(BOOL))DSL_UserInteractionEnabled {
    weak_Self;
    return ^UIImageView *(BOOL userInteractionEnabled) {
        strong_Self;
        strongSelf.userInteractionEnabled = userInteractionEnabled;
        return strongSelf;
    };
}

/// 此处省略...,请自行脑补...

@end
/// 客户端调用
UIImageView *imageView = UIImageView.new
.DSL_frame(CGRectMake(100, 100, 100, 60))
.DSL_image([UIImage imageNamed:@"imgxxx"]);

基于以上代码,然后进行编译,编译器会报以下错误:

这里写图片描述

DSL_image这个东西在UIView中找不到,为什么是UIView呢?明明我们创建的是一个UIImageView。原因很简单,因为我们的DSL_frame是在UIView的category中声明并实现的,更要命的是,UIView(DSL)中声明的DSL_frame这个方法返回的block的返回值是一个UIView对象,UIView对象当然没有DSL_image方法。当DSL_frame返回的block返回了一个UIView类型的对象后,这个imageView就会被当成UIView使用,后面所有对UIImageView的方法的调用都不会成功,UIView(DSL)声明的方法如下:

- (UIView* (^)(CGRect))DSL_frame;,

针对于这个问题,目前笔者只想到一种解决方法:把在UIView(DSL)中声明的方法拷贝一份到UIImageView(DSL).h中,并修改block的返回值类型为UIImageView。最终的UIImageView(DSL)头文件 如下:

@interface UIImageView (DSL)
#pragma mark - UIView
/// 这些是在UIView(DSL)中拷贝过来的方法,不同的是,需要修改block的返回值类型为UIImageView,而不是原来的UIView,如下所示:
- (UIImageView* (^)(CGRect))DSL_frame;
- (UIImageView* (^)(UIColor *))DSL_backgroundColor;

#pragma mark - UIImageView
- (UIImageView* (^)(UIImage *))DSL_image;
- (UIImageView* (^)(UIImage *))DSL_HighlightedImage;
- (UIImageView* (^)(BOOL))DSL_UserInteractionEnabled;
- (UIImageView* (^)(BOOL))DSL_highlighted;
- (UIImageView* (^)(NSArray <UIImage *> *))DSL_AnimationImages;
- (UIImageView* (^)(NSArray <UIImage *> *))DSL_HighlightedAnimationImages;
- (UIImageView* (^)(NSTimeInterval))DSL_AnimationDuration;
- (UIImageView* (^)(NSInteger))SDL_AnimationRepeatCount;
- (UIImageView* (^)(UIColor *))DSL_TintColor;
@end

而UIImageView(DSL).m实现文件中不需要再实现DSL_frame和DSL_backgroundColor这两个方法,因为已经在UIView(DSL).m中实现过。只需要消除对应的警告即可。

综上,通过category的方式实现链式调用好处在于每次调用都会返回对象本身,缺点在于category中的方法不能和系统的方法重名,因此笔者在这里使用了一个前缀DSL_来进行区分。而中间类方式实现链式调用就可以避免前缀的问题。

中间类方式实现

上面已经说过,使用category的方式给类扩展链式调用的方法,我们必须要和原生的方法进行区分(比如增加前缀)。这样的缺点在于开发者开发者链式调用的时候还必须要时刻谨记调用指定前缀的方法,使用起来不是很友好。

所以,还有另一种方法,我们可以使用一个中间类,中间类持有一个UIView对象,给这个中间类增加和UIView同名的方法,通过调用这个中间类的方法来间接调用UIView对象的方法。具体实现如下:

/// DSLViewMaker.h文件

@interface DSLViewMaker : NSObject
DSLViewMaker *alloc_view(void);

/// 一些和UIView同名的方法
- (DSLViewMaker *(^)(CGRect))frame;
- (DSLViewMaker *(^)(UIColor *))backgroundColor;
/// 返回DSLViewMaker配置的对象
- (id)view;

@end
/// DSLViewMaker.m文件

#import "DSLViewMaker.h"
#define weak_Self __weak typeof(self) weakSelf = self
#define strong_Self __strong typeof((weakSelf)) strongSelf = (weakSelf)

@interface DSLViewMaker()
@property(nonatomic, strong) UIView *view;
@end

DSLViewMaker *alloc_view(void) {
    return DSLViewMaker.new;
}

@implementation DSLViewMaker
- (instancetype)init {
    if (self = [super init]) {
        _view = [UIView new];
    }
    return self;
}

- (DSLViewMaker *(^)(CGRect))frame {
    weak_Self;
    return ^DSLViewMaker *(CGRect frame) {
        strong_Self;
        strongSelf.view.frame = frame;
        return strongSelf;
    };
}

- (DSLViewMaker *(^)(UIColor *))backgroundColor {
    weak_Self;
    return ^DSLViewMaker *(UIColor *backgroundColor) {
        strong_Self;
        strongSelf.view.backgroundColor = backgroundColor;
        return strongSelf;
    };
}

- (id)view {
    return _view;
}
@end
/// 客户端调用
    UIView *view = alloc_view().frame(CGRectMake(0, 20, 100, 100)).backgroundColor([UIColor redColor]).view;
    [self.view addSubview:view];

看完上面的代码,你可能会有几个疑惑:

  1. 为什么客户端进行链式调用是以一个函数开头的?
  2. 为什么最后要使用一个.view来返回我们创建的view?

针对于第一个问题,我们是以一个中间类DSLViewMaker来创建了一个view,然后链式调用DSLViewMaker的对象方法对这个view进行配置。为了不让外部调用的客户端感知到DSLViewMaker的存在,所有使用了一个函数直接返回一个DSLViewMaker对象。

针对于第二个问题,还是因为中间类,因为链式调用要保证每次都要返回链式调用的对象(这里是指的maker对象),而客户端无法拿到maker配置好的view,为了让客户端能够获取链式调用配置好的view对象,所以暴露了一个view方法供外部调用。

如果你觉得使用函数作为链式调用的开头不够面向对象。那么还可以给UIView增加一个如下分类:

/// category头文件
#import <UIKit/UIKit.h>

@class DSLViewMaker;

@interface UIView (DSLMaker)
+ (DSLViewMaker *)make;
@end

/// category实现文件
#import "UIView+DSLMaker.h"
#import "DSLViewMaker.h"

@implementation UIView (DSLMaker)
+ (DSLViewMaker *)make {
    return [DSLViewMaker new];
}

@end

然后客户端的调用就变成了这样:

//    UIView *view = alloc_view().frame(CGRectMake(0, 20, 100, 100)).backgroundColor([UIColor redColor]).view;
//    [self.view addSubview:view];

UIView *view = UIView.make.frame(CGRectMake(0, 20, 100, 100)).backgroundColor([UIColor redColor]).view;
[self.view addSubview:view];

总结

综上,Objective-C语言实现链式语法可以有两种形式,但最终都是使用block实现的。使用category实现链式语法,需要加前缀。使用中间类来实现链式语法,需要有一个特定的方法返回被配置的对象。两种方式各有利弊。

最后附上代码地址

作者:VV木公子

链接:https://www.jianshu.com/p/82012265e882

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Objective-C实现链式编程语法(DSL) 的相关文章

随机推荐

  • 抓取第三方网站数据

    最近需要把某网站的统计数据聚合到我们自己的系统里 但是该网站没有提供标准API 所以就尝试自己抓取了一下 本文总结一下一般的方法 分析服务地址 通常网站有2种做法 一种是后端渲染 直接把渲染后的完整界面呈现在浏览器 另一种前端是静态页面 通
  • mybatis plus中的${ew.sqlSegment},${ew.sqlSelect},${ew.customSqlSegment},${ew.sqlSet}使用

    ew是mapper方法里的 Param Constants WRAPPER Wrapper queryWrapper对象 首先判断ew emptyOfWhere是否存在where条件 有的话再拼接上去 ew customSqlSegment
  • java毕业设计——基于JSP+JavaBean+sqlserver的在线购物系统设计与实现(毕业论文+程序源码)——在线购物系统

    基于JSP JavaBean sqlserver的在线购物系统设计与实现 毕业论文 程序源码 大家好 今天给大家介绍基于JSP JavaBean sqlserver的在线购物系统设计与实现 文章末尾附有本毕业设计的论文和源码下载地址哦 需要
  • 品味树莓派:GPIO Zero库使用入门

    文章目录 目的 基础说明 入门使用 LED PWMLED Button 更多入门例程 类基础说明 注意事项 总结 目的 树莓派有很多GPIO口可供用户使用 官方同时也提供了一些方式来操作这些IO口 其中目前主要推荐的是基于Python的GP
  • 1、什么是Shader

    什么是Shader Shader 中文名为着色器 Shader其实就是专门用来渲染图形的一种技术 通过shader 我们可以自定义显卡渲染画面的算法 使画面达到我们想要的效果 小到每一个像素点 大到整个屏幕 Shader分为两类 顶点Sha
  • 使用Gradle构建SpringBoot工程系列:第八篇:使用spring-data-jpa 实现数据持久化

    本篇文章是SpringBoot 系列文章的第八篇文章 由于本人工作原因 中断了一段时间 接下来的一段时间里 将陆续更新本系列的其他文章 回归Spring Boot技术体系 记录本人学习和使用Gradle构建spring Boot工程的过程
  • C/C++蓝桥杯三升序列

    本题答案可能是错的 我算出的是180414 和很多博主的答案不一样 我也不太懂哪里有问题 大家可以探讨一下 题目描述 对于一个字母矩阵 我们称矩阵中的一个三升序列是指在矩阵中找到三个字母 它们在同一行 同一列 或者在同一45 度的斜线上 这
  • OpenFlow流表_时间因素

    目标 现有拓扑结构如下的网络结构 s1 s4为交换机 h1 h9为主机 现欲让h1和h2白天ping不通 晚上ping的通 拓扑结构 s1
  • es6查找指定字符下标,并把第一个字符前面的内容删掉

    前言 用es6实现查找指定字符下标 并把第一个字符前面的内容删掉 原字符串 abc 123 bcd 444 目标字符 123 bcd 444 实现方法 let abc abc 123 bcd 444 let mmm abc indexOf
  • springcloud与springboot版本列表查看及依赖关系查看

    一 版本规则说明 1 springboot版本 Spring Boot 2 2 5 RELEASE表示主版本 次版本 增量版本 Bug修复 主要 版本中的第一个数字 2 和 3 是 Python 的著名 主要版本 主要部分是基于日历的最常见
  • (华硕)电脑第一次开机蓝屏,二次开机启动

    华硕主板 第一次开机蓝屏 二次开机启动 使用了无缓存的M 2 SSD 2个通道的SSD 修改开机模式 改成兼容模式启动
  • 【H5】阻止H5页面播放视频默认全屏

    老是看到有人找不到阻止视频默认全屏的问题 看到别人发的帖子不是隐藏video标签使用canvas绘制视频就是使用插件来禁止视频默认全屏的问题 其实没有那么麻烦的只需要设置一下属性就可以了 x5 playsinline true 安卓需要设置
  • gRPC-Java实现各种服务类型

    gRPC实现 借助gRPC 我们可以在一个 proto文件中定义一次服务 并以gRPC支持的任何语言生成客户端和服务器 而这又可以在从大型数据中心内的服务器到电脑的各种环境中运行 gRPC为您处理不同的语言和环境 还获得了使用协议缓冲区的所
  • 设计模式-Visitor模式(访问者模式)

    什么是访问者模式 另一个例子 双重分发 开闭原则 对扩展开放 对修改关闭 优缺点 什么是访问者模式 将数据结构与处理分离开来 比如一个人 他有自己的名字 他可以吃东西 这里的 人 就是一个数据结构 名字是属性 吃东西是行为方法 处理 pub
  • Apache Tomcat Websocket 教程

    WebSocket 是一种计算机通信协议 旨在在 Web 浏览器和 Web 服务器中实现 但它可以被任何客户端或服务器应用程序使用 WebSocket 协议是一个独立的基于 TCP 的协议 它与 HTTP 的唯一关系是它的握手被 HTTP
  • 过来人告诉你:Java学到什么程度可以找工作?

    大部分初次学习Java的同学都非常关注自己学到什么程度可以找工作就业 因为学习的目的一方面在于掌握知识 提高技能 另一方面就是就业谋生 今天笔者就来跟大家聊一聊一下Java学习到什么地步可以面试找工作 任何企业 不论大小 对于初级的Java
  • python 之 进程与线程区别、GIL锁产生背景及对Python性能的影响?python的多线程是假的,为啥还用多线程

    一 进程与线程区别 根本区别 进程是操作系统资源分配的基本单位 每个进程都有自己独立的地址空间 数据 堆栈和状态 线程是处理器任务调度和执行的基本单位 一个进程可以有多个线程 这些线程共享进程的地址空间和资源 资源开销 内存分配 进程创建新
  • linux 线程详解

    前言 程序运行在内存空间中叫进程 进程中包含有若干线程 线程是系统调度和执行的基本单位 线程才是程序运行的实体 通常程序里的main 函数就相当于主线程 把进程理解成一个容器 里面可以包含有若干线程和若干资源 进程环境变量 打开的文件描述符
  • RabbitMQ学习笔记3-Java连接rabbitmq

    Java连接rabbitmq 新建工程 Pom配置 代码 演示如何通过java连接rabbitmq 此代码为后面获取rabbitmq连接的工具代码 新建工程 新建一个maven project工程 名字为rabbitmq demo Pom配
  • Objective-C实现链式编程语法(DSL)

    您越着急开始写代码 代码就会花费越长的时间 Carlson University of Wisconsin 前言 熟悉Objective C这一门编程语言的人都知道 Objective C中方法的调用都是通过中括号 实现的 比如 self