iOS 开发中对代码规范的一点见解

作者: CoderHG | 来源:发表于2017-04-07 21:40 被阅读241次

    阅读本篇简书,请先忘记所谓的 MVC 与 MVVM。

    本篇简书大概有如下模块:


    目 录

    发现在开发的过程中很少会有人关注这些问题。

    特别提醒:本篇介绍,主观性会很强。同时请忘记所谓的 MVC 与 MVVM。

    一、团队与规范

    1.1 杜绝规范文档式

    强烈的不推荐编写文档式的 代码规范。下面有几个一直思考的问答题:

    A) 规范一出,所有的人都会按照规范执行么?

    这是不可能的,写过代码的人都知道为什么不可能。除非你的团队少到只有四五个人,否则很难统一。

    B) 自己定的规范,自己会严格执行么?

    说句实话,很多的时候也在考虑这个问题。一直都有一个习惯,开发完一个模块后,总会自己进行一次 Review,过程中也会做出相应的调整。说明在实际开发中,自己也不会严格按照心中的规范弄,导致这样的原因有很多。出现这种情况,对于自己来说自己直接改了即可。如果是形成了一种文档式的规范,被自己的团队看到了,他们会怎么想:你定的规范,你自己都不严格执行。。。。

    C) 规范之外的代码,一定很差劲么?

    不能一概而论,要求代码规范,并不是要搞一个 俄罗斯方块 ,而是要求代码具有 健壮性、可读性,从而提高项目的可维护性,这才是关键。

    1.2 当年实施方案

    团队中 代码规范,肯定是要有的,主要是如何的去要求。应该从面试就开始注重 代码规范这个细节。之前特意提炼出一套面试题Objective-C 精选五道题,是的、就这五道题,所谓精选就是简单。从对方对前四道题的理解,基本上能感觉到对方对代码的要求。毕竟这五道题主要是与其交流,交流过程中能挖掘其代码阅读能力与见解。

    那么,问题来了:

    如果新人的代码在规范上出现了问题,应该怎么办?

    借力推力的方式进行指导,建议看 MJ 老师的 MJExtension,上班时间要求花 30分钟的学习时间。这点时间也只能看过大概,悟性高的同事会在下班时间花时间去深究的。

    如果说在代码的命名上出现了问题,应该怎么办?

    所有的规范,都尽量按照苹果的规范弄,主要有以下几个方面:

    1)全局变量的定义
    第一、字符串

    字符串,不允许定义成宏。 苹果系统的代码中,就没有出现过字符串使用宏来定义的。
    比如通知,主要参考 UIKeyboardDidChangeFrameNotification ,后缀必须为 Notification

    第二、delegate 的定义与使用

    主要参考 UITableView 的 delegate 与 dataSource。名称后缀必须为Delegate 与 DataSource 。所有方法的第一个参数必须是当前的 instance,即使是这个参数没有用到也必须要加。主要用于与普通方法做一个细微的区分,虽然每个代理的前面都会有一个 #pragram,但是肯定不会每个代理方法都会有 #pragram。

    第三、枚举的定义

    主要参考:UIViewAutoresizing、NSKeyValueObservingOptionsUIButtonType

    第四、@property语法不能乱

    内存语义必须要写,虽然各种类型都会有对应的默认值。
    其次 NSString 如果使用 copy,那么在重写 setter 方法的时候,务必要 copy 一下,虽然在很多时候没有什么鸟用。

    核心的就是以上的四点,其它的就是以此类推。

    二、习惯即规范

    以上都是在围绕 团队与规范 在介绍,其实追根到底、没有形成习惯的规范,那就不叫 规范,顶多就是一时的激情而已。把好的规范形成一种习惯,进而成为一种团队的文化,那才是真正的牛逼!

    很多的时候,总是会遇到这样的情况,交流的时候说得天花乱坠,真正看代码的时候却 徐徐微风,并且还 处处冻人

    三、核心介绍可能在这里

    好了、上面说了一堆的废话,接下来说点 正儿八经 的事,就是这张图:

    Controllers&Views&Models

    看到这张图,空中飘过三个字:MVC。再次提醒,看到这里请忘记 MVC&MVVM,看我接下来的 胡扯 就行。

    接下来,会以第一次看到新代码的顺序来做介绍。

    好的、那我们继续:👉👇👉👇👉👇👉👇👉👇👉👇

    3.1 项目的结构

    这里不想做详细的介绍,总之项目的结构不能乱。不要让别人一打开项目,到处都是目录,到处都是代码。我之前一直使用的是这样的目录:


    NewStart

    其中 Modules 中是当前项目的不同业务模块。与下图类似:

    Modules

    这就是 Modules 中对项目中不同业务的区分。当然这里一定要说明一点的是:比如这里的 Home 模块中仅有 Controllers&Views&Models,在实际项目中,肯定是不可能的,这里面肯定还是需要再细分的。

    这是我之前一直使用的模板,但是这个模板其实还是有点问题的,比如 Macro 这个目录名,有点不合理,因为这个目录中实际放的是整个项目除第三方之外的通用代码,比如网络请求,常量定义,基类。但是从某种角度上来说,还是可以的。

    这样的目录,至少能让别人知道大概的什么样的代码是放到哪里的。当然了,保持项目目录与本地目录一致,这肯定是必须的。

    3.2 模块中的目录

    上面提到,一个项目一般会有不同的业务模块,然后在某个业务模块中会有小模块,当然小模块中可能还会有小模块,我们暂时把不能再划分的模块命名为 单元模块。比如有一个项目,其中有一个业务模块是 个人中心, 在个人中心 里面会有很多的小模块,比如有一个 钱包,于是将 钱包 这个模块命名为 单元模块

    切记,一旦将某个模块定义成一个 单元模块 之后,里面只允许出现 Controllers&Views&Models 这三个目录,不允许再出现其它的目录,否则视为不规范。

    但是问题又来了。

    在这个 钱包单元模块 中,还有其它的小部分,比如 充值与提现。那应该怎么办?

    当然了,这个是需要视情况而定的,如果 充值与提现 这两个模块很复杂的话,就不能将 钱包 定义成 单元模块 了,而是将 充值与提现 分别再弄成两个 单元模块

    接下来,就看一下:在单元模块中如何再做区分? 直接看图:

    钱包

    上图是什么意思?
    Wallet 就代表是 钱包 这个 单元模块,在这个单元模块中的直属目录必须有且仅有 Controllers&Views&Models,只能少不能多,否则视为 不规范
    其中每个目录中的 Base&Recharge&Withdraw 这三个目录,Base 可有可无,视情况而定,毕竟一个项目肯定有对应的 BaseClass。
    其次,更值得提醒的是:

    • 1、Controllers 只允许有相关联的 控制器
    • 2、Views 只允许有相关联的 视图
    • 3、Models 只允许有相关联的 Model

    否则视为 不规范

    3.3 单元模块中的交互

    还是又要回到这张图:


    Controllers&Views&Models

    这张图已经是第三次出现了。以上介绍的是如何布局目录,如何定义 单元模块。那么问题又来了:

    单元模块 中的 Controllers&Views&Models 有应该如何交互呢?

    首先要分别介绍一下各自的职责:

    3.3.1 Controllers

    存放的是控制器,控制器类似一个中央处理器。管理着当前 单元模块 的来龙去脉,主要职责为:获取数据(网络/数据库)、数据的传递、视图的显示以及控制器之间的跳转。

    有两点需要做特别说明:

    (1)获取数据,并不是说 获取数据 这个功能在控制器中实现,而是说在控制器中获取数据。如果是网络请求,在项目中肯定已经封装了对应的网络库,直接在控制器中使用。数据库,也是类似。

    (2)数据的传递, 并非是 数据的处理。很多的伙伴,把数据的处理都放到控制器中了。可想而知,控制器到底有多乱。对于数据,在控制器中仅仅做传递就可以:网络下发,直接给到 Model,然后在 Model 中做逻辑处理,处理结束之后再放到相关联的 View 上,或者给到另一个控制器。再强调一遍:逻辑是在 Model 中处理的。

    3.3.2 Views

    对于这个,说简单也简单,说复杂也并不是吓唬人。很多的时候,时间花费最多的就是这个部分了。这部分的主要职责为:数据显示、数据修改以及事件传递。

    特别说明:

    (1)数据显示,视图本来就是显示信息的,信息本来就是数据,这个数据可能是通过控制器中传递过来的,也有可能是写死的。
    (2)数据修改,这个也很好理解,比如一些表单的填写。
    (3)事件传递,一个视图,除了数据的修改,往往还会有一些事件,比如点击按钮,需要做控制器的跳转。在订单列表中,点击支付后跳转到支付界面,点击取消后跳转到取消的控制器。这些事件都是需要从视图中告诉控制器的,在控制器中做跳转。

    3.3.3 Models

    Model 是模型,某些大神也将其统一成 DTO,比如 S**EBuy 的项目中使用的就是 DTO。当然在同一个项目中只能有其一,不推荐在同一个项目有 Model 也有 DTO。
    那么在 iOS 开发中,Model 主要的职责是:数据载体与数据逻辑处理。

    特别说明:

    (1)数据载体,往往从网络/数据库中获取的数据都是 JSON 字符串成或者字典,为么方便参阅与使用方便,所以转化为 Model 之后再进行使用。
    (2)数据逻辑处理,在 Model 中有数据了之后,很多的时候都需要对其中的数据做进一步的处理。

    3.4 实战

    举个例子,要实现如下列表界面:


    Simulator Screen Shot - iPhone 8 Plus - 2018-06-03 at 19.07.06.png
    分析:
    • 1、一个列表。
    • 2、有四种样式的 Cell:无图、带一张图、带两张图、带三张图。
    技术实现:
    • 1、使用 UITableView 实现。
    • 2、同一个 UITableViewCell 中根据不同的数据显示不同的 Cell 样式。

    总之就是:在一个列表中显示四种样式的 cell。接下来就按照 3.3 单元模块中的交互 中介绍的规则一一介绍。
    代码目录布局如下:

    Home

    是的、没错,这就是全部的代码。

    3.4.1 Controllers

    主要见于 HomeController 中。

    (1)获取数据

    HomeModel&datas

    实际项目中,大多数都是网路请求下发的,这里仅仅是从 plist 文件中提取而已,但是原理是一样的。

    在控制器中获取数据,这个几乎所有的开发都能满足。

    当然也有一种情况,在一个视图需要显示一些网络数据,比如版本更新,仅仅是一个提示而已。然后有的开发就将网络请求放到了对应的视图中,这个我是不推荐的。如果我是团队负责人的话,肯定是需要强制调整到控制器中的。

    (2)数据的传递

    更多的是将获取的数据展示到对应的视图中,具体如下:


    数据的传递

    直接看图片中的代码,一个视图对应一个 Model。这也可以说视图与数据是一一对应的。

    (3)视图的显示
    当然了,这个例子是使用 UITableView, 只要实现了对应的 delegate,具体的显示就出来了。

    (4)控制器之间的跳转

    image.png

    image.png
    3.3.2 Views

    主要见于 HomeCell 中。

    (1)数据显示
    当然,这是有一个前提是的,在视图中必定会有很多的子视图。通过以上我们知道,一个视图与一个数据(Model)一一对应,所以可以想到在 setter 方法中做关联。

    数据显示

    (2)数据修改
    在本例子中,没有做数据修改的介绍。但是这个也很好理解,比如表单的填写。当然,这样的功能相对来说是比较复杂的,感兴趣的话,可以看一下这个:个性签名编辑框的简单实现

    相关连的代码如下:


    image.png

    图中的 delegate 方法就是由SetupSignatureCell 触发的。

    (3)事件传递
    在这个例子中,对于事件的传递没有做处理。比如这个:

    image.png

    这个事件对应的就是这里:


    image.png

    一般这种操作是需要跳转控制器的,但是这个事件是在一个 Cell 的子类中实现的。不管怎样,都需要通过 delegate 或者 block 的方式传回到控制器中处理。

    3.3.3 Models

    主要见 HomeModel 中。

    (1)数据载体

    image.png

    图中有一个红框区域与紫区域。通常是 红框区域 中的属性就是这个 Model 的主要属性,然而 紫区域 中的属性隶属于辅助属性,或者是由于某种逻辑的需要,或者是有事数据在视图显示数据时需要,视情况而不同。(按照当前的功能也是可以将紫色区域的属性单独创建一个独立的 Model 来管理。)

    总之:红框区域 的数据是由网络或者数据库之后获取的,然后 紫区域 的属性是由于某种需要而添加的。

    (2)数据逻辑处理

    是的、很多的情况,我们获取的数据是需要做一些特别处理的。就使用当前的例子来说,我们从 plist 获取数据之后,这些数据会由于带有的图片的不同而现实不同的样式,所以当前 Cell (HomeCell) 中的子视图中的 frame 会有不同的调整。但是同一个数据(Model)的显示肯定是固定的、比如 Cell 的高度。所以这样的逻辑完全可以放在 Model 中处理,所以才有了 紫区域 的属性。

    具体的处理,如下:

    // 通过数据计算cell的高度
    - (CGFloat)cellHeight {
        if (_cellHeight == -1) {
            // 开始计算实际的cell高度
            
            // 标题的位置
            // 标题宽
            CGFloat titleWidht = UI_SCREEN_WIDTH - Margin*2;
            if (self.photos.count == 1) {
                titleWidht = UI_SCREEN_WIDTH - Margin*2 - DefaultCellHeight-MidMargin;
                _loneFrame = CGRectMake(UI_SCREEN_WIDTH-DefaultCellHeight-Margin, MidMargin, DefaultCellHeight, DefaultCellHeight- MidMargin*2.0);
            }
            
            // 标题高
            CGFloat titleHeight = [self.title heightWithWidth:titleWidht font:Font(TitleFontSize)];
            { // 目的就是 最大只能显示两行
                if (titleHeight > 30) {
                    // 两行固定是49.0  故意弄点误差
                    titleHeight = 49.0;
                } else {
                    // 一行固定是25.0  故意弄点误差
                    titleHeight = 25.0;
                }
            }
            _titleFrame = CGRectMake(Margin, MidMargin, titleWidht, titleHeight);
            
            
            if (self.photos.count == 0) {
                _cellHeight = DefaultNoPhotoCellHeight;
            } else if (self.photos.count == 1) {
                _cellHeight = DefaultCellHeight;
            } else {
                
                CGFloat photoWidht = -1;
                if (self.photos.count == 2) {
                    photoWidht = (UI_SCREEN_WIDTH - 2*Margin - MidMargin)*0.5;
                } else {
                    // 按照3张的宽度
                    photoWidht = (UI_SCREEN_WIDTH - 2*Margin - 2*MidMargin)/3.0;
                }
                
                _photoWidth = photoWidht;
                
                CGFloat photoHeight = photoWidht*PhotoScale;
                
                _collectionFrame = CGRectMake(Margin, CGRectGetMaxY(_titleFrame)+MidMargin, UI_SCREEN_WIDTH-2*Margin, photoHeight);
                
                _cellHeight = MidMargin + titleHeight + MidMargin + photoHeight + BottomHeight;
            }
            
            // 底部视图的宽 就是标题的宽度
            _bottomFrame = CGRectMake(Margin, _cellHeight - BottomHeight, titleWidht, BottomHeight);
        }
        
        return _cellHeight;
    }
    

    很多的时候,我们总是会看到这样的代码却是放到控制器中处理的。厉害了我的歌,代码该有多乱。更有甚者,直接在 UITableView 的代理方法中计算。这样的设计,真的挺佩服的。

    还有一种情况,直接放到对应的 Cell 中以 Class 方法 的形式计算出对应的 cell 高度。这种方式,我是极力不推荐的。原因有二:
    1、一个 Cell 的高度是由数据来决定的,不是有 Cell 来决定。
    2、相对来说 Cell 的逻辑会比 Model 中的多,比如子视图的创建与布局,一些事件、代理的处理。也是为了将代码做一个分流。

    以上的两个原因,我比较看重的是第一个

    自此、关于 Controllers&Views&Models 中的代码布局,基本上介绍结束了。当然只是介绍主要的流程,更多的细节,可以参考这份代码 NewStart

    3.5 代码的归属

    什么样的代码,就应该放到合理的地方。

    3.5.1 该 Model 传参的,就应该 Model 传参

    image.png

    看着这样的代参,总是会很兴奋,因为又要删一波代码了。

    为什么不换成这样传参呢?


    image.png

    当然,可能会听到这样的解释:在 hgModel 仅仅使用到其中的四个属性而已,没有必要将全部都传进去。

    传进去有什么影响么,不使用不就可以了么?别忘了,有一个技术叫 注释, 这样不就可以了么?
    HGController 中这样注释:

    @property

    使用的时候,这样注释:


    image.png

    注释不就是为了做特殊说明的么??很多的时候也发现,很多的代码直来直往,就为了减少注释。

    特别解释

    这里需要做一个解释,一般情况一个控制器有一个 Model 属性即可,但是有的时候一个控制器可能会有多个业务场景。所以就会有这样的情况:


    image.png

    这个是完全没有问题,但是还需要添加这样的注释:


    image.png

    3.5.2 保证在 Model 中做数据解析

    往往会在控制器、或者网络工具中看到这样的代码:


    image.png

    代码是挺规整的,为什么不放到 Model 中去解析呢?


    image.png

    很多的时候,相同的代码放在不同的地方,虽然什么都没有受到影响,但是数据还是应该放到与数据相关的地方处理。偏偏要放到控制器中处理,这也太随意,太不规范了。

    四、提高代码质量的几点小建议

    • 1、不去学会如何欣赏与鄙视别人的代码,你就永远体会不到自己的代码被别人欣赏的乐趣与被别人鄙视的狼狈。
    • 2、有的时候、自己写的代码导致别人看不懂,真的不是别人的技术不如你!
    • 3、学会鄙视别人的代码,也是一种自我激励的一种方式

    相关文章

      网友评论

      本文标题:iOS 开发中对代码规范的一点见解

      本文链接:https://www.haomeiwen.com/subject/qalkattx.html