Divide y vencerás. Aplicación de monolito modular en Objective-C y Swift





¡Hola, Habr! Mi nombre es Vasily Kozlov, soy un líder tecnológico de iOS en Delivery Club y encontré el proyecto en su forma monolítica. Confieso que participé en lo que este artículo está a punto de combatir, pero me arrepentí y transformé mi conciencia junto con el proyecto.



Quiero contarles cómo dividí un proyecto existente en Objective-C y Swift en módulos separados: marcos. Según Apple , un marco es un directorio de una estructura específica.



Inicialmente, establecimos un objetivo: aislar el código que implementa la función de chat para brindar soporte al usuario y reducir el tiempo de compilación. Esto llevó a consecuencias útiles que son difíciles de seguir sin hábito y que existen en el mundo monolítico de un proyecto.



De repente, los notorios principios SOLID comenzaron a tomar forma y, lo más importante, la propia formulación del problema nos obligó a organizar el código de acuerdo con ellos. Al mover una entidad a un módulo separado, automáticamente se encuentra con todas sus dependencias, que no deberían estar en este módulo y también duplicadas en el proyecto de la aplicación principal. Por tanto, la cuestión de organizar un módulo adicional con una funcionalidad común está madura. ¿No es este el principio de responsabilidad única, cuando una entidad debe tener un propósito?



La complejidad de dividir un proyecto con dos lenguajes y un gran legado en módulos puede asustar a primera vista, lo que me pasó a mí, pero prevaleció el interés por la nueva tarea.



En artículos encontrados anteriormente, los autores prometieronun futuro sin nubes con pasos simples y claros típicos de un nuevo proyecto. Pero cuando moví la primera clase base al módulo para código general, salieron a la luz tantas dependencias no obvias, tantas líneas de código estaban cubiertas en rojo en Xcode que no quería continuar.



El proyecto contenía una gran cantidad de código heredado, dependencias cruzadas de clases en Objective-C y Swift, diferentes objetivos en términos de desarrollo de iOS, una lista impresionante de CocoaPods. Cualquier paso lejos de este monolito llevó al hecho de que el proyecto dejó de construirse en Xcode, encontrando a veces errores en los lugares más inesperados.



Por lo tanto, decidí anotar la secuencia de acciones que tomé para facilitar la vida de los propietarios de dichos proyectos.



Los primeros pasos



Son obvios, se han escrito muchos artículos sobre ellos . Apple ha intentado que sean lo más fáciles de usar posible.



1. Cree el primer módulo: Archivo → Nuevo proyecto → Cocoa Touch Framework



2. Agregue el módulo al área de trabajo del proyecto











3. Cree la dependencia del proyecto principal en el módulo, especificando este último en la sección Embedded Binaries. Si hay varios destinos en el proyecto, el módulo deberá incluirse en la sección Binarios integrados de cada destino que dependa de él.



Solo agregaré un comentario mío: no se apresure.



¿Sabe qué se colocará en este módulo, sobre qué base se dividirán los módulos? En mi versión, debería haber sidoUIViewControllerpara charlar con mesa y celdas. Los cocoapods con un chat deben adjuntarse al módulo. Pero resultó un poco diferente. Tuve que posponer la implementación del chat, porque UIViewControllertanto su presentador como el celular estaban basados ​​en clases base y protocolos de los que el nuevo módulo no sabía nada.



¿Cómo resaltar un módulo? El enfoque más lógico - en "ficham» ( características ), es decir, para algunas tareas del usuario. Por ejemplo, chatee con soporte técnico, pantallas de registro / inicio de sesión, hoja inferior con la configuración de la pantalla principal. Además, lo más probable es que necesite algún tipo de funcionalidad básica, que no es una característica, sino solo un conjunto de elementos de la interfaz de usuario, clases base, etc. Esta funcionalidad debe trasladarse a un módulo común similar al famoso archivo Utils... No tenga miedo de dividir este módulo también. Cuanto más pequeños sean los cubos, más fácil será colocarlos en el edificio principal. Me parece que así es como se puede formular uno más de los principios SOLID .



