Статьи

Использование Groovy для генерации Objective-C

Я в процессе портирования приложения Windows Mobile на iPhone для местной компании. Существующее приложение Windows Mobile использует локальную базу данных для хранения всего, а затем происходит ручная синхронизация, когда пользователи имеют какое-то подключение. Затем данные передаются на материнский корабль, а обновления и новые данные передаются на устройство. Таблицы совсем не реляционные. Данные дублируются, где это возможно, и каждая таблица более или менее представляет форму.

Из-за нехватки времени и с учетом того, что компания приступила к разработке после того, как я выпустил версию 1.0, я решил, что попытаться улучшить эту модель было невозможно. Подобный подход был самым быстрым решением, и я верил, что другие разработчики смогут его понять.

 Короче говоря, мне нужно было несколько классов, которые представляли таблицы. Эти классы также будут следовать шаблону Active Record, так как доступ к данным будет осуществляться через экземпляры этих классов, и они будут действовать непосредственно на свои соответствующие таблицы в базе данных. После создания примерно двух из этих классов (как файла заголовка, так и файла класса) я искал лучший способ. Мне нужно было около 40 из них, и я уже устал набирать текст и делать много глупых ошибок. Введите GroTy’s SimpleTemplateEngine.

Сначала давайте посмотрим, что мне нужно было сгенерировать. Заголовочный файл:

#import <Foundation/Foundation.h>
#import "BaseDataObj.h"

@interface User : BaseDataObj {

NSString *userId;
NSString *password;
NSString *userType;
NSString *prefix;
NSString *firstName;
NSString *lastName;
NSString *middleName;
NSString *suffix;
NSString *address;
NSString *address2;
NSString *city;
NSString *state;
NSString *zipCode;
NSString *sex;
NSString *dateOfBirth;
NSString *emailAddress;
NSString *staffNo;
NSString *useAuthentic;
NSString *neworkName;
NSString *networkDomain;
NSString *sProviderId;
NSString *wlkstGrp;

}

@property (nonatomic, retain) NSString *userId;
@property (nonatomic, retain) NSString *password;
@property (nonatomic, retain) NSString *userType;
@property (nonatomic, retain) NSString *prefix;
@property (nonatomic, retain) NSString *firstName;
@property (nonatomic, retain) NSString *lastName;
@property (nonatomic, retain) NSString *middleName;
@property (nonatomic, retain) NSString *suffix;
@property (nonatomic, retain) NSString *address;
@property (nonatomic, retain) NSString *address2;
@property (nonatomic, retain) NSString *city;
@property (nonatomic, retain) NSString *state;
@property (nonatomic, retain) NSString *zipCode;
@property (nonatomic, retain) NSString *sex;
@property (nonatomic, retain) NSString *dateOfBirth;
@property (nonatomic, retain) NSString *emailAddress;
@property (nonatomic, retain) NSString *staffNo;
@property (nonatomic, retain) NSString *useAuthentic;
@property (nonatomic, retain) NSString *neworkName;
@property (nonatomic, retain) NSString *networkDomain;
@property (nonatomic, retain) NSString *sProviderId;
@property (nonatomic, retain) NSString *wlkstGrp;

@end

И файл класса:

#import "User.h"
#import "DBManager.h"

@implementation User
@synthesize userId;
@synthesize password;
@synthesize userType;
@synthesize prefix;
@synthesize firstName;
@synthesize lastName;
@synthesize middleName;
@synthesize suffix;
@synthesize address;
@synthesize address2;
@synthesize city;
@synthesize state;
@synthesize zipCode;
@synthesize sex;
@synthesize dateOfBirth;
@synthesize emailAddress;
@synthesize staffNo;
@synthesize useAuthentic;
@synthesize neworkName;
@synthesize networkDomain;
@synthesize sProviderId;
@synthesize wlkstGrp;

-(id)init {

if (self = [super init]) {
}
return self;
}

