I want to understand basic, abstract and correct architectural approach for networking applications in iOS: no existe un enfoque "el mejor" o "el más correcto" para crear una arquitectura de aplicación. Es un muy trabajo 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 un "bien" y un "mal" "arquitectura.
Usted dijo: collect the most interesting approaches from experienced iOS developersno 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 ha mencionado 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 y modismos conocidos. Creo que muchos de los patrones empresariales de Fowler se pueden aplicar con éxito a las aplicaciones móviles. Aquí hay una lista de los 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 ,Puerta de enlace , 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 Datapara esto. Pero no debe olvidar que Core Datano es 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 Core Datapuede ser demasiado pesado para sus necesidades y puede buscar nuevas soluciones como Realm y Couchbase Lite , o crear su propia capa ligera de mapeo / persistencia de objetos, basada en SQLite sin procesar o LevelDB. También le aconsejo que se familiarice con el diseño impulsado por dominio y CQRS .
Al principio, creo, deberíamos crear otra capa para redes, porque no queremos controladores gordos o modelos pesados y abrumados. No creo en esas fat model, skinny controllercosas. Pero sí creo en el skinny everythingenfoque, porque ninguna clase debería ser gorda, nunca. En general, todas las redes pueden abstraerse como lógica de negocios, por lo tanto, deberíamos tener otra capa, donde podamos ponerla. La capa de servicio es lo que necesitamos:
It encapsulates the application's business logic, controlling transactions
and coordinating responses in the implementation of its operations.
En nuestro MVCámbito Service Layeres algo así como un mediador entre el modelo de dominio y los controladores. Hay una variación bastante similar de este enfoque llamada MVCS donde a Storees en realidad nuestra Servicecapa. Storevende instancias de modelos y maneja las redes, el almacenamiento en caché, etc. Quiero mencionar que no debe escribir toda su lógica de negocios y redes en su capa de servicio. Esto también puede considerarse como un mal diseño. Para obtener más información, consulte los modelos de dominio Anemic y Rich . Algunos métodos de servicio y lógica de negocios pueden manejarse en el modelo, por lo que será un modelo "rico" (con comportamiento).
Siempre uso ampliamente dos bibliotecas: AFNetworking 2.0 y ReactiveCocoa . Creo que es imprescindible para cualquier aplicación moderna que interactúe con la red y los servicios web o que contenga una lógica de interfaz de usuario compleja.
ARQUITECTURA
Al principio creo una APIClientclase general , 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 le delegan solicitudes REST reales. Contiene todas las personalizaciones del cliente HTTP, que necesito en la aplicación en particular: fijación SSL, procesamiento de errores y creación de NSErrorobjetos directos con razones detalladas de fallas y descripciones de todos APIy errores de conexión (en tal caso, el controlador podrá mostrar mensajes correctos para el usuario), configurando serializadores de solicitud y respuesta, encabezados http y otras cosas relacionadas con la red. Entonces lógicamente divido todas las solicitudes de la API en subservicios o, más correctamente, microservicios : UserSerivces, CommonServices, SecurityServices,FriendsServicesy así sucesivamente, de acuerdo con la lógica empresarial que implementan. Cada uno de estos microservicios es una clase separada. Ellos, juntos, forman a Service Layer. Estas clases contienen métodos para cada solicitud de API, procesan modelos de dominio y siempre devuelven un RACSignalcon el modelo de respuesta analizado o NSErroral llamante.
Quiero mencionar que si tiene una lógica de serialización de modelo compleja, cree otra capa para ella: algo como Data Mapper pero más general, por ejemplo, JSON / XML -> Model mapper. 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 almacenamiento en caché correcta puede ser bastante compleja con sus propios problemas. Las personas implementan una lógica compleja para obtener un almacenamiento en caché válido y predecible como, por ejemplo, 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 el repositorioNSManagedObjectContext modelos de solicitud del servidor, puede usarPatrón de , 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 recomiendo usar el patrón de repositorio incluso cuando tenga una arquitectura basada en Core Data. Repositorio puede cosas abstractas, como NSFetchRequest, NSEntityDescription, NSPredicatey así sucesivamente con los métodos de civil como geto put.
Después de todas estas acciones en la capa de Servicio, la persona que llama (controlador de vista) puede hacer algunas cosas complejas asincrónicas con la respuesta: manipulaciones de señal, encadenamiento, mapeo, etc. con la ayuda de ReactiveCocoaprimitivas, o simplemente suscribirse y mostrar resultados en la vista . Me inyecto con la inyección de dependencia en todas estas clases de servicios mis APIClient, lo que se traducirá una llamada de servicio particular, en los correspondientes GET, POST, PUT, DELETE, etc. solicitud al extremo REST. En este caso APIClientse pasa implícitamente a todos los controladores, puede hacer esto explícito con una parametrizada sobre APIClientclases de servicio. Esto puede tener sentido si desea utilizar diferentes personalizaciones deAPIClientpara clases de servicio particulares, pero si, por alguna razón, no desea copias adicionales o está seguro de que siempre usará una instancia en particular (sin personalizaciones) de APIClient- conviértalo en un singleton, pero NO, por favor NO HAGA 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 la interfaz de usuario. Para la inyección de dependencia, me gusta usar BloodMagic o un framework más potente Typhoon . Nunca uso singletons, APIManagerWhateverclase de Dios u otras cosas incorrectas. Porque si llamas a tu clase WhateverManager, esto indica que no conoces su propósito y es una mala elección de diseño . Singletons también es un antipatró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;
- La inicialización perezosa es deseable;
- El acceso global no está previsto de otra manera.
En nuestro caso, la propiedad de la instancia única no es un problema y tampoco necesitamos acceso global después de dividir a nuestro God Manager en servicios, porque ahora solo uno o varios controladores dedicados necesitan un servicio en particular (por ejemplo, UserProfilenecesidades de controladores, UserServicesetc.) .
Siempre debemos respetar los Sprincipios en SOLID y usar la separación de preocupaciones , así que no coloque todos sus métodos de servicio y llamadas de red en una clase, porque es una locura, especialmente si desarrolla una aplicación de gran empresa. 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 pasamos alrededor de 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 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 amigos. Creo un método en mi FriendsServicesclase llamado:
- (RACSignal *)removeFriend:(Friend * const)friend
donde Friendes un objeto modelo / dominio (o puede ser solo un Userobjeto si tienen atributos similares). Bajo el cofre este método análisis sintácticos Frienda NSDictionarylos parámetros JSON friend_id, name, surname, friend_request_idy así sucesivamente. Siempre uso la biblioteca Mantle para este tipo de repetitivo 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 que llama APIClient DELETEmétodo para hacer una solicitud de un descanso efectivo y regresa Responseen RACSignalque la persona que llama ( FriendsViewControlleren nuestro caso) para mostrar el mensaje adecuado 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 Repositoryo modelar la lógica con Serviceuno. Cuando describí mi enfoque, dije que el removeFriendmétodo debería estar en la Servicecapa, pero si vamos a ser más pedantes, podemos notar que pertenece mejor 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 cierto tipo como un conjunto conceptual. Actúa como una colección, excepto con una capacidad de consulta más elaborada.
Entonces, a Repositoryes esencialmente una fachada que usa semántica de estilo Colección (Agregar, Actualizar, Eliminar) para proporcionar acceso a datos / objetos. Es por eso que cuando se tiene algo como: getFriendsList, getUserGroups, removeFriendse puede colocar en el Repository, porque la recolección como la semántica está bastante claro aquí. Y código como:
- (RACSignal *)approveFriendRequest:(FriendRequest * const)request;
definitivamente es una lógica de negocios, porque está más allá de las CRUDoperaciones básicas y conecta dos objetos de dominio ( Friendy Request), es por eso que debe colocarse en la Servicecapa. También quiero notar: no cree abstracciones innecesarias . Use todos estos enfoques sabiamente. Porque si abruma 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 "viejo" ejemplo de Objective-C, pero este enfoque puede adaptarse muy fácilmente para el lenguaje Swift con muchas más mejoras, ya que tiene características más útiles y azúcar funcional. Recomiendo utilizar esta biblioteca: Moya . Le permite crear una APIClientcapa más elegante (nuestro caballo de batalla como recordará). Ahora nuestro APIClientproveedor será un tipo de valor (enumeración) con extensiones que se ajustan a los protocolos y que aprovechan la coincidencia de patrones de desestructuración. Las combinaciones rápidas de enumeraciones + patrones nos permiten crear tipos de datos algebraicos como en la programación funcional clásica. Nuestros microservicios utilizarán este APIClientproveedor mejorado como en el enfoque habitual de Objective-C. Para la capa de modelo en lugar de Mantlepuede usar la biblioteca ObjectMappero me gusta usar una biblioteca Argo más elegante y funcional .
Entonces, describí mi enfoque arquitectónico general, que creo que se puede adaptar a cualquier aplicación. 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 también. Eliminar, en general, un estado mutable global excesivo, compartido, crear un modelo de dominio inmutable o crear funciones puras sin efectos secundarios externos es, en general, una buena práctica, y un nuevo Swiftlenguaje fomenta esto. Pero recuerde siempre que sobrecargar su código con patrones funcionales puros y pesados, los enfoques teóricos de categoría es una mala idea, porque hay otras desarrolladores leerán y apoyarán su código, y pueden sentirse frustrados o asustados por elprismatic profunctorsy ese tipo de cosas en tu modelo inmutable. Lo mismo con el ReactiveCocoa: no RACifycodifique demasiado , porque puede volverse ilegible muy rápido, especialmente para los novatos. Úselo cuando realmente puede simplificar sus objetivos y lógica.
Por lo tanto, read a lot, mix, experiment, and try to pick up the best from different architectural approaches. Es el mejor consejo que puedo darte.