Hay consejos listos para dividir en módulos que no usé, por lo que rompí tantas copias e incluso decidí hablar sobre el doloroso. Sin embargo, este enfoque, primero actuar, luego pensar, me abrió los ojos al horror del código dependiente en un proyecto monolítico. Cuando se encuentra al comienzo del viaje, le resulta difícil comprender la cantidad total de cambios que serán necesarios para eliminar las dependencias.



Así que simplemente mueva la clase de un módulo a otro, vea lo que se ruboriza en Xcode e intente descubrir las dependencias. Xcode 10 es complicado: cuando mueve enlaces a archivos de un módulo a otro, deja los archivos en el mismo lugar. Por lo tanto, el siguiente paso será así ...



4. Mueva archivos en el administrador de archivos, elimine los enlaces antiguos en Xcode y vuelva a agregar archivos al nuevo módulo. Hacer esta clase a la vez hará que sea más fácil no enredarse en dependencias.



Para que todas las entidades independientes estén disponibles desde fuera del módulo, debes tener en cuenta las peculiaridades de Swift y Objective-C.



5. En Swift, todas las clases, enumeraciones y protocolos deben estar marcados con un modificador de acceso.publicluego se puede acceder a ellos desde fuera del módulo. Si una clase base se mueve a un marco separado, debe marcarse con un modificador open; de lo contrario, no funcionará para crear una clase descendiente a partir de ella.



¡Debe recordar inmediatamente (o aprender por primera vez) qué niveles de acceso hay en Swift y obtener ganancias!







Al cambiar el nivel de acceso para la clase portada, Xcode requerirá que cambie el nivel de acceso para todos los métodos anulados al mismo.







Luego, debe agregar la importación del nuevo marco al archivo Swift, donde se usa la funcionalidad seleccionada, junto con algo de UIKit. Después de eso, debería haber menos errores en Xcode.



import UIKit
import FeatureOne
import FeatureTwo

class ViewController: UIViewController {
//..
}


Con Objective-C, la secuencia es un poco más complicada. Además, los marcos no admiten el uso de un encabezado de puente para importar clases Objective-C en Swift.







Por lo tanto, el campo Encabezado puente de Objective-C debe estar vacío en la configuración del marco.







Hay una manera de salir de esta situación y por qué esto es así es un tema para un estudio separado.



6. Cada marco tiene su propio archivo de encabezado general , a través del cual todas las interfaces públicas de Objective-C mirarán al mundo exterior.



Si especifica la importación de todos los demás archivos de encabezado en este encabezado general, estarán disponibles en Swift.







import UIKit
import FeatureOne
import FeatureTwo

class ViewController: UIViewController {    
    var vc: Obj2ViewController?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }


En Objective-C, para acceder a clases fuera de un módulo, debe jugar con su configuración: hacer públicos los archivos de encabezado.







7. Cuando todos los archivos se hayan transferido uno por uno a un módulo separado, no se olvide de Cocoapods. El Podfile debe reorganizarse si alguna funcionalidad termina en un marco separado. Así fue para mí: el pod con indicadores gráficos tenía que incorporarse al marco general, y el chat, el nuevo pod, se incluyó en su propio marco independiente.



Es necesario indicar explícitamente que el proyecto ahora no es solo un proyecto, sino un espacio de trabajo con subproyectos:



workspace 'myFrameworkTest'


Las dependencias comunes para los marcos se deben mover a variables separadas, por ejemplo, networkPodsy uiPods:



def networkPods
     pod 'Alamofire'
end



 def uiPods
     pod 'GoogleMaps'
 end


Luego, las dependencias del proyecto principal se describirán de la siguiente manera:



target 'myFrameworkTest' do
project 'myFrameworkTest'
    networkPods
    uiPods
    target 'myFrameworkTestTests' do
    end
end 


Las dependencias del marco con chat, de esta manera:



target 'FeatureOne' do
    project 'FeatureOne/FeatureOne'
    uiPods
    pod 'ChatThatMustNotBeNamed'
end


Rocas submarinas



Probablemente, esto podría estar terminado, pero luego descubrí varios problemas implícitos, que también quiero mencionar.



