移动端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服务层的自动化,外加说一句后端的话为了性能可能更好的方案是根据描述生成代码吧,用代码写代码才是正道。。。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容