sirve - Los mejores enfoques arquitectónicos para construir aplicaciones de redes iOS(clientes REST)
rest y json (11)
Soy un desarrollador de iOS con alguna experiencia y esta pregunta es realmente interesante para mí. Vi muchos recursos y materiales diferentes sobre este tema, pero sin embargo todavía estoy confundido. ¿Cuál es la mejor arquitectura para una aplicación de red iOS? Me refiero a un marco abstracto abstracto, patrones, que se ajustarán a cada aplicación de red, ya sea una aplicación pequeña que solo tiene unas pocas solicitudes de servidor o un complejo cliente REST. Apple recomienda utilizar MVC
como un enfoque arquitectónico básico para todas las aplicaciones de iOS, pero ni MVC
ni los patrones MVVM
más modernos explican dónde colocar el código de lógica de red y cómo organizarlo en general.
¿Debo desarrollar algo como MVCS
( S
for Service
) y en esta capa de Service
, colocar todas API
solicitudes de API
y otras lógicas de red, que en perspectiva pueden ser realmente complejas? Después de hacer algunas investigaciones encontré dos enfoques básicos para esto. Here se recomendó crear una clase separada para cada solicitud de red a la API
servicio web (como la clase LoginRequest
o la clase PostCommentRequest
, etc.) que se hereda de la clase AbstractBaseRequest
base abstracta AbstractBaseRequest
y, además, crear un administrador de red global que encapsule código de red común y otras preferencias (puede ser la personalización de la AFNetworking
o el ajuste de RestKit
, si tenemos asignaciones complejas y persistencia de objetos, o incluso una implementación de comunicación de red propia con API estándar). Pero este enfoque parece una sobrecarga para mí. Otro enfoque es tener algún despachador de la API
singleton o clase de administrador como en el primer enfoque, pero no crear clases para cada solicitud y, en cambio, encapsular cada solicitud como un método público de instancia de esta clase de administrador como: fetchContacts
, métodos de loginUser
fetchContacts
loginUser
, etc. Entonces, ¿cuál es la mejor y correcta manera? ¿Hay otros enfoques interesantes que no conozco todavía?
¿Y debería crear otra capa para todas estas cosas de redes como Service
, o Capa de NetworkProvider
red o lo que sea que esté en la parte superior de mi arquitectura MVC
, o esta capa debería estar integrada (inyectada) en las capas MVC
existentes, por ejemplo, Model
?
Sé que existen enfoques hermosos, ¿o cómo entonces esos monstruos móviles como el cliente de Facebook o el cliente de LinkedIn lidian con la creciente complejidad de la lógica de la red?
Sé que no hay una respuesta exacta y formal al problema. El objetivo de esta pregunta es recopilar los enfoques más interesantes de los desarrolladores de iOS con experiencia . El mejor enfoque sugerido se marcará como aceptado y se otorgará con una recompensa de reputación, los demás se votarán. Es sobre todo una pregunta teórica y de investigación. Quiero entender el enfoque arquitectónico básico, abstracto y correcto para las aplicaciones de red en iOS. Espero una explicación detallada de desarrolladores experimentados.
De acuerdo con el objetivo de esta pregunta, me gustaría describir nuestro enfoque de arquitectura.
Enfoque de la arquitectura
La arquitectura general de nuestra aplicación iOS se MVVM en los siguientes patrones: capas de servicio , MVVM , enlace de datos de UI , inyección de dependencia ; y paradigma de la programación reactiva funcional .
Podemos dividir una aplicación típica orientada al consumidor en las siguientes capas lógicas:
- Montaje
- Modelo
- Servicios
- Almacenamiento
- Gerentes
- Coordinadores
- UI
- Infraestructura
La capa de ensamblaje es un punto de arranque de nuestra aplicación. Contiene un contenedor de inyección de dependencias y declaraciones de los objetos de la aplicación y sus dependencias. Esta capa también puede contener la configuración de la aplicación (URL, claves de servicios de terceros, etc.). Para ello utilizamos la biblioteca Typhoon .
Capa de modelo contiene clases de modelos de dominio, validaciones, mapeos. Utilizamos la biblioteca Mantle para mapear nuestros modelos: admite la serialización / deserialización en formato JSON
y modelos NSManagedObject
. Para la validación y representación de nuestros modelos, usamos las bibliotecas FXForms y FXModelValidation .
La capa de servicios declara los servicios que utilizamos para interactuar con sistemas externos con el fin de enviar o recibir datos representados en nuestro modelo de dominio. Por lo general, contamos con servicios de comunicación con las API del servidor (por entidad), servicios de mensajería (como PubNub ), servicios de almacenamiento (como Amazon S3), etc. Los servicios envuelven los objetos proporcionados por los SDK (por ejemplo, PubNub SDK) o implementamos su propia comunicación lógica. Para redes generales usamos la biblioteca AFNetworking .
El propósito de la capa de almacenamiento es organizar el almacenamiento de datos local en el dispositivo. Usamos Core Data o Realm para esto (ambos tienen sus pros y sus contras, la decisión de qué usar se basa en especificaciones concretas). Para la configuración de Core Data MDMCoreData biblioteca MDMCoreData y un montón de clases (almacenamientos) (similares a los servicios) que brindan acceso al almacenamiento local para cada entidad. Para Realm solo usamos almacenamientos similares para tener acceso al almacenamiento local.
La capa de administradores es un lugar donde viven nuestras abstracciones / envoltorios.
En un rol de gerente podría ser:
- Credentials Manager con sus diferentes implementaciones (llavero, NSDefaults, ...)
- Administrador de sesión actual que sabe cómo mantener y proporcionar sesión de usuario actual
- Capture Pipeline que proporciona acceso a dispositivos multimedia (grabación de video, audio, toma de fotografías)
- BLE Manager que proporciona acceso a servicios de bluetooth y periféricos
- Geo Location Manager
- ...
Por lo tanto, en función de administrador podría ser cualquier objeto que implemente la lógica de un aspecto particular o una preocupación necesaria para el funcionamiento de la aplicación.
Intentamos evitar los Singletons, pero esta capa es un lugar donde viven si son necesarios.
La capa de coordinadores proporciona objetos que dependen de los objetos de otras capas (servicio, almacenamiento, modelo) para combinar su lógica en una secuencia de trabajo necesaria para cierto módulo (característica, pantalla, historia del usuario o experiencia del usuario). Generalmente encadena operaciones asíncronas y sabe cómo reaccionar en sus casos de éxito y fracaso. Como ejemplo, puede imaginar una función de mensajería y el correspondiente objeto MessagingCoordinator
. El manejo de la operación de envío de mensajes podría verse así:
- Validar mensaje (capa de modelo)
- Guardar mensaje localmente (almacenamiento de mensajes)
- Adjuntar mensaje adjunto (servicio amazon s3)
- Actualice el estado de los mensajes y las direcciones URL de los adjuntos y guarde el mensaje localmente (almacenamiento de mensajes)
- Mensaje de serialización a formato JSON (capa de modelo)
- Publicar mensaje a PubNub (servicio PubNub)
- Actualice el estado y los atributos del mensaje y guárdelo localmente (almacenamiento de mensajes)
En cada uno de los pasos anteriores se maneja un error de manera correspondiente.
La capa de interfaz de usuario consta de las siguientes subcapas:
- ViewModels
- ViewControllers
- Puntos de vista
Para evitar los controladores de vista masiva, usamos el patrón MVVM e implementamos la lógica necesaria para la presentación de la interfaz de usuario en ViewModels. Un ViewModel usualmente tiene coordinadores y gerentes como dependencias. ViewModels utilizados por ViewControllers y algunos tipos de Vistas (por ejemplo, celdas de vista de tabla). El pegamento entre ViewControllers y ViewModels es el patrón de enlace y comando de datos. Para que sea posible tener ese pegamento, utilizamos la biblioteca ReactiveCocoa .
También utilizamos ReactiveCocoa y su concepto RACSignal
como interfaz y tipo de valor de retorno de todos los coordinadores, servicios, métodos de almacenamiento. Esto nos permite encadenar operaciones, ejecutarlas en paralelo o en serie, y muchas otras cosas útiles proporcionadas por ReactiveCocoa.
Intentamos implementar nuestro comportamiento UI de manera declarativa. El enlace de datos y el diseño automático ayudan mucho para lograr este objetivo.
La capa de infraestructura contiene todos los ayudantes, extensiones, utilidades necesarias para el trabajo de la aplicación.
Este enfoque funciona bien para nosotros y para los tipos de aplicaciones que usualmente creamos. Pero debe comprender que este es solo un enfoque subjetivo que debe adaptarse / cambiarse para el propósito concreto del equipo.
¡Espero que esto te ayudará!
También puede encontrar más información sobre el proceso de desarrollo de iOS en esta publicación de blog Desarrollo de iOS como servicio
Debido a que todas las aplicaciones de iOS son diferentes, creo que hay diferentes enfoques que se deben considerar aquí, pero generalmente lo hago de esta manera:
Cree una clase de administrador central (singleton) para manejar todas las solicitudes de API (generalmente llamada APICommunicator) y cada método de instancia es una llamada a API. Y hay un método central (no público):
-
(RACSignal *)sendGetToServerToSubPath:(NSString *)path withParameters:(NSDictionary *)params;
Para el registro, uso 2 bibliotecas / marcos principales, ReactiveCocoa y AFNetworking. ReactiveCocoa maneja las respuestas de red asíncronas a la perfección, puedes hacerlo (sendNext :, sendError :, etc.).
Este método llama a la API, obtiene los resultados y los envía a través de RAC en formato "sin formato" (como NSArray lo que devuelve AFNetworking).
Luego, un método como getStuffList:
que llamado el método anterior se suscribe a su señal, analiza los datos en bruto en objetos (con algo como Motis) y envía los objetos uno por uno al llamante ( getStuffList:
y métodos similares también devuelven una señal de que controlador puede suscribirse a).
El controlador suscrito recibe los objetos mediante subscribeNext:
''s block y los maneja.
Probé de muchas maneras en diferentes aplicaciones, pero esta funcionó de la mejor manera, así que he estado usando esto en algunas aplicaciones recientemente, se adapta a proyectos grandes y pequeños y es fácil de ampliar y mantener si es necesario modificar algo.
Espero que esto ayude, me gustaría escuchar las opiniones de otros acerca de mi enfoque y tal vez cómo otros piensan que esto podría mejorarse.
En mi situación, normalmente uso la biblioteca ResKit para configurar la capa de red. Proporciona un análisis fácil de usar. Reduce mi esfuerzo en la configuración del mapeo para diferentes respuestas y cosas.
Solo agrego algo de código para configurar el mapeo automáticamente. Defino la clase base para mis modelos (no protocolo debido a la gran cantidad de código para verificar si algún método se implementa o no, y menos código en los modelos en sí):
MappableEntry.h
@interface MappableEntity : NSObject
+ (NSArray*)pathPatterns;
+ (NSArray*)keyPathes;
+ (NSArray*)fieldsArrayForMapping;
+ (NSDictionary*)fieldsDictionaryForMapping;
+ (NSArray*)relationships;
@end
MappableEntry.m
@implementation MappableEntity
+(NSArray*)pathPatterns {
return @[];
}
+(NSArray*)keyPathes {
return nil;
}
+(NSArray*)fieldsArrayForMapping {
return @[];
}
+(NSDictionary*)fieldsDictionaryForMapping {
return @{};
}
+(NSArray*)relationships {
return @[];
}
@end
Las relaciones son objetos que representan objetos anidados en respuesta:
RelationshipObject.h
@interface RelationshipObject : NSObject
@property (nonatomic,copy) NSString* source;
@property (nonatomic,copy) NSString* destination;
@property (nonatomic) Class mappingClass;
+(RelationshipObject*)relationshipWithKey:(NSString*)key andMappingClass:(Class)mappingClass;
+(RelationshipObject*)relationshipWithSource:(NSString*)source destination:(NSString*)destination andMappingClass:(Class)mappingClass;
@end
RelationshipObject.m
@implementation RelationshipObject
+(RelationshipObject*)relationshipWithKey:(NSString*)key andMappingClass:(Class)mappingClass {
RelationshipObject* object = [[RelationshipObject alloc] init];
object.source = key;
object.destination = key;
object.mappingClass = mappingClass;
return object;
}
+(RelationshipObject*)relationshipWithSource:(NSString*)source destination:(NSString*)destination andMappingClass:(Class)mappingClass {
RelationshipObject* object = [[RelationshipObject alloc] init];
object.source = source;
object.destination = destination;
object.mappingClass = mappingClass;
return object;
}
@end
Entonces estoy configurando el mapeo para RestKit así:
ObjectMappingInitializer.h
@interface ObjectMappingInitializer : NSObject
+(void)initializeRKObjectManagerMapping:(RKObjectManager*)objectManager;
@end
ObjectMappingInitializer.m
@interface ObjectMappingInitializer (Private)
+ (NSArray*)mappableClasses;
@end
@implementation ObjectMappingInitializer
+(void)initializeRKObjectManagerMapping:(RKObjectManager*)objectManager {
NSMutableDictionary *mappingObjects = [NSMutableDictionary dictionary];
// Creating mappings for classes
for (Class mappableClass in [self mappableClasses]) {
RKObjectMapping *newMapping = [RKObjectMapping mappingForClass:mappableClass];
[newMapping addAttributeMappingsFromArray:[mappableClass fieldsArrayForMapping]];
[newMapping addAttributeMappingsFromDictionary:[mappableClass fieldsDictionaryForMapping]];
[mappingObjects setObject:newMapping forKey:[mappableClass description]];
}
// Creating relations for mappings
for (Class mappableClass in [self mappableClasses]) {
RKObjectMapping *mapping = [mappingObjects objectForKey:[mappableClass description]];
for (RelationshipObject *relation in [mappableClass relationships]) {
[mapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:relation.source toKeyPath:relation.destination withMapping:[mappingObjects objectForKey:[relation.mappingClass description]]]];
}
}
// Creating response descriptors with mappings
for (Class mappableClass in [self mappableClasses]) {
for (NSString* pathPattern in [mappableClass pathPatterns]) {
if ([mappableClass keyPathes]) {
for (NSString* keyPath in [mappableClass keyPathes]) {
[objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:[mappingObjects objectForKey:[mappableClass description]] method:RKRequestMethodAny pathPattern:pathPattern keyPath:keyPath statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];
}
} else {
[objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:[mappingObjects objectForKey:[mappableClass description]] method:RKRequestMethodAny pathPattern:pathPattern keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];
}
}
}
// Error Mapping
RKObjectMapping *errorMapping = [RKObjectMapping mappingForClass:[Error class]];
[errorMapping addAttributeMappingsFromArray:[Error fieldsArrayForMapping]];
for (NSString *pathPattern in Error.pathPatterns) {
[[RKObjectManager sharedManager] addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:errorMapping method:RKRequestMethodAny pathPattern:pathPattern keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassClientError)]];
}
}
@end
@implementation ObjectMappingInitializer (Private)
+ (NSArray*)mappableClasses {
return @[
[FruiosPaginationResults class],
[FruioItem class],
[Pagination class],
[ContactInfo class],
[Credentials class],
[User class]
];
}
@end
Algunos ejemplos de implementación de MappableEntry:
Usuario.h
@interface User : MappableEntity
@property (nonatomic) long userId;
@property (nonatomic, copy) NSString *username;
@property (nonatomic, copy) NSString *email;
@property (nonatomic, copy) NSString *password;
@property (nonatomic, copy) NSString *token;
- (instancetype)initWithUsername:(NSString*)username email:(NSString*)email password:(NSString*)password;
- (NSDictionary*)registrationData;
@end
Usuario.m
@implementation User
- (instancetype)initWithUsername:(NSString*)username email:(NSString*)email password:(NSString*)password {
if (self = [super init]) {
self.username = username;
self.email = email;
self.password = password;
}
return self;
}
- (NSDictionary*)registrationData {
return @{
@"username": self.username,
@"email": self.email,
@"password": self.password
};
}
+ (NSArray*)pathPatterns {
return @[
[NSString stringWithFormat:@"/api/%@/users/register", APIVersionString],
[NSString stringWithFormat:@"/api/%@/users/login", APIVersionString]
];
}
+ (NSArray*)fieldsArrayForMapping {
return @[ @"username", @"email", @"password", @"token" ];
}
+ (NSDictionary*)fieldsDictionaryForMapping {
return @{ @"id": @"userId" };
}
@end
Ahora sobre el envoltorio de solicitudes:
Tengo un archivo de encabezado con definición de bloques, para reducir la longitud de la línea en todas las clases de APIRequest:
APICallbacks.h
typedef void(^SuccessCallback)();
typedef void(^SuccessCallbackWithObjects)(NSArray *objects);
typedef void(^ErrorCallback)(NSError *error);
typedef void(^ProgressBlock)(float progress);
Y ejemplo de mi clase APIRequest que estoy usando:
LoginAPI.h
@interface LoginAPI : NSObject
- (void)loginWithCredentials:(Credentials*)credentials onSuccess:(SuccessCallbackWithObjects)onSuccess onError:(ErrorCallback)onError;
@end
LoginAPI.m
@implementation LoginAPI
- (void)loginWithCredentials:(Credentials*)credentials onSuccess:(SuccessCallbackWithObjects)onSuccess onError:(ErrorCallback)onError {
[[RKObjectManager sharedManager] postObject:nil path:[NSString stringWithFormat:@"/api/%@/users/login", APIVersionString] parameters:[credentials credentialsData] success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
onSuccess(mappingResult.array);
} failure:^(RKObjectRequestOperation *operation, NSError *error) {
onError(error);
}];
}
@end
Y todo lo que necesita hacer en el código, simplemente inicialice el objeto API y llámelo cuando lo necesite:
SomeViewController.m
@implementation SomeViewController {
LoginAPI *_loginAPI;
// ...
}
- (void)viewDidLoad {
[super viewDidLoad];
_loginAPI = [[LoginAPI alloc] init];
// ...
}
// ...
- (IBAction)signIn:(id)sender {
[_loginAPI loginWithCredentials:_credentials onSuccess:^(NSArray *objects) {
// Success Block
} onError:^(NSError *error) {
// Error Block
}];
}
// ...
@end
Mi código no es perfecto, pero es fácil de configurar una vez y usarlo para diferentes proyectos. Si es interesante para cualquiera, mb podría pasar un tiempo y hacer una solución universal para él en algún lugar de GitHub y CocoaPods.
I want to understand basic, abstract and correct architectural approach for networking applications in iOS
: no existe el enfoque "mejor" o "el más correcto" para construir una arquitectura de aplicación. Es un trabajo muy creativo. Siempre debe elegir la arquitectura más sencilla y extensible, que será clara para cualquier desarrollador, que comience a trabajar en su proyecto o para otros desarrolladores de su equipo, pero estoy de acuerdo, que puede haber una "buena" y una "mala". "la arquitectura.
Usted dijo: collect the most interesting approaches from experienced iOS developers
, no creo que mi enfoque sea el más interesante o correcto, pero lo he usado en varios proyectos y estoy satisfecho con él. Es un enfoque híbrido de los que mencionó anteriormente, y también con mejoras de mis propios esfuerzos de investigación. Me interesan los problemas de los enfoques de construcción, que combinan varios patrones e idiomas bien conocidos. Creo que muchos de los patrones empresariales de Fowler pueden aplicarse con éxito a las aplicaciones móviles. Aquí hay una lista de las más interesantes, que podemos aplicar para crear una arquitectura de aplicación iOS ( en mi opinión ): Capa de servicio , Unidad de trabajo , Fachada remota , Objeto de transferencia de datos , Gateway , Supertipo de capa , Caso especial , Modelo de dominio . Siempre debe diseñar correctamente una capa de modelo y no olvidarse nunca de la persistencia (puede aumentar significativamente el rendimiento de su aplicación). Puedes usar Core Data
para esto. Pero no debe olvidar que los Core Data
no son un ORM o una base de datos, sino un administrador de gráficos de objetos con persistencia como una buena opción. Por lo tanto, muy a menudo los Core Data
pueden ser demasiado pesados para sus necesidades y puede buscar nuevas soluciones como Realm y Couchbase Lite , o crear su propia capa de persistencia / mapeo de objetos ligeros, basada en SQLite sin formato o LevelDB . También le aconsejo que se familiarice con el diseño impulsado por dominios y CQRS .
Al principio, creo que deberíamos crear otra capa para las redes, porque no queremos controladores pesados ni modelos pesados y abrumados. No creo en ese fat model, skinny controller
cosas. Pero sí creo en el enfoque de skinny everything
, porque ninguna clase debería ser gorda, nunca. Todas las redes se pueden abstraer generalmente como lógica de negocios, por lo tanto, deberíamos tener otra capa, donde podamos ubicarla. Service Layer es lo que necesitamos:
It encapsulates the application''s business logic, controlling transactions
and coordinating responses in the implementation of its operations.
En nuestra Service Layer
reino MVC
es algo así como un mediador entre el modelo de dominio y los controladores. Existe una variación bastante similar de este enfoque llamado MVCS donde una Store
es en realidad nuestra capa de Service
. Store
vendedores de la Store
modelan instancias y manejan las redes, el almacenamiento en caché, etc. Quiero mencionar que no debe escribir toda su red y lógica empresarial en su capa de servicio. Esto también puede ser considerado como un mal diseño. Para más información, mira los modelos de dominio Anemic y Rich . Algunos métodos de servicio y lógica empresarial pueden manejarse en el modelo, por lo que será un modelo "rico" (con comportamiento).
Siempre uso extensivamente dos bibliotecas: AFNetworking 2.0 y ReactiveCocoa . Creo que es una necesidad para cualquier aplicación moderna que interactúe con la red y los servicios web o contenga una lógica de interfaz de usuario compleja.
ARQUITECTURA
Al principio creo una clase general de APIClient
, que es una subclase de AFHTTPSessionManager . Este es un caballo de batalla de todas las redes en la aplicación: todas las clases de servicio delegan las solicitudes REST reales a ella. Contiene todas las personalizaciones del cliente HTTP, que necesito en la aplicación en particular: fijación de SSL, procesamiento de errores y creación de objetos NSError
directos con razones de falla detalladas y descripciones de todos los errores de conexión y API
(en tal caso, el controlador podrá mostrar correctamente mensajes para el usuario), configuración de serializadores de solicitud y respuesta, encabezados http y otras cosas relacionadas con la red. Luego UserSerivces
lógicamente todas las solicitudes de API en subservicios o, más correctamente, microservices : UserSerivces
, CommonServices
, SecurityServices
, FriendsServices
, etc., de acuerdo con la lógica empresarial que implementen. Cada uno de estos microservicios es una clase separada. Ellos, juntos, forman una Service Layer
. Estas clases contienen métodos para cada solicitud de API, procesan modelos de dominio y siempre devuelven una RACSignal
con el modelo de respuesta analizada o NSError
a la persona que llama.
Quiero mencionar que si tiene una lógica de serialización de modelo compleja, cree otra capa para ella: algo como el asignador de datos, pero más general, por ejemplo, JSON / XML -> asignador de modelos. Si tiene caché: créelo también como una capa / servicio separado (no debe mezclar la lógica empresarial con el almacenamiento en caché). ¿Por qué? Porque la capa de caché correcta puede ser bastante compleja con sus propios errores. La gente implementa lógica compleja para obtener un almacenamiento en caché válido y predecible como, por ejemplo, el almacenamiento en caché monoidal con proyecciones basadas en profunctores. Puedes leer sobre esta hermosa biblioteca llamada Carlos para entender más. Y no olvide que Core Data realmente puede ayudarlo con todos los problemas de almacenamiento en caché y le permitirá escribir menos lógica. Además, si tiene alguna lógica entre los modelos de solicitud de servidor y NSManagedObjectContext
, puede usar el patrón Repository , que separa la lógica que recupera los datos y los asigna al modelo de entidad de la lógica de negocios que actúa sobre el modelo. Por lo tanto, le aconsejo que utilice el patrón Repositorio incluso cuando tenga una arquitectura basada en Core Data. El repositorio puede abstraer cosas, como NSFetchRequest
, NSEntityDescription
, NSFetchRequest
, NSEntityDescription
, a métodos simples como get
o put
.
Después de todas estas acciones en la capa de Servicio, la persona que llama (controlador de vista) puede hacer algunas cosas asíncronas complejas con la respuesta: manipulaciones de señal, encadenamiento, mapeo, etc. con la ayuda de primitivas de ReactiveCocoa
, o simplemente suscribirse y mostrar resultados en el ver. Yo inyecto la inyección de dependencia en todas estas clases de servicio que mi APIClient
, que traducirá una llamada de servicio en particular al GET
correspondiente, POST
, PUT
, DELETE
, etc. solicitará al punto final REST. En este caso, APIClient
se pasa implícitamente a todos los controladores, puede hacerlo explícito con una APIClient
servicio parametrizado sobre APIClient
. Esto puede tener sentido si desea usar diferentes personalizaciones de APIClient
para clases de servicio particulares, pero si, por alguna razón, no desea copias adicionales o está seguro de que siempre usará una instancia particular (sin personalizaciones) de el APIClient
: APIClient
en un singleton, pero NO, por favor NO haga clases de servicio como singletons.
Luego, cada controlador de vista nuevamente con el DI inyecta la clase de servicio que necesita, llama a los métodos de servicio apropiados y compone sus resultados con la lógica de UI. Para la inyección de dependencia, me gusta usar BloodMagic o un framework Typhoon más potente. Nunca uso singletons, God APIManagerWhatever
clase u otras cosas incorrectas. Porque si llama a su clase WhateverManager
, esto indica que no conoce su propósito y es una mala elección de diseño . Singletons también es un anti-patrón, y en la mayoría de los casos (excepto los raros) es una solución incorrecta . Singleton debe considerarse solo si se cumplen los tres criterios siguientes:
- La propiedad de la instancia única no se puede asignar razonablemente;
- Inicialización perezosa es deseable;
- El acceso global no está previsto de otro modo.
En nuestro caso, la propiedad de la instancia única no es un problema y tampoco necesitamos acceso global después de dividir nuestro administrador de dios en servicios, porque ahora solo uno o varios controladores dedicados necesitan un servicio en particular (por ejemplo, el controlador UserProfile
necesita UserServices
y por lo tanto en).
Siempre debemos respetar el principio S
en SOLID y usar la separación de inquietudes , así que no coloque todos los métodos de servicio y llamadas de red en una clase, porque es una locura, especialmente si desarrolla una gran aplicación empresarial. Es por eso que debemos considerar la inyección de dependencia y el enfoque de servicios. Considero este enfoque como moderno y post-OO . En este caso, dividimos nuestra aplicación en dos partes: lógica de control (controladores y eventos) y parámetros.
Un tipo de parámetros serían los parámetros ordinarios de "datos". Eso es lo que transmitimos a las funciones, manipular, modificar, persistir, etc. Estas son entidades, agregados, colecciones, clases de casos. El otro tipo serían los parámetros de "servicio". Estas son clases que encapsulan la lógica empresarial, permiten la comunicación con sistemas externos y proporcionan acceso a los datos.
Aquí hay un flujo de trabajo general de mi arquitectura por ejemplo. Supongamos que tenemos un FriendsViewController
, que muestra la lista de amigos del usuario y tenemos una opción para eliminar de los amigos. Creo un método en mi clase FriendsServices
llamado:
- (RACSignal *)removeFriend:(Friend * const)friend
donde Friend
es un objeto de modelo / dominio (o puede ser solo un objeto User
si tienen atributos similares). Underhood este método analiza Friend
to NSDictionary
de JSON parámetros friend_id
, name
, surname
, friend_request_id
y así sucesivamente. Siempre uso la biblioteca Mantle para este tipo de repetición y para mi capa de modelo (análisis hacia adelante y hacia atrás, gestión de jerarquías de objetos anidados en JSON, etc.). Después de analizar, llama APIClient
método DELETE
APIClient
para realizar una solicitud REST real y devuelve la Response
en RACSignal
a la persona que llama ( FriendsViewController
en nuestro caso) para mostrar el mensaje apropiado para el usuario o lo que sea.
Si nuestra aplicación es muy grande, tenemos que separar nuestra lógica aún más claramente. Por ejemplo, no siempre es bueno mezclar el Repository
o la lógica del modelo con el Service
uno. Cuando describí mi enfoque, dije que el método removeFriend
debería estar en la capa de Service
, pero si vamos a ser más pedantes, podemos notar que pertenece mejor al Repository
. Recordemos qué es el Repositorio. Eric Evans le dio una descripción precisa en su libro [DDD]:
Un repositorio representa todos los objetos de un determinado tipo como un conjunto conceptual. Actúa como una colección, excepto con una capacidad de consulta más elaborada.
Por lo tanto, un Repository
es esencialmente una fachada que usa semántica de estilo de Colección (Agregar, Actualizar, Eliminar) para proporcionar acceso a datos / objetos. Es por eso que cuando tienes algo como: getFriendsList
, getUserGroups
, removeFriend
, puedes colocarlo en el Repository
, porque la semántica de colección es bastante clara aquí. Y código como:
- (RACSignal *)approveFriendRequest:(FriendRequest * const)request;
Definitivamente es una lógica de negocios, ya que está más allá de las CRUD
básicas de CRUD
y conecta dos objetos de dominio ( Friend
y Request
), por eso se debe colocar en la capa de Service
. También quiero notar: no cree abstracciones innecesarias . Utilice todos estos enfoques con prudencia. Porque si abrumará su aplicación con abstracciones, esto aumentará su complejidad accidental, y la complejidad causa más problemas en los sistemas de software que cualquier otra cosa.
Le describo un ejemplo "antiguo" de Objective-C, pero este enfoque puede adaptarse muy fácilmente al lenguaje Swift con muchas más mejoras, ya que tiene características más útiles y azúcar funcional. Recomiendo encarecidamente utilizar esta biblioteca: Moya . Le permite crear una capa más elegante de APIClient
(nuestro caballo de batalla como usted recuerda). Ahora nuestro proveedor de APIClient
será un tipo de valor (enum) con extensiones que se ajustan a los protocolos y que aprovechan la desestructuración de patrones. Las enums de Swift + la coincidencia de patrones nos permite crear tipos de datos algebraicos como en la programación funcional clásica. Nuestros microservicios utilizarán este proveedor de APIClient
mejorado como en el enfoque habitual de Objective-C. Para la capa de modelo en lugar de Mantle
, puede usar la biblioteca ObjectMapper o me gusta usar la biblioteca Argo más elegante y funcional.
Entonces, describí mi enfoque arquitectónico general, que se puede adaptar para cualquier aplicación, creo. Puede haber muchas más mejoras, por supuesto. Te aconsejo que aprendas programación funcional, porque puedes beneficiarte mucho de ella, pero no vayas demasiado lejos con ella. Eliminar un estado global mutable excesivo, compartido, crear un modelo de dominio inmutable o crear funciones puras sin efectos secundarios externos es, en general, una buena práctica, y el nuevo lenguaje Swift
alienta. Pero recuerde siempre que sobrecargar su código con patrones funcionales puros y pesados, los enfoques de categorías teóricas es una mala idea, ya que otros desarrolladores leerán y respaldarán su código, y pueden frustrarse o asustar a los prismatic profunctors
y todo tipo de cosas en Tu modelo inmutable. Lo mismo con el ReactiveCocoa
: no RACify
demasiado su código, ya que puede llegar a ser ilegible realmente rápido, especialmente para los novatos. Úsalo cuando realmente pueda simplificar tus objetivos y tu lógica.
Entonces, read a lot, mix, experiment, and try to pick up the best from different architectural approaches
. Es el mejor consejo que puedo darte.
Desde una perspectiva de diseño puramente de clase, por lo general tendrá algo como esto:
- Sus controladores de vista controlando una o más vistas.
Clase de modelo de datos : realmente depende de cuántas entidades distintas reales está tratando y cómo están relacionadas.
Por ejemplo, si tiene una matriz de elementos para mostrar en cuatro representaciones diferentes (lista, tabla, gráfico, etc.), tendrá una clase de modelo de datos para la lista de elementos, una más para un elemento. La lista de clases de elementos será compartida por cuatro controladores de vista: todos los hijos de un controlador de barra de pestañas o un controlador de navegación.
Las clases de modelos de datos serán útiles no solo para mostrar datos, sino también para serializarlos en donde cada uno de ellos puede exponer su propio formato de serialización a través de los métodos de exportación JSON / XML / CSV (o cualquier otra cosa).
Es importante comprender que también necesita clases de generador de solicitudes de API que se asignen directamente con sus puntos finales REST API. Supongamos que tiene una API que registra al usuario, por lo que su clase de generador de API de inicio de sesión creará la carga de POST JSON para la API de inicio de sesión. En otro ejemplo, una clase de generador de solicitudes de API para la lista de elementos del catálogo API creará la cadena de consulta GET para la API correspondiente y activará la consulta REST GET.
Estas clases de constructor de solicitudes de API generalmente recibirán datos de los controladores de vista y también pasarán los mismos datos para ver los controladores para la actualización de la interfaz de usuario / otras operaciones. Los controladores de vista luego decidirán cómo actualizar los objetos del modelo de datos con esos datos.
Finalmente, el corazón del cliente REST : la clase de captación de datos de API que no tiene en cuenta todo tipo de solicitudes de API que realiza su aplicación. Es probable que esta clase sea un singleton, pero como otros lo señalaron, no tiene que ser un singleton.
Tenga en cuenta que el enlace es solo una implementación típica y no tiene en cuenta escenarios como la sesión, las cookies, etc., pero es suficiente para que pueda comenzar a utilizar los marcos de terceros.
Esta pregunta ya tiene muchas respuestas excelentes y extensas, pero creo que tengo que mencionarla ya que nadie más lo ha hecho.
Alamofire para Swift. https://github.com/Alamofire/Alamofire
Es creado por las mismas personas que AFNetworking, pero está diseñado más directamente teniendo en cuenta a Swift.
En mi opinión, toda la arquitectura del software está impulsada por la necesidad. Si esto es para fines de aprendizaje o personales, entonces decida cuál es el objetivo principal y haga que conduzca la arquitectura. Si este es un trabajo para contratar, entonces la necesidad del negocio es primordial. El truco es no dejar que las cosas brillantes te distraigan de las necesidades reales. Encuentro esto difícil de hacer. Siempre hay cosas nuevas y brillantes que aparecen en este negocio y muchas de ellas no son útiles, pero no siempre se puede decir eso por adelantado. Concéntrese en la necesidad y esté dispuesto a abandonar las malas decisiones si puede.
Por ejemplo, recientemente hice un prototipo rápido de una aplicación para compartir fotos para un negocio local. Dado que la necesidad empresarial era hacer algo rápido y sucio, la arquitectura terminó siendo un código de iOS para mostrar una cámara y un código de red adjunto a un botón de envío que cargó la imagen a una tienda de S3 y escribió en un dominio SimpleDB. El código era trivial y el costo mínimo y el cliente tiene una colección de fotos escalable accesible a través de la web con llamadas REST. Barato y tonto, la aplicación tenía muchas fallas y en ocasiones bloquearía la interfaz de usuario, pero sería un desperdicio hacer más por un prototipo y les permite desplegar a su personal y generar miles de imágenes de prueba fácilmente sin rendimiento ni escalabilidad preocupaciones Arquitectura de mal gusto, pero se ajusta a la necesidad y al coste perfectamente.
Otro proyecto involucró la implementación de una base de datos segura local que se sincroniza con el sistema de la compañía en segundo plano cuando la red está disponible. Creé un sincronizador de fondo que usaba RestKit ya que parecía tener todo lo que necesitaba. Pero tuve que escribir tanto código personalizado para que RestKit tratara con JSON idiosyncratic que podría haberlo hecho todo más rápido al escribir mis propias transformaciones JSON a CoreData. Sin embargo, el cliente quería traer esta aplicación en casa y sentí que RestKit sería similar a los marcos que utilizaron en otras plataformas. Estoy esperando a ver si fue una buena decisión.
Una vez más, el problema para mí es centrarse en la necesidad y dejar que eso determine la arquitectura. Intento muchísimo evitar el uso de paquetes de terceros, ya que generan costos que solo aparecen después de que la aplicación ha estado en el campo por un tiempo. Intento evitar hacer jerarquías de clase ya que rara vez rinden frutos. Si puedo escribir algo en un período de tiempo razonable en lugar de adoptar un paquete que no encaja perfectamente, entonces lo hago. Mi código está bien estructurado para la depuración y está debidamente comentado, pero los paquetes de terceros rara vez lo están. Dicho esto, encuentro AF Networking demasiado útil para ignorarlo y bien estructurado, bien comentado y mantenido, ¡y lo uso mucho! RestKit cubre muchos casos comunes, pero siento que he estado en una pelea cuando lo uso,y la mayoría de las fuentes de datos que encuentro están llenas de peculiaridades y problemas que se manejan mejor con código personalizado. En mis últimas aplicaciones, solo uso los convertidores JSON integrados y escribo algunos métodos de utilidad.
Un patrón que siempre uso es quitar las llamadas de red del hilo principal. Las últimas 4-5 aplicaciones que he hecho configuran una tarea de temporizador de fondo usando dispatch_source_create que se activa cada cierto tiempo y realiza tareas de red según sea necesario. Debe realizar un trabajo de seguridad de subprocesos y asegurarse de que el código de modificación de la interfaz de usuario se envíe al subproceso principal. También ayuda a hacer su incorporación / inicialización de tal manera que el usuario no se sienta agobiado o retrasado. Hasta ahora esto ha estado funcionando bastante bien. Sugiero mirar estas cosas.
Finalmente, creo que a medida que trabajamos más y el sistema operativo evoluciona, tendemos a desarrollar mejores soluciones. Me ha llevado años superar mi creencia de que debo seguir patrones y diseños que otras personas afirman que son obligatorios. Si estoy trabajando en un contexto donde eso forma parte de la religión local, ejem, me refiero a las mejores prácticas de ingeniería del departamento, entonces sigo las costumbres de la carta, eso es por lo que me pagan. Pero rara vez encuentro que seguir diseños y patrones antiguos es la solución óptima. Siempre trato de ver la solución a través del prisma de las necesidades del negocio y construir la arquitectura para que coincida y mantener las cosas tan simples como pueden ser. Cuando siento que no hay suficiente allí, pero todo funciona correctamente, entonces estoy en el camino correcto.
Evito los singletons al diseñar mis aplicaciones. Son gente típica de muchas personas, pero creo que puedes encontrar soluciones más elegantes en otros lugares. Normalmente, lo que hago es construir mis entidades en CoreData y luego colocar mi código REST en una categoría NSManagedObject. Si, por ejemplo, quisiera crear y publicar un nuevo usuario, haría esto:
User* newUser = [User createInManagedObjectContext:managedObjectContext];
[newUser postOnSuccess:^(...) { ... } onFailure:^(...) { ... }];
Utilizo RESTKit para la asignación de objetos y lo inicializo en el inicio. Considero que enrutar todas sus llamadas a través de un singleton es una pérdida de tiempo y agrega un montón de repetitivo que no es necesario.
En NSManagedObject + Extensions.m:
+ (instancetype)createInContext:(NSManagedObjectContext*)context
{
NSAssert(context.persistentStoreCoordinator.managedObjectModel.entitiesByName[[self entityName]] != nil, @"Entity with name %@ not found in model. Is your class name the same as your entity name?", [self entityName]);
return [NSEntityDescription insertNewObjectForEntityForName:[self entityName] inManagedObjectContext:context];
}
En NSManagedObject + Networking.m:
- (void)getOnSuccess:(RESTSuccess)onSuccess onFailure:(RESTFailure)onFailure blockInput:(BOOL)blockInput
{
[[RKObjectManager sharedManager] getObject:self path:nil parameters:nil success:onSuccess failure:onFailure];
[self handleInputBlocking:blockInput];
}
¿Por qué agregar clases de ayuda adicionales cuando puede extender la funcionalidad de una clase base común a través de categorías?
Si está interesado en información más detallada sobre mi solución, hágamelo saber. Estoy feliz de compartir.
Pruebe github.com/kevin0571/STNetTaskQueue
Crear solicitudes de API en clases separadas.
STNetTaskQueue se ocupará de los subprocesos y delegado / devolución de llamada.
Extensible para diferentes protocolos.
Usamos algunos enfoques dependiendo de la situación. Para la mayoría de las cosas, AFNetworking es el enfoque más simple y robusto, ya que puede establecer encabezados, cargar datos de varias partes, usar GET, POST, PUT & DELETE y hay un montón de categorías adicionales para UIKit que le permiten, por ejemplo, configurar una imagen desde una url En una aplicación compleja con muchas llamadas, a veces abstraemos esto con un método de conveniencia propio que sería algo así como:
-(void)makeRequestToUrl:(NSURL *)url withParameters:(NSDictionary *)parameters success:(void (^)(id responseObject))success failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure;
Hay algunas situaciones en las que AFNetworking no es apropiado, sin embargo, como cuando está creando un marco de trabajo u otro componente de biblioteca, ya que AFNetworking ya puede estar en otra base de código. En esta situación, usaría un NSMutableURLRequest en línea si está realizando una sola llamada o abstraído en una clase de solicitud / respuesta.
Utilizo el enfoque que obtuve aquí: https://github.com/Constantine-Fry/Foursquare-API-v2 . He reescrito esa biblioteca en Swift y puedes ver el enfoque arquitectónico desde estas partes del código:
typealias OpertaionCallback = (success: Bool, result: AnyObject?) -> ()
class Foursquare{
var authorizationCallback: OperationCallback?
var operationQueue: NSOperationQueue
var callbackQueue: dispatch_queue_t?
init(){
operationQueue = NSOperationQueue()
operationQueue.maxConcurrentOperationCount = 7;
callbackQueue = dispatch_get_main_queue();
}
func checkIn(venueID: String, shout: String, callback: OperationCallback) -> NSOperation {
let parameters: Dictionary <String, String> = [
"venueId":venueID,
"shout":shout,
"broadcast":"public"]
return self.sendRequest("checkins/add", parameters: parameters, httpMethod: "POST", callback: callback)
}
func sendRequest(path: String, parameters: Dictionary <String, String>, httpMethod: String, callback:OperationCallback) -> NSOperation{
let url = self.constructURL(path, parameters: parameters)
var request = NSMutableURLRequest(URL: url)
request.HTTPMethod = httpMethod
let operation = Operation(request: request, callbackBlock: callback, callbackQueue: self.callbackQueue!)
self.operationQueue.addOperation(operation)
return operation
}
func constructURL(path: String, parameters: Dictionary <String, String>) -> NSURL {
var parametersString = kFSBaseURL+path
var firstItem = true
for key in parameters.keys {
let string = parameters[key]
let mark = (firstItem ? "?" : "&")
parametersString += "/(mark)/(key)=/(string)"
firstItem = false
}
return NSURL(string: parametersString.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding))
}
}
class Operation: NSOperation {
var callbackBlock: OpertaionCallback
var request: NSURLRequest
var callbackQueue: dispatch_queue_t
init(request: NSURLRequest, callbackBlock: OpertaionCallback, callbackQueue: dispatch_queue_t) {
self.request = request
self.callbackBlock = callbackBlock
self.callbackQueue = callbackQueue
}
override func main() {
var error: NSError?
var result: AnyObject?
var response: NSURLResponse?
var recievedData: NSData? = NSURLConnection.sendSynchronousRequest(self.request, returningResponse: &response, error: &error)
if self.cancelled {return}
if recievedData{
result = NSJSONSerialization.JSONObjectWithData(recievedData, options: nil, error: &error)
if result != nil {
if result!.isKindOfClass(NSClassFromString("NSError")){
error = result as? NSError
}
}
if self.cancelled {return}
dispatch_async(self.callbackQueue, {
if (error) {
self.callbackBlock(success: false, result: error!);
} else {
self.callbackBlock(success: true, result: result!);
}
})
}
override var concurrent:Bool {get {return true}}
}
Básicamente, hay una subclase de NSOperation que realiza la NSURLRequest, analiza la respuesta JSON y agrega el bloque de devolución de llamada con el resultado a la cola. La clase principal de la API construye NSURLRequest, inicializa esa subclase de NSOperation y la agrega a la cola.