iOS开发 -- 定制功能模块化

最近在整理公司的项目结构。想着总结一篇有关于定制功能模块化的文章。
分享了10几篇文章,还木有介绍过我平时的iOS开发产品,由于公司的产品是地理信息规划软件,平时的客户基本都是地理信息部门,所以一般都是客户提出功能需求,公司这边再提供软件方案,且基本都是针对性的模块化功能、只提供给特定用户。
因此,啊左的公司使用的是苹果企业级开发者账号(碰过许多坑,有兴趣的童鞋也可以交流下),平时开发的APP都不放在AppStore上面的,属于企业级分发软件。
这样就有个问题,不同客户需要不同的软件,且基本功能都是差不多,不同的产品只是增删改一部分。这样的话,需要多少套代码呢?
其实是可以只用一套代码的。
此次想分享的就是:多个项目(Target)共用一套代码

多个功能的实现,其实更多的是技术点的堆积,在开发难度上,属于业务类的开发。所以在开始动手前期基本功能都完善,业务开发基本完成后,更重要的是功能的模块化,也就是项目结构优化。
那么,怎么在保证功能的实现去进行结构的优化,已达到可维护性与可重用性的增强呢?


业务代码

没日没夜地撸代码,王者是不可能的,砖石也别想了,反正就是干。不断把天敌(我说的就是项目经理😐)提出的功能需求堆积出来。

【当然,也别把代码弄得很乱,起码哪个部分是写的什么要清楚。】


功能(业务)模块化

"啊左的APP"功能定制 2017-06-05 17.29.45.png

上面是刚创建的比较基础的功能定制属性列表。这是我们项目的重心,增删查改大部分功能都可以在上面操作,也会是接下来我们探讨的模块化设计模式。

【本次开发环境: Xcode:7.2 iOS Simulator:iphone6S By:啊左 本文Demo下载链接:CustomMap-Demo

<u>一、使用“定制功能属性列表”</u>

创建自己定制的.plist文件,在本文中,我们称作“定制功能属性列表”,“Custom-one.plist”(其中,“Custom-”是前缀; “one”是项目名称;)
(这种属性的读取方式,各个需要使用的字段都可以放在一个公共类“AllCommons”里面被调用):

#import "AllCommons.h"
@implementation AllCommons

//获取项目定制信息;
+(NSDictionary *)customSettings
{
    NSString *prefix = [[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString*)kCFBundleNameKey];
    NSString *plistFileName = [NSString stringWithFormat:@"%@.plist", prefix];
    
    //以字典的格式,读取资源包里的"项目名.plist"文件。
    NSDictionary *customizedSetting = [NSDictionary dictionaryWithContentsOfFile:
                    [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:plistFileName]];
    return [NSDictionary dictionaryWithDictionary:customizedSetting];
}
@end

接下来,举三个例子。
例子一:读取方式(例如AppTitle字段):

    /*--- 主程序   ---*/
    NSString *appTitle = [[AllCommons customSettings]objectForKey:@"AppTitle"];
    NSLog(@"appTitle的值:%@",appTitle);

输出"Custom-项目.plist"的"AppTitle"对应信息(字符串)。
例子二:是否打开APP时提示更新?

    NSString * updateUrlStr = [[AllCommons customSettings]objectForKey:@"AppUpdateURL"];
    NSString * isupDate =[[AllCommons customSettings]objectForKey:@"UpdateCheck-permission"];
    if(isupDate)
    {
        NSURL *updateUrl = [NSURL URLWithString:[updateUrlStr stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding]];
        if (![[UIApplication sharedApplication] openURL:updateUrl]) {
            NSLog(@"%@%@", @"Failed to open url:", [updateUrl description]);
        }
    }

例子三:自定义.xib。
在tableViewController中:

    CustomShowViewController *customXibViewController = nil;
    NSDictionary *dictForXib =[[AllCommons customSettings]objectForKey:@"MeasureViewController_xib"];
    NSString *customXibName = [dictForXib objectForKey:@"xib_name"];
    //判断是否定制信息中.xib为空
    BOOL iscustomXib = (customXibName == nil || customXibName.length == 0);
    //这里读到的字符串为“MeasureViewController_az”,可做为控制器的xib视图
    if(iscustomXib)  //没有自定义.xib则创建新控制器
    {
        customXibViewController = [[CustomShowViewController alloc] init];
    }else{                 //.xibName存在则通过创建.xib实例化控制器
        customXibViewController = [[CustomShowViewController alloc] initWithNibName:customXibName bundle:nil];
    }
