最近在整理公司的项目结构。想着总结一篇有关于定制功能模块化的文章。
分享了10几篇文章,还木有介绍过我平时的iOS开发产品,由于公司的产品是地理信息规划软件,平时的客户基本都是地理信息部门,所以一般都是客户提出功能需求,公司这边再提供软件方案,且基本都是针对性的模块化功能、只提供给特定用户。
因此,啊左的公司使用的是苹果企业级开发者账号(碰过许多坑,有兴趣的童鞋也可以交流下),平时开发的APP都不放在AppStore上面的,属于企业级分发软件。
这样就有个问题,不同客户需要不同的软件,且基本功能都是差不多,不同的产品只是增删改一部分。这样的话,需要多少套代码呢?
其实是可以只用一套代码的。
此次想分享的就是:多个项目(Target)共用一套代码。
多个功能的实现,其实更多的是技术点的堆积,在开发难度上,属于业务类的开发。所以在开始动手前期基本功能都完善,业务开发基本完成后,更重要的是功能的模块化,也就是项目结构优化。
那么,怎么在保证功能的实现去进行结构的优化,已达到可维护性与可重用性的增强呢?
业务代码
没日没夜地撸代码,王者是不可能的,砖石也别想了,反正就是干。不断把天敌(我说的就是项目经理😐)提出的功能需求堆积出来。
【当然,也别把代码弄得很乱,起码哪个部分是写的什么要清楚。】
功能(业务)模块化
上面是刚创建的比较基础的功能定制属性列表。这是我们项目的重心,增删查改大部分功能都可以在上面操作,也会是接下来我们探讨的模块化设计模式。
【本次开发环境: 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。
2.区分不同target的处理。
修改target的名称如下:
然后"run"键旁边的target管理,"manage Schemes"
进行Scheme管理,也就是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的各项设置完成后,读取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、开发实践
这个项目的名字是
所以,可以作为一个总项目test target,测试功能与代码;
创建一个"CustomMap.plist",把需要测试的plist字段填好,需要的资源关联打钩,可提供单一功能的测试,而不用每次都生成one/two等项目;
当然,不是所有项目都适合这种开发模式,但是,如果是偏向于定制功能,可以使用这种方式。
尾声:
上天给你编制了整套代码,你永远不知道未使用的模块是什么功能,人的一生,就是在你的属性列表上面,努力地去添加不同的属性,然后,不断Debug它,直到它顺畅运行。
(转载请标明原文出处,谢谢支持 ~ - ~)
by:啊左~