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 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 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 Data
para esto. Pero no debe olvidar que Core Data
no 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 Data
puede 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 controller
cosas. Pero sí creo en el skinny everything
enfoque, 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 Layer
es 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 Store
es en realidad nuestra Service
capa. Store
vende 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 APIClient
clase 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 NSError
objetos directos con razones detalladas de fallas y descripciones de todos API
y 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
,FriendsServices
y 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 RACSignal
con el modelo de respuesta analizado o NSError
al 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
, NSPredicate
y así sucesivamente con los métodos de civil 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 complejas asincrónicas con la respuesta: manipulaciones de señal, encadenamiento, mapeo, etc. con la ayuda de ReactiveCocoa
primitivas, 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 APIClient
se pasa implícitamente a todos los controladores, puede hacer esto explícito con una parametrizada sobre APIClient
clases de servicio. Esto puede tener sentido si desea utilizar diferentes personalizaciones deAPIClient
para 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, APIManagerWhatever
clase 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, UserProfile
necesidades de controladores, UserServices
etc.) .
Siempre debemos respetar los S
principios 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 FriendsServices
clase llamado:
- (RACSignal *)removeFriend:(Friend * const)friend
donde Friend
es un objeto modelo / dominio (o puede ser solo un User
objeto si tienen atributos similares). Bajo el cofre este método análisis sintácticos Friend
a NSDictionary
los parámetros JSON friend_id
, name
, surname
, friend_request_id
y 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
DELETE
método para hacer una solicitud de un descanso efectivo y regresa Response
en RACSignal
que la persona que llama ( FriendsViewController
en 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 Repository
o modelar la lógica con Service
uno. Cuando describí mi enfoque, dije que el removeFriend
método debería estar en la Service
capa, 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 Repository
es 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
, removeFriend
se 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 CRUD
operaciones básicas y conecta dos objetos de dominio ( Friend
y Request
), es por eso que debe colocarse en la Service
capa. 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 APIClient
capa más elegante (nuestro caballo de batalla como recordará). Ahora nuestro APIClient
proveedor 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 APIClient
proveedor mejorado como en el enfoque habitual de Objective-C. Para la capa de modelo en lugar de Mantle
puede 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 Swift
lenguaje 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 profunctors
y ese tipo de cosas en tu modelo inmutable. Lo mismo con el ReactiveCocoa
: no RACify
codifique 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.