//这里得到的customXibViewController就是不同项目.plist自定义的控制器了。

其他的功能字段类似如此,可自定义是否数据,以及是否使用该功能;
例如在"MyTools"中你可以自定义添加需要的功能,当然项目中必须存在已实现该功能的代码。

二、通过Target管理多个相似的项目

1. Duplicate两个个target:

如图,右键选择Duplicate(复制)两个新的target。


添加一个新的Target
2.区分不同target的处理。

修改target的名称如下:


修改target名称

然后"run"键旁边的target管理,"manage Schemes"


manage Schemes

进行Scheme管理,也就是Scheme部分target名称的修改:
Scheme部分target名称的修改

啊左用的是Xcode7,复制另个target出来后,可以看到2个新的"info.plist",这是每个独立项目都会有的系统文件配置,如果要修改名字的话,记得在target的设置上面的设置也同步下。


两个系统的文件配置
3、<u>重点</u>:不同target使用不同的功能定制

像前面“使用“定制功能属性列表”部分,创建不同需求的项目的属性列表。建议不同的target可像截图一样,创建不同的文件夹,放置该项目的数据信息;
(.plist前面的名称记得与项目一样。)


不同项目的不同功能定制

这里需要特别注意的是:
不管是.plist文件,还是图片资源,在导入的时候记得在“Target Membership”下面准确关联到相关的项目,例如"CustomMap-one.plist"就是关联到“CustomMap-one”,如果"CustomMap-two"也打钩的话,那就是“CustomMap-two”这个项目里面的资源包“CustomMap-two.app”也会包含这个文件。


Target Membership与项目同步

以上,当target的各项设置完成后,读取one和two的“定制功能属性列表”中的字段"AppTtitle"字段,

    NSString *appTitle = [[self customSettings] objectForKey:@"AppTitle"];
    NSLog(@"app的名字:%@",appTitle);

我们可以看到输出结果分别是CustomMap-one和CustomMap-two.
建议:不同的target可以在代码都完成的时候在创建,就不用每次创建新的类时,“Target Membership”都要选择全部。因为Duplicate就已经将“CustomMap”这个基础图层的所有关联代码依旧引用上了。

<u>▲三、定制生产线——工厂设计模式</u>

从前文例子三,可以看到自定义不同的.xib可以在tableViewController中使用initwithNibName读取,那么,如何确保不同项目也能定制不同的功能类?下面我们对于不同项目定制不同地图处理功能的过程进行剖析。
在需要添加地图操作功能的项目定制.plist文件中,增加这一段功能:


定制功能类
1.创建一个父类Operation类

定义,但保留主要方法的实现;
MapOperation.h中:

@property (nonatomic, strong)NSDictionary *dict;
-(NSDictionary *)createOperationForMap:(NSString *)map withArea:(double)area;

MapOperation.m中:

-(NSDictionary *)createOperationForMap:(NSString *)map withArea:(double)area
{
    NSDictionary *dict = nil;
    //父类创建一个对map的area面积进行某种处理的操作,由子类继承,并返回一个字典给外部;
    return dict;
}
2.创建不同target需求的Operation子类:

例如CustomMap-one项目需要的是“MapOperation_analyze”功能、
CustomMap-two需要的是“MapOperation_measure”等,并在.m中各自实现父类关于操作的方法;
MapOperation_analyze.m中:
-(NSDictionary *)createOperationForMap:(NSString *)map withArea:(double)area { NSMutableDictionary *dict = [NSMutableDictionary dictionary]; [dict setValue:@"分析" forKey:@"City"]; //加上20.2,与measure区别开来 area = area + 20.2; [dict setValue:[NSNumber numberWithDouble:area] forKey:@"count"]; return dict; }
MapOperation_measure中:
-(NSDictionary *)createOperationForMap:(NSString *)map withArea:(double)area { NSMutableDictionary *dict = [NSMutableDictionary dictionary]; [dict setValue:@"测量" forKey:@"City"]; [dict setValue:[NSNumber numberWithDouble:area] forKey:@"count"]; return dict; }

3.创建工厂类“MapOperationFactory”(继承于NSObject)搭好生产线,读取ClassName,获取对应class,进行class的创建。

代码中,创建:


+ (MapOperation *)createMaoperation
{
    //1.初识化操作
    id operation;
    
    //2.读取定制信息
    NSString *targetClassName = [[AllCommons customSettings:@"MapOperation"] objectForKey:@"classsName"];
    Class targetClass = NSClassFromString(targetClassName);
    //这里其实也可以不用Class,而用switch判断生成各自的子类实例,只是用Class相比这种简单也清晰多了。
    if (targetClass != nil) { 
        operation = [[targetClass alloc]init];
    }
    //3.可再进一步对实例化对象进行处理

   //产生一个实例化的操作对象:
    return operation;
}

Operation是子类的共同父类。
OperationFactory是子类们工厂方法。需要在开头导入各子类的头文件,读取定制className,进行对象实例化。
具体的使用如下:

    //Factory类中已根据.plist文件中的ClassName实例化对象;
    MapOperation *operation = [MapOperationFactory createMaoperation];
    NSDictionary *dict = [operation createOperationForMap:@"地图" withArea:1214.12];
    NSLog(@"操作类型:%@\n数量:%@",[dict objectForKey:@"City"],[dict objectForKey:@"count"]);

Runnig~...
如果是Custom-one项目,输出:
操作类型:分析 数量:1234.32

如果是Custom-two项目,输出:
操作类型:测量 数量:1214.12


其他细节介绍

一、经常出错的地方:

1.字段的拼写错误;
2.代码都关联所有项目没关系,因为共用同一套代码。重点是资源(例如图片、bundle包):
是否关联勾选正确的“Target Membership”。
是否多勾选了,因为即使不使用,也会增加生成项目的内存;
3.业务层是否足够灵活,避免写死、增加扩展的地方,例如在工厂类中,增加一个Other子类;

二、这种优化的优缺点:

1.特色:项目优化、功能模块化。使得项目更加条理清晰、增加重用性、封装性。
2.功能接口化:每部分的功能更像一个接口,就像每个标有功能屋子,只提供一个窗口,放一个数据包进去,最后会从这个窗口出来一个处理完的result;
3.人性化:
对于客户:可根据需求,定制信息;
对于项目经理:无需了解代码,可根据范例.plist文件,主动使用需要的功能,且能提供给上一级领导清晰的业务开发进度。
4.缺点:
代码最基础的业务功能容易写死。所以前期需要灵活设计,后期需求大幅度修改的话,需要与产品、项目经理协商,避免代码块的修改,影响到other项目。
解决方案:
a.前期要把逻辑抽离开来,避免耦合度过高(例如某个需要经常改动页面的结构业务功能,可进行设计几个.xib,例如截图中第9点。那个_az就是我负责的项目所属的页面)
b.对于某部分稍微大幅度的修改。可继承原来的功能父类,为子类添加新的功能、覆盖旧的结构。(例如,Operation下的各个子类)
d.项目经理这样被惯了之后,会觉得添加一个功能很容易,不就是添加一个字段么你那边再稍微"协调"一下么。。。
但从正面来说,整个开发过程,流程清晰,日报、项目进度,自己分内工作有明确反馈,养成规划的好习惯;

三、其他补充:

1.公共类:AllCommons
除了提供定制.plist文件字段的接口数据;
也可以把其他经常用到的代码块放置这里:
例如沙盒的读取、数据库的读取、文件复制与移动操作等可提供共用小功能模块;

2、开发实践
这个项目的名字是


CustomMap总项目

所以,可以作为一个总项目test target,测试功能与代码;
创建一个"CustomMap.plist",把需要测试的plist字段填好,需要的资源关联打钩,可提供单一功能的测试,而不用每次都生成one/two等项目;

当然,不是所有项目都适合这种开发模式,但是,如果是偏向于定制功能,可以使用这种方式。

尾声:

上天给你编制了整套代码,你永远不知道未使用的模块是什么功能,人的一生,就是在你的属性列表上面,努力地去添加不同的属性,然后,不断Debug它,直到它顺畅运行。


(转载请标明原文出处,谢谢支持 ~ - ~)
 by:啊左~

推荐阅读更多精彩内容

  • Swift版本点击这里欢迎加入QQ群交流: 594119878最新更新日期:18-09-17 About A cu...
    ylgwhyh阅读 20,974评论 7 240
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 158,505评论 24 688
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 121,952评论 17 134
  • 感恩所发生的一切,让我现在心态越来越平静了,不会因为别人开车抢道、排队等候别人插队、别人说话冲动而觉得烦恼、生气,...
    丽日风清阅读 50评论 0 0
  • 大家好,我是一班的总教练李婕 我的三个标签 1.李闯王,上海地摊王,北京6000租金服装店月入42万,深圳不懂用电...
    娃娃的家阅读 102评论 1 1