移动端Model层与Server服务层自动化
Model层自动化
前言(纯属捎带扯一下,后端大咖勿看)
谈到Model层自动化的产出我们就来从最初的地方开始讲,数据库!
无论你是用啥写服务器如果还手写Model那真的只能说你够low我服!不过如果是手写Model自动生成数据库那另说,总的来说就是要么从数据库转实体出来,要么从实体转数据库这才有点意思。
上边只是开玩笑下面正题,很多时候大家都无奈没办法不能说low不low,其中奥妙各有体会,通常有点规模的团队都是先定义表然后就出Model了
后端的Model层
后端数据库和实体互转的方案都不用Google,百度就能出来一大推,有直接读库生成的,也有从实体转sql生成库的,更有提前定义协议然后开始出对应模块对应语言的实体及sql
移动端的Model层
先继续谈会后台Model,这里要说的只是最好有一个提前定义的过程,这样一方面规范开发流程提前想好怎样建库合理,一方面有利于跨平台跨语言的开发,有了提前定义的协议,Model和枚举的各平台自动生成就方便了很多,写个简单的小程序即可,类型也就那几类。关键真的是一劳永逸,省去了大家互相校对的过程。
自动化生成移动端乃至前端用的Model层说白了就是做个类型映射,细说的话基本就是分为两类,一类直接就着后端现有项目读Model层的文件,然后做个类型的映射直接导出移动端需要的类,另一类也就是设计数据库时先定义Model的协议,然后根据协议自动生成各个平台需要的实体,而协议定义通常用序列化后的数据如xml(极力抵制,结构复杂),json,pb,sql文件都能干这事。
Model自动化实现
上面说到了xml,json,pb,sql文件都能干这件事,但其中最容易就是json,github上搜个json class基本就能有一大片总有你想要的语言,但json的局限在于也就能转换一下model,当然通过特殊定义中间转换,枚举啥的也能搞定,在这我推荐pb,首先它就是专门用来定义协议的,枚举实体不用说都能搞定,包括默认值设置也能写出来,而且是谷歌出品本身是im通讯协议,被它序列化的数据在上面说的里面算是最小的,而另一方面关于转码参考https://github.com/google/protobuf/blob/master/docs/third_party.md ,直接开放了各个语言的转换方法,当然你根本用不到它里面写的那么复杂,它的里面实体可是直接带pb转换model方法的,如果不是开发im根本用不到,要删部分源码实现自己的需要也行,。。。(下面讲一下正经方法)
Protobuf Convert转码
下面放代码片段(反馈的人多放全的,之所以不想放还有个原因是这边实现有点粗)
这里以Python为例,只是因为安装执行方便所以选它
转成OC的例子,之所以选OC因为我就是个搞IOS的。。。写起来各个公司需求不同每次都要改改改。。。也就是这个原因懒得放全的了,因为用的人说到底还是需要手动改成自己想要的,没有通用的。。。
#头文件引用
import os
from optparse import OptionParser
#pb转成方便处理的对象
from protoDef import *
#读取pb的
from protoReader import ProtoReader as reader
#上边两个就不放源码了,感兴趣的人多再说,毕竟上边的实现只是读pb逻辑大家估计都有自己的好办法
ENUM_TYPE = 'NSInteger'
#基础类型映射
typeMap = {
'int64': 'NSNumber',
'int32': 'NSNumber',
'string': 'NSString',
'bool': 'NSNumber',
'float': 'NSNumber',
'double': 'NSNumber',
}
#默认值映射
defaultMap = {
'int64': '@(%s)',
'int32': '@(%s)',
'string': '@"%s"',
'bool': '@(%s)',
'float': '@(%s)',
'double': '@(%s)',
}
def _convertType(pClz):
if typeMap.has_key(pClz):
return typeMap[pClz]
return pClz
def _convertDefault(pClz):
if defaultMap.has_key(pClz):
return defaultMap[pClz]
return 'nil'
#写文件方法(就是一点点输出oc的方法)
def _writeLine(outf, line = ''):
outf.write(line + '\n')
class IOSWriter:
def __init__(self, outDir, proto):
self.outDir = outDir
self.proto = proto
def __writeMsg(self, msg):
if isDeprecated(msg.comment):
return
self.__writeMsgH(msg)#生成.h
self.__writeMsgM(msg)#生成.m
#为对应类添加前缀做为命名空间(oc没命名空间。。。)
def __makeMsgName(self, msg):
if msg.protoPkg == '不想加前缀的条件':
return msg.name
return msg.protoPkg.upper() + msg.name
def __writeMsgH(self, msg):
msgName = self.__makeMsgName(msg)
path = os.path.join(self.outDir, msg.name)
# .h
outf = file(path + '.h', 'w')
_writeLine(outf, '/*\n * generated by proto-pygen, NEVER CHANGE!!\n * source file: %s\n */\n' % self.proto.filePath)
# comment
_writeLine(outf, '/**\n * %s\n */' % msg.comment)
# import
_writeLine(outf, '#import <Foundation/Foundation.h>')
importSet = set()
for field in msg.fields:
if isDeprecated(field.comment):
continue
if self.proto.hasMsg(field.clz) and field.clz not in importSet:
importSet.add(field.clz)
importMsg = self.proto.getMsg(field.clz)
importName = self.__makeMsgName(importMsg)
if self.__atClass(field):
_writeLine(outf, '@class %s;' % importName)
else:
_writeLine(outf, '#import "%s.h"' % importName)
_writeLine(outf)
# declare
_writeLine(outf, '@interface %s : NSObject' % msgName)
# field
for field in msg.fields:
if isDeprecated(field.comment):
continue
if field.comment:
_writeLine(outf, '/**\n * %s\n */' % field.comment)
if field.repeated:
if field.repeated and not typeMap.has_key(field.clz):
fieldMsg = self.proto.getMsg(field.clz)
fieldType = self.__makeMsgName(fieldMsg)
_writeLine(outf, '@property(nonatomic, strong) NSMutableArray <%s*>* %s;' % (field.name, fieldType))
else
_writeLine(outf, '@property(nonatomic, strong) NSMutableArray * %s;' % field.name)
elif self.proto.hasMsg(field.clz):
fieldMsg = self.proto.getMsg(field.clz)
fieldType = self.__makeMsgName(fieldMsg)
if fieldMsg.kind == Proto.PROTO_MSG:
_writeLine(outf, '@property(nonatomic, strong) %s * %s;' % (fieldType, field.name))
elif fieldMsg.kind == Proto.PROTO_ENUM:
_writeLine(outf, '@property(nonatomic, assign) %s %s;' % (fieldType, field.name))
else:
clz = _convertType(field.clz)
_writeLine(outf, '@property(nonatomic, strong) %s * %s;' % (clz, field.name))
# end
_writeLine(outf, '\n@end')
outf.close()
def __atClass(self, field):
return '@class' in field.comment
def __writeMsgM(self, msg):
msgName = self.__makeMsgName(msg)
path = os.path.join(self.outDir, msgName)
# .m
outf = file(path + '.m', 'w')
_writeLine(outf, '/*\n * generated by proto-pygen, NEVER CHANGE!!\n * source file: %s\n */\n' % self.proto.filePath)
# import
_writeLine(outf, '#import "%s.h"' % msgName)
for field in msg.fields:
if self.proto.hasMsg(field.clz) and self.__atClass(field):
importMsg = self.proto.getMsg(field.clz)
importName = self.__makeMsgName(importMsg)
_writeLine(outf, '#import "%s.h"' % importName)
_writeLine(outf, "\n@implementation %s" % msgName)
_writeLine(outf)
# repeated
kvlist = [] #数组内实体类 数组
dvlist = [] #默认值 数组
for field in msg.fields:
if isDeprecated(field.comment):
continue
if typeMap.has_key(field.clz) and field.default:
default=_convertDefault(field.default)
dvlist.append((field.name,default))
if field.repeated and not typeMap.has_key(field.clz):
fieldMsg = self.proto.getMsg(field.clz)
fieldType = self.__makeMsgName(fieldMsg)
kvlist.append((field.name, fieldType))
#默认值设置
if len(dvlist) > 0:
_writeLine(outf, "- (id)init {")
_writeLine(outf, " if(self=[super init]){ ")
for i, (name, default) in enumerate(dvlist):
line = ''' _%s=%s''' % (name, default)
_writeLine(outf, line)
_writeLine(outf, " } ")
_writeLine(outf, " return self;")
_writeLine(outf, "} ")
#用了YYModel转换所以有了这个方法
if len(kvlist) > 0:
_writeLine(outf, "+ (NSDictionary *)modelContainerPropertyGenericClass {")
_writeLine(outf, " return @{")
for i, (name, clz) in enumerate(kvlist):
line = ''' @"%s" : [%s class]''' % (name, clz)
if i < len(kvlist) - 1:
line = line + ','
_writeLine(outf, line)
_writeLine(outf, " };")
_writeLine(outf, "}")
# end
_writeLine(outf, '\n@end')
outf.close()
#生成枚举,之所以有.h .m是为了搞枚举string
def __writeEnum(self, enum):
if isDeprecated(enum.comment):
return
self.__writeEnumH(enum)
self.__writeEnumM(enum)
def __writeEnumH(self, enum):
enumName = self.__makeMsgName(enum)
path = os.path.join(self.outDir, enumName)
# .h
outf = file(path + '.h', 'w')
_writeLine(outf, '/*\n * generated by proto-pygen, NEVER CHANGE!!\n * source file: %s\n */\n' % self.proto.filePath)
# comment
_writeLine(outf, '/**\n * %s\n */' % enum.comment)
# import
_writeLine(outf, '#import <Foundation/Foundation.h>')
_writeLine(outf)
# declare
_writeLine(outf, 'typedef enum {')
# field
fields = []
for field in enum.fields:
if isDeprecated(field.comment):
continue
fields.append(field)
i = 0
for field in fields:
i += 1
if field.comment:
_writeLine(outf, '/**\n * %s\n */' % field.comment)
if i == len(fields):
_writeLine(outf, ' %s = %s' % (field.name, field.number))
else:
_writeLine(outf, ' %s = %s,' % (field.name, field.number))
_writeLine(outf, '} %s;' % enumName)
_writeLine(outf)
_writeLine(outf, '%s %sValueOf(NSString *text);' % (enumName, enumName))
_writeLine(outf, 'NSString* %sDescription(%s value);' % (enumName, enumName))
outf.close()
def __writeEnumM(self, enum):
enumName = self.__makeMsgName(enum)
path = os.path.join(self.outDir, enumName)
# .m
outf = file(path + '.m', 'w')
_writeLine(outf, '/*\n * generated by proto-pygen, NEVER CHANGE!!\n * source file: %s\n */\n' % self.proto.filePath)
# import
_writeLine(outf, '#import "%s.h"' % enumName)
_writeLine(outf)
# valueOf
_writeLine(outf, '%s %sValueOf(NSString *text) {' % (enumName, enumName))
_writeLine(outf, ' if (text) {')
fields = []
for field in enum.fields:
if isDeprecated(field.comment):
continue
fields.append(field)
i = 0;
for field in fields:
if i == 0:
_writeLine(outf, ' if ([text isEqualToString:@"%s"])' % field.name)
else:
_writeLine(outf, ' else if ([text isEqualToString:@"%s"])' % field.name)
_writeLine(outf, ' return %s;' % field.name)
i += 1
_writeLine(outf, ' }')
_writeLine(outf, ' return -1;')
_writeLine(outf, '}\n')
# description
_writeLine(outf, 'NSString* %sDescription(%s value) {' % (enumName, enumName))
_writeLine(outf, ' switch (value) {')
for field in fields:
_writeLine(outf, ' case %s:' % field.name)
_writeLine(outf, ' return @"%s";' % field.name)
_writeLine(outf, ' }')
_writeLine(outf, ' return @"";')
_writeLine(outf, '}')
outf.close()
def write(self):
for msg in self.proto.definedMsgs:
if msg.kind == Proto.PROTO_MSG:
self.__writeMsg(msg)
elif msg.kind == Proto.PROTO_ENUM:
self.__writeEnum(msg)
#写文件到本地
if __name__ == '__main__':
parser = OptionParser()
parser.add_option("-r", "--root", dest="protoDir", help="root proto dir", metavar="DIR")
parser.add_option("-f", "--file", dest="protoFile", help="input proto file", metavar="FILE")
parser.add_option("-o", "--out", dest="out", help="output dir", metavar="DIR")
options, args = parser.parse_args()
if not options.protoDir:
parser.print_help()
parser.error('no proto dir')
if not options.protoFile:
parser.print_help()
parser.error('no proto file')
if not options.out:
parser.print_help()
parser.error('no out dir')
if not os.path.exists(options.out):
os.makedirs(options.out)
proto = reader(options.protoDir, options.protoFile).read()
IOSWriter(options.out, proto).write()
上边就是Protobuf 转Model的逻辑,有了这个前提,下边Server服务层的自动化就有了
Server服务层自动化
这里可以引申一下基本上写功能时只要分成配置类和启动器这样,根据配置类就可以实现自动化了,这里就拿IOS我这的实现讲。
源码地址:https://github.com/heroims/ServerAPI
这里也只是简单说一下
ServerAPI 定义一个请求的地址,重试次数,返回数据转换模式
ServerAPIManager 根据ServerAPI发起请求
ServerAPIProtocol 定义需要实现的方法(为了扩展性高,这里定义必须实现的协议,具体需要定制的需求通过Category实现相关方法)
ServerResult 返回的通用型实体包含解析的字典,错误信息等
总的思路就是ServerAPI来定义一个请求的具体内容参数,而ServerAPIManager负责发起请求返回数据,然后就只需要继承ServerAPI对不同请求具体参数直接返回具体的值即可,比如requestHost,resultFormat,retryTimes,timeOut,returnClass
回到Protobuf这个就相当于定义request,但差别还是很大有了对一个API的描述,那么移动端包括后端,前端都可以通过这个描述来做对应的事情,只需要封装一个东西去处理描述,至此就完成了Server服务层的自动化,外加说一句后端的话为了性能可能更好的方案是根据描述生成代码吧,用代码写代码才是正道。。。。