#pragma mark NSCoding
-(void) encodeWithCoder:(NSCoder *) encoder {
[encoder encodeObject:userId forKey:@"userId"];
[encoder encodeObject:password forKey:@"password"];
[encoder encodeObject:userType forKey:@"userType"];
[encoder encodeObject:prefix forKey:@"prefix"];
[encoder encodeObject:firstName forKey:@"firstName"];
[encoder encodeObject:lastName forKey:@"lastName"];
[encoder encodeObject:middleName forKey:@"middleName"];
[encoder encodeObject:suffix forKey:@"suffix"];
[encoder encodeObject:address forKey:@"address"];
[encoder encodeObject:address2 forKey:@"address2"];
[encoder encodeObject:city forKey:@"city"];
[encoder encodeObject:state forKey:@"state"];
[encoder encodeObject:zipCode forKey:@"zipCode"];
[encoder encodeObject:sex forKey:@"sex"];
[encoder encodeObject:dateOfBirth forKey:@"dateOfBirth"];
[encoder encodeObject:emailAddress forKey:@"emailAddress"];
[encoder encodeObject:staffNo forKey:@"staffNo"];
[encoder encodeObject:useAuthentic forKey:@"useAuthentic"];
[encoder encodeObject:neworkName forKey:@"neworkName"];
[encoder encodeObject:networkDomain forKey:@"networkDomain"];
[encoder encodeObject:sProviderId forKey:@"sProviderId"];
[encoder encodeObject:wlkstGrp forKey:@"wlkstGrp"];

}

-(id) initWithCoder:(NSCoder *) decoder {
if (self = [super init]) {
self.userId = [decoder decodeObjectForKey:@"userId"];
self.password = [decoder decodeObjectForKey:@"password"];
self.userType = [decoder decodeObjectForKey:@"userType"];
self.prefix = [decoder decodeObjectForKey:@"prefix"];
self.firstName = [decoder decodeObjectForKey:@"firstName"];
self.lastName = [decoder decodeObjectForKey:@"lastName"];
self.middleName = [decoder decodeObjectForKey:@"middleName"];
self.suffix = [decoder decodeObjectForKey:@"suffix"];
self.address = [decoder decodeObjectForKey:@"address"];
self.address2 = [decoder decodeObjectForKey:@"address2"];
self.city = [decoder decodeObjectForKey:@"city"];
self.state = [decoder decodeObjectForKey:@"state"];
self.zipCode = [decoder decodeObjectForKey:@"zipCode"];
self.sex = [decoder decodeObjectForKey:@"sex"];
self.dateOfBirth = [decoder decodeObjectForKey:@"dateOfBirth"];
self.emailAddress = [decoder decodeObjectForKey:@"emailAddress"];
self.staffNo = [decoder decodeObjectForKey:@"staffNo"];
self.useAuthentic = [decoder decodeObjectForKey:@"useAuthentic"];
self.neworkName = [decoder decodeObjectForKey:@"neworkName"];
self.networkDomain = [decoder decodeObjectForKey:@"networkDomain"];
self.sProviderId = [decoder decodeObjectForKey:@"sProviderId"];
self.wlkstGrp = [decoder decodeObjectForKey:@"wlkstGrp"];
}
return self;
}

#pragma mark -
#pragma mark NSCopying
-(id) copyWithZone:(NSZone *) zone {
User *copy = [[[self class] allocWithZone: zone] init];
userId = [self.userId copy];
password = [self.password copy];
userType = [self.userType copy];
prefix = [self.prefix copy];
firstName = [self.firstName copy];
lastName = [self.lastName copy];
middleName = [self.middleName copy];
suffix = [self.suffix copy];
address = [self.address copy];
address2 = [self.address2 copy];
city = [self.city copy];
state = [self.state copy];
zipCode = [self.zipCode copy];
sex = [self.sex copy];
dateOfBirth = [self.dateOfBirth copy];
emailAddress = [self.emailAddress copy];
staffNo = [self.staffNo copy];
useAuthentic = [self.useAuthentic copy];
neworkName = [self.neworkName copy];
networkDomain = [self.networkDomain copy];
sProviderId = [self.sProviderId copy];
wlkstGrp = [self.wlkstGrp copy];

return copy;
}

- (void)dealloc {

[userId release];
[password release];
[userType release];
[prefix release];
[firstName release];
[lastName release];
[middleName release];
[suffix release];
[address release];
[address2 release];
[city release];
[state release];
[zipCode release];
[sex release];
[dateOfBirth release];
[emailAddress release];
[staffNo release];
[useAuthentic release];
[neworkName release];
[networkDomain release];
[sProviderId release];
[wlkstGrp release];

[super dealloc];
}

@end

 Я думаю, вы поймете, почему написание 40 из них потребует от меня психологической помощи. XCode, насколько я знаю, мне здесь тоже не поможет. Первое, что мне нужно было сделать, это определить, как я собираюсь указать все свойства. Что-то слишком многословное победило бы цель. Я решил использовать ConfigSlurper для чтения файла конфигурации всех моих свойств. Вот как выглядит этот файл

