Model層自動(dòng)化
前言(純屬捎帶扯一下拍冠,后端大咖勿看)
談到Model層自動(dòng)化的產(chǎn)出我們就來(lái)從最初的地方開(kāi)始講尿这,數(shù)據(jù)庫(kù)廉丽!
無(wú)論你是用啥寫(xiě)服務(wù)器如果還手寫(xiě)Model那真的只能說(shuō)你夠low我服!不過(guò)如果是手寫(xiě)Model自動(dòng)生成數(shù)據(jù)庫(kù)那另說(shuō)妻味,總的來(lái)說(shuō)就是要么從數(shù)據(jù)庫(kù)轉(zhuǎn)實(shí)體出來(lái),要么從實(shí)體轉(zhuǎn)數(shù)據(jù)庫(kù)這才有點(diǎn)意思欣福。
上邊只是開(kāi)玩笑下面正題责球,很多時(shí)候大家都無(wú)奈沒(méi)辦法不能說(shuō)low不low,其中奧妙各有體會(huì)拓劝,通常有點(diǎn)規(guī)模的團(tuán)隊(duì)都是先定義表然后就出Model了
后端的Model層
后端數(shù)據(jù)庫(kù)和實(shí)體互轉(zhuǎn)的方案都不用Google雏逾,百度就能出來(lái)一大推,有直接讀庫(kù)生成的郑临,也有從實(shí)體轉(zhuǎn)sql生成庫(kù)的栖博,更有提前定義協(xié)議然后開(kāi)始出對(duì)應(yīng)模塊對(duì)應(yīng)語(yǔ)言的實(shí)體及sql
移動(dòng)端的Model層
先繼續(xù)談會(huì)后臺(tái)Model,這里要說(shuō)的只是最好有一個(gè)提前定義的過(guò)程厢洞,這樣一方面規(guī)范開(kāi)發(fā)流程提前想好怎樣建庫(kù)合理仇让,一方面有利于跨平臺(tái)跨語(yǔ)言的開(kāi)發(fā),有了提前定義的協(xié)議躺翻,Model和枚舉的各平臺(tái)自動(dòng)生成就方便了很多丧叽,寫(xiě)個(gè)簡(jiǎn)單的小程序即可,類(lèi)型也就那幾類(lèi)公你。關(guān)鍵真的是一勞永逸踊淳,省去了大家互相校對(duì)的過(guò)程。
自動(dòng)化生成移動(dòng)端乃至前端用的Model層說(shuō)白了就是做個(gè)類(lèi)型映射陕靠,細(xì)說(shuō)的話基本就是分為兩類(lèi)迂尝,一類(lèi)直接就著后端現(xiàn)有項(xiàng)目讀Model層的文件,然后做個(gè)類(lèi)型的映射直接導(dǎo)出移動(dòng)端需要的類(lèi)剪芥,另一類(lèi)也就是設(shè)計(jì)數(shù)據(jù)庫(kù)時(shí)先定義Model的協(xié)議垄开,然后根據(jù)協(xié)議自動(dòng)生成各個(gè)平臺(tái)需要的實(shí)體,而協(xié)議定義通常用序列化后的數(shù)據(jù)如xml(極力抵制粗俱,結(jié)構(gòu)復(fù)雜)说榆,json,pb寸认,sql文件都能干這事签财。
Model自動(dòng)化實(shí)現(xiàn)
上面說(shuō)到了xml,json偏塞,pb唱蒸,sql文件都能干這件事,但其中最容易就是json灸叼,github上搜個(gè)json class基本就能有一大片總有你想要的語(yǔ)言神汹,但json的局限在于也就能轉(zhuǎn)換一下model庆捺,當(dāng)然通過(guò)特殊定義中間轉(zhuǎn)換,枚舉啥的也能搞定屁魏,在這我推薦pb滔以,首先它就是專(zhuān)門(mén)用來(lái)定義協(xié)議的,枚舉實(shí)體不用說(shuō)都能搞定氓拼,包括默認(rèn)值設(shè)置也能寫(xiě)出來(lái)你画,而且是谷歌出品本身是im通訊協(xié)議,被它序列化的數(shù)據(jù)在上面說(shuō)的里面算是最小的桃漾,而另一方面關(guān)于轉(zhuǎn)碼參考https://github.com/google/protobuf/blob/master/docs/third_party.md 坏匪,直接開(kāi)放了各個(gè)語(yǔ)言的轉(zhuǎn)換方法,當(dāng)然你根本用不到它里面寫(xiě)的那么復(fù)雜撬统,它的里面實(shí)體可是直接帶pb轉(zhuǎn)換model方法的适滓,如果不是開(kāi)發(fā)im根本用不到,要?jiǎng)h部分源碼實(shí)現(xiàn)自己的需要也行恋追,凭迹。。苦囱。(下面講一下正經(jīng)方法)
Protobuf Convert轉(zhuǎn)碼
下面放代碼片段(反饋的人多放全的蕊苗,之所以不想放還有個(gè)原因是這邊實(shí)現(xiàn)有點(diǎn)粗)
這里以Python為例,只是因?yàn)榘惭b執(zhí)行方便所以選它
轉(zhuǎn)成OC的例子沿彭,之所以選OC因?yàn)槲揖褪莻€(gè)搞IOS的瞧柔。。哥蔚。寫(xiě)起來(lái)各個(gè)公司需求不同每次都要改改改糙箍。牵祟。。也就是這個(gè)原因懶得放全的了雹拄,因?yàn)橛玫娜苏f(shuō)到底還是需要手動(dòng)改成自己想要的,沒(méi)有通用的。摧阅。汰蓉。
#頭文件引用
import os
from optparse import OptionParser
#pb轉(zhuǎn)成方便處理的對(duì)象
from protoDef import *
#讀取pb的
from protoReader import ProtoReader as reader
#上邊兩個(gè)就不放源碼了,感興趣的人多再說(shuō),畢竟上邊的實(shí)現(xiàn)只是讀pb邏輯大家估計(jì)都有自己的好辦法
ENUM_TYPE = 'NSInteger'
#基礎(chǔ)類(lèi)型映射
typeMap = {
'int64': 'NSNumber',
'int32': 'NSNumber',
'string': 'NSString',
'bool': 'NSNumber',
'float': 'NSNumber',
'double': 'NSNumber',
}
#默認(rèn)值映射
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'
#寫(xiě)文件方法(就是一點(diǎn)點(diǎn)輸出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
#為對(duì)應(yīng)類(lèi)添加前綴做為命名空間(oc沒(méi)命名空間。沈撞。贷岸。)
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 = [] #數(shù)組內(nèi)實(shí)體類(lèi) 數(shù)組
dvlist = [] #默認(rèn)值 數(shù)組
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))
#默認(rèn)值設(shè)置
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轉(zhuǎn)換所以有了這個(gè)方法
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)
#寫(xiě)文件到本地
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 轉(zhuǎn)Model的邏輯贺纲,有了這個(gè)前提航闺,下邊Server服務(wù)層的自動(dòng)化就有了
Server服務(wù)層自動(dòng)化
這里可以引申一下基本上寫(xiě)功能時(shí)只要分成配置類(lèi)和啟動(dòng)器這樣,根據(jù)配置類(lèi)就可以實(shí)現(xiàn)自動(dòng)化了猴誊,這里就拿IOS我這的實(shí)現(xiàn)講潦刃。
源碼地址:https://github.com/heroims/ServerAPI
這里也只是簡(jiǎn)單說(shuō)一下
ServerAPI 定義一個(gè)請(qǐng)求的地址,重試次數(shù)懈叹,返回?cái)?shù)據(jù)轉(zhuǎn)換模式
ServerAPIManager 根據(jù)ServerAPI發(fā)起請(qǐng)求
ServerAPIProtocol 定義需要實(shí)現(xiàn)的方法(為了擴(kuò)展性高乖杠,這里定義必須實(shí)現(xiàn)的協(xié)議,具體需要定制的需求通過(guò)Category實(shí)現(xiàn)相關(guān)方法)
ServerResult 返回的通用型實(shí)體包含解析的字典澄成,錯(cuò)誤信息等
總的思路就是ServerAPI來(lái)定義一個(gè)請(qǐng)求的具體內(nèi)容參數(shù)胧洒,而ServerAPIManager負(fù)責(zé)發(fā)起請(qǐng)求返回?cái)?shù)據(jù),然后就只需要繼承ServerAPI對(duì)不同請(qǐng)求具體參數(shù)直接返回具體的值即可墨状,比如requestHost略荡,resultFormat,retryTimes歉胶,timeOut汛兜,returnClass
回到Protobuf這個(gè)就相當(dāng)于定義request,但差別還是很大有了對(duì)一個(gè)API的描述通今,那么移動(dòng)端包括后端粥谬,前端都可以通過(guò)這個(gè)描述來(lái)做對(duì)應(yīng)的事情,只需要封裝一個(gè)東西去處理描述辫塌,至此就完成了Server服務(wù)層的自動(dòng)化漏策,外加說(shuō)一句后端的話為了性能可能更好的方案是根據(jù)描述生成代碼吧,用代碼寫(xiě)代碼才是正道臼氨。掺喻。。。