Todas las dependencias comunes se mueven a un marco separado, chat, a otro, el código se ha vuelto un poco más limpio, el proyecto está construido, pero falla cuando se inicia.



El primer problema estuvo en la implementación del chat. En la inmensidad de la red, el problema también ocurre en otros pods, solo google " Biblioteca no cargada: Razón: imagen no encontrada ". Fue con este mensaje que tuvo lugar la caída.



No pude encontrar una solución más elegante y me vi obligado a duplicar la conexión del pod con el chat en la aplicación principal:



target 'myFrameworkTest' do
    project 'myFrameworkTest'
    pod 'ChatThatMustNotBeNamed'
    networkPods
    uiPods
    target 'myFrameworkTestTests' do
    end
end


Por lo tanto, Cocoapods permite que la aplicación vea la biblioteca vinculada dinámicamente al inicio y cuando se compila el proyecto.



Otro problema eran los recursos, que había olvidado con seguridad y nunca había visto ninguna mención de este aspecto a tener en cuenta. La aplicación se bloqueó al intentar registrar el archivo xib de la celda: "No se pudo cargar NIB en el paquete" .



El constructor de init(nibName:bundle:)clases UINibpredeterminado busca un recurso en el módulo de la aplicación principal. Naturalmente, no sabes nada de esto cuando el desarrollo se lleva a cabo en un proyecto monolítico.



La solución es especificar el paquete en el que se define la clase de recurso, o dejar que el compilador lo haga él mismo usando el constructor de init(for:)clases.Bundle... Y, por supuesto, no olvide en el futuro que los recursos ahora pueden ser comunes a todos los módulos o específicos de un módulo.



Si el módulo usa xibs, entonces Xcode, como de costumbre, ofrecerá botones y UIImageViewseleccionará recursos gráficos de todo el proyecto, pero en el tiempo de ejecución no se cargarán todos los recursos ubicados en otros módulos. Cargué imágenes en código usando el constructor de la init(named:in:compatibleWith:)clase UIImage, donde el segundo parámetro Bundlees donde se encuentra el archivo de imagen.



Las celdas en UITableViewy UICollectionViewahora también deben registrarse de manera similar. Y debemos recordar que las clases Swift en la representación de cadenas también incluyen el nombre del módulo, y un método NSClassFromString()de Objective-C devuelvenil, por lo que recomiendo registrar celdas especificando no una cadena, sino una clase. Porque UITableViewpuede utilizar el siguiente método auxiliar:



@objc public extension UITableView {

    func registerClass(_ classType: AnyClass) {
        let bundle = Bundle(for: classType)
        let name = String(describing: classType)
        register(UINib(nibName: name, bundle: bundle), forCellReuseIdentifier: name)
    }
}


conclusiones



Ahora no tiene que preocuparse si una solicitud de extracción contiene cambios en la estructura del proyecto realizados en diferentes módulos, porque cada módulo tiene su propio archivo xcodeproj. Puede distribuir el trabajo para no tener que pasar varias horas armando el archivo del proyecto. Es útil tener una arquitectura modular en equipos grandes y distribuidos. Como consecuencia, la velocidad de desarrollo debería aumentar, pero lo contrario también es cierto. Pasé mucho más tiempo en mi primer módulo que si creara un chat dentro de un monolito.



De las ventajas obvias que Apple también señala, - la capacidad de reutilizar el código. Si la aplicación tiene diferentes objetivos (extensiones de aplicación), este es el enfoque más accesible. Quizás el chat no sea el mejor ejemplo. Debería haber comenzado por trazar la capa de red, pero seamos honestos con nosotros mismos, este es un camino muy largo y peligroso que es mejor dividir en pequeñas secciones. Y dado que en los últimos dos años esta fue la introducción de un segundo servicio para organizar el soporte técnico, quería implementarlo sin presentarlo. ¿Dónde están las garantías de que el tercero no aparecerá pronto?



Un efecto sutil al diseñar un módulo son interfaces más inteligentes y limpias. El desarrollador tiene que diseñar las clases para que ciertas propiedades y métodos sean accesibles desde el exterior. Inevitablemente, hay que pensar en qué ocultar y cómo hacer el módulo para que pueda volver a usarse fácilmente.



All Articles