descriptions {
USERS {
objName = "User"
columns {
USER_ID = "text"
PASSWORD = "text"
USER_TYPE = "text"
PREFIX = "text"
FIRST_NAME = "text"
LAST_NAME = "text"
MIDDLE_NAME = "text"
SUFFIX = "text"
ADDRESS = "text"
ADDRESS_2 = "text"
CITY = "text"
STATE = "text"
ZIP_CODE = "text"
SEX = "text"
DATE_OF_BIRTH = "text"
EMAIL_ADDRESS = "text"
STAFF_NO = "text"
USE_AUTHENTIC = "text"
NEWORK_NAME = "text"
NETWORK_DOMAIN = "text"
S_PROVIDER_ID = "text"
WLKST_GRP = "text"
}
}
}

Обратите внимание, что для каждой таблицы в базе данных будет следовать другой раздел после «USER». По причинам, которые я решил не объяснять в этой статье, свойства в файле конфигурации представляют имя таблицы, имена столбцов и типы столбцов. Читать эту конфигурацию довольно просто:

// an array of all the tables I need to read
def tables = ['USERS']

// TableDescriptions is the name of the config file
def configObject = new ConfigSlurper().parse(TableDescriptions)

tables.each() {
// get the column data for this table
def columnData = configObject.descriptions."${it}".columns
// do something with it
}

Теперь вопрос в том, что мы делаем с этими данными? Первое, что мне нужно было сделать, это преобразовать свойство типа NETWORK_NAME в networkName.

def camalCase(key) {
def bits = key.split('_')
def newKey = ""
bits.eachWithIndex() {obj, index ->
if (index == 0) {
newKey += obj.toLowerCase()
}else{
def fLetter = obj.substring(0,1)
newKey += fLetter.toUpperCase()
newKey += obj.substring(1, obj.length()).toLowerCase()
}
}
return newKey
}

Теперь я могу использовать этот метод, а также некоторый другой код для генерации моих заголовочных файлов и файлов классов. Каждый файл .h и .m имеет шаблон, который используется SimpleTemplateEngine.

#import <Foundation/Foundation.h>
#import "BaseDataObj.h"

@interface $className : BaseDataObj {

$variables
}

$properties

@end
#import "$className.h"
#import "DBManager.h"

@implementation $className
$synthesizers

-(id)init {
return self;
}

#pragma mark NSCoding
-(void) encodeWithCoder:(NSCoder *) encoder {
$encoders
}

-(id) initWithCoder:(NSCoder *) decoder {
if (self = [super init]) {
$decoders
}
return self;
}

#pragma mark -
#pragma mark NSCopying
-(id) copyWithZone:(NSZone *) zone {
$className *copy = [[[self class] allocWithZone: zone] init];
$zoneCopies
return copy;
}

- (void)dealloc {
$deallocs
[super dealloc];
}

@end

С этими шаблонами я могу начать генерировать код, который заменит переменные $ в шаблонах.

tables.each() {
def objName = configObject.descriptions."${it}".objName
def table = "${it}"
def columnData = configObject.descriptions."${it}".columns
createHeaderFromTemplate(objName, columnData)
createImplFromTemplate(objName, table, columnData)
}

def createHeaderFromTemplate(objName, columnData){

def variables = ""
columnData.each() { key, value ->
def newKey = camalCase(key)
if (value.equals('text')) {
variables += "NSString *${newKey};\n\t"
}else if (value.equals('integer')) {
variables += "NSInteger ${newKey};\n\t"
}
}

def properties = ""

columnData.each() { key, value ->
def newKey = camalCase(key)
if (value.equals('text')) {
properties += "@property (nonatomic, retain) NSString *${newKey};\n"
}else{
properties += "@property NSInteger ${newKey};\n"
}
}

def binding = ['className':objName, 'variables':variables, 'properties':properties]
def template = new File('templates/header.h').getText()

def engine = new SimpleTemplateEngine()
def output = engine.createTemplate(template).make(binding)

new File("data/${objName}.h").text = output

}

Я создаю строку для каждой переменной в шаблоне, которая будет заменена, и перебираю данные столбца для генерации всего кода. Затем я создаю карту привязок, получаю содержимое моего файла шаблона, создаю экземпляр нового SimpleTemplateEngine и позволяю ему выполнять всю работу по поиску / замене. Затем я просто сохраняю вывод в файл с соответствующим именем на основе данных из ConfigSlurper.

Этот код генерирует файл заголовка. Подобный код используется для генерации файла класса. Я опустил это для краткости. На первый взгляд это похоже на большой код для генерации кода. Но конечный результат — у меня есть готовая структура для любого шаблона, который мне нужен, и я экономлю кучу текста (помните, 40+ файлов для генерации, некоторые с более чем 30 свойствами) и множество типографских ошибок. Все это стало возможным благодаря огромным возможностям ConfigSlurper и SimpleTemplateEngine в Groovy.