Dividir para reinar. Aplicação de monólito modular em Objective-C e Swift





Olá, Habr! Meu nome é Vasily Kozlov, sou líder de tecnologia iOS no Delivery Club e encontrei o projeto em sua forma monolítica. Confesso que participei da luta contra a qual este artigo é dedicado, mas me arrependi e transformei minha consciência junto com o projeto.



Quero dizer como divido um projeto existente em Objective-C e Swift em módulos separados - frameworks. De acordo com a Apple , um framework é um diretório de uma estrutura específica.



Inicialmente, estabelecemos uma meta: isolar o código que implementa a função de chat para suporte ao usuário e reduzir o tempo de construção. Isso levou a consequências úteis que são difíceis de seguir sem o hábito e existentes no mundo monolítico de um projeto.



De repente, os notórios princípios SOLID começaram a tomar forma e, o mais importante, a própria formulação do problema nos obrigou a organizar o código de acordo com eles. Ao mover uma entidade para um módulo separado, você encontra automaticamente todas as suas dependências, que não deveriam estar neste módulo, e também duplicadas no projeto do aplicativo principal. Portanto, a questão de organizar um módulo adicional com funcionalidade comum está madura. Não é este o princípio da responsabilidade única, quando uma entidade deve ter um propósito?



A complexidade de dividir um projeto com duas linguagens e um grande legado em módulos pode assustar à primeira vista, o que aconteceu comigo, mas o interesse pela nova tarefa prevaleceu.



Em artigos encontrados anteriormente, os autores prometeramum futuro sem nuvens com etapas simples e claras típicas de um novo projeto. Mas quando mudei a primeira classe base para o módulo de código geral, tantas dependências não óbvias vieram à tona, tantas linhas de código foram cobertas em vermelho no Xcode que eu não queria continuar.



O projeto continha muito código legado, dependências cruzadas em classes em Objective-C e Swift, diferentes alvos em termos de desenvolvimento iOS, uma lista impressionante de CocoaPods. Qualquer passo para longe desse monólito levou ao fato de que o projeto parou de construir no Xcode, às vezes encontrando erros nos lugares mais inesperados.



Portanto, decidi escrever a sequência de ações que tomei para facilitar a vida dos donos desses projetos.



Os primeiros passos



Eles são óbvios, muitos artigos foram escritos sobre eles . A Apple tentou torná-los o mais amigáveis ​​possível.



1. Crie o primeiro módulo: Arquivo → Novo projeto → Cocoa Touch Framework



2. Adicione o módulo à área de trabalho do projeto











3. Crie a dependência do projeto principal no módulo, especificando o último na seção Binários incorporados. Se houver vários destinos no projeto, o módulo precisará ser incluído na seção Binários incorporados de cada destino que depende dele.



Acrescentarei apenas um comentário: não se apresse.



Você sabe o que será colocado neste módulo, em que base os módulos serão divididos? Na minha versão, deveria ter sidoUIViewControllerpara conversar com a mesa e células. Cocoapods com bate-papo devem ser anexados ao módulo. Mas acabou sendo um pouco diferente. Tive que adiar a implementação do chat, pois UIViewControllertanto o apresentador quanto o celular eram baseados em classes e protocolos básicos que o novo módulo desconhecia.



Como destacar um módulo? A abordagem mais lógica - em "ficham» ( recursos ), isto é, para algumas tarefas do usuário. Por exemplo, converse com o suporte técnico, telas de registro / login, folha de fundo com as configurações da tela principal. Além disso, muito provavelmente, você precisará de algum tipo de funcionalidade básica, que não é um recurso, mas apenas um conjunto de elementos da IU, classes básicas, etc. Esta funcionalidade deve ser movida para um módulo comum semelhante ao famoso arquivo Utils... Não tenha medo de dividir este módulo também. Quanto menores forem os cubos, mais fácil será encaixá-los no edifício principal. Parece-me que é assim que mais um dos princípios SOLID pode ser formulado .



Existem dicas prontas para dividir em módulos que não usei, por isso quebrei tantos exemplares, e até resolvi falar sobre o doloroso. No entanto, essa abordagem - primeiro agir, depois pensar - apenas abriu meus olhos para o horror do código dependente em um projeto monolítico. Quando você está no início da jornada, é difícil compreender a quantidade total de mudanças que serão necessárias para eliminar as dependências.



Portanto, basta mover a classe de um módulo para outro, ver o que está corado no Xcode e tentar descobrir as dependências. O Xcode 10 é complicado: quando você move links para arquivos de um módulo para outro, ele deixa os arquivos no mesmo lugar. Portanto, o próximo passo será assim ...



4. Mova os arquivos no gerenciador de arquivos, exclua os links antigos no Xcode e adicione os arquivos novamente ao novo módulo. Fazer essa aula por vez tornará mais fácil não ficar preso em dependências.



Para disponibilizar todas as entidades desanexadas de fora do módulo, você deve levar em consideração as peculiaridades do Swift e Objective-C.



5. Em Swift, todas as classes, enumerações e protocolos devem ser marcados com um modificador de acessopublicentão, eles podem ser acessados ​​de fora do módulo. Se uma classe base for movida para uma estrutura separada, ela deve ser marcada com um modificador open, caso contrário, não funcionará criar uma classe descendente a partir dela.



Você deve se lembrar imediatamente (ou aprender pela primeira vez) quais são os níveis de acesso do Swift e obter lucro!







Ao alterar o nível de acesso para a classe portada, o Xcode exigirá que você altere o nível de acesso de todos os métodos substituídos para o mesmo.







Em seguida, você precisa adicionar a importação da nova estrutura ao arquivo Swift, onde a funcionalidade selecionada é usada, junto com alguns UIKit. Depois disso, deve haver menos erros no Xcode.



import UIKit
import FeatureOne
import FeatureTwo

class ViewController: UIViewController {
//..
}


Com Objective-C, a sequência é um pouco mais complicada. Além disso, o uso de um cabeçalho de ponte para importar classes Objective-C para o Swift não é suportado em estruturas.







Portanto, o campo Objective-C Bridging Header deve estar vazio nas configurações da estrutura.







Há uma saída para essa situação e o motivo disso é um tópico para um estudo separado.



6. Cada estrutura tem seu próprio arquivo de cabeçalho guarda-chuva , por meio do qual todas as interfaces públicas do Objective-C olharão para o mundo externo.



Se você especificar a importação de todos os outros arquivos de cabeçalho neste cabeçalho abrangente, eles estarão disponíveis no 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.
    }


Em Objective-C, para acessar classes fora de um módulo, você tem que brincar com suas configurações: tornar os arquivos de cabeçalho públicos.







7. Quando todos os arquivos forem transferidos um a um para um módulo separado, não se esqueça dos Cocoapods. O Podfile precisa ser reorganizado se alguma funcionalidade acabar em uma estrutura separada. Foi assim para mim: o pod com indicadores gráficos teve que ser trazido para a estrutura geral, e o chat - o novo pod - foi incluído em sua própria estrutura separada.



É necessário indicar explicitamente que o projeto agora não é apenas um projeto, mas um espaço de trabalho com subprojetos:



workspace 'myFrameworkTest'


Dependências comuns para frameworks devem ser movidas para variáveis ​​separadas, por exemplo, networkPodse uiPods:



def networkPods
     pod 'Alamofire'
end



 def uiPods
     pod 'GoogleMaps'
 end


Em seguida, as dependências do projeto principal serão descritas da seguinte forma:



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


As dependências do framework com chat - desta forma:



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


Rochas subaquáticas



Provavelmente, isso poderia ser terminado, mas depois descobri vários problemas implícitos, que também quero mencionar.



Todas as dependências comuns são movidas para uma estrutura separada, chat - para outra, o código ficou um pouco mais limpo, o projeto foi construído, mas ele travou ao iniciar.



O primeiro problema foi na implementação do chat. Na vastidão da rede, o problema ocorre em outros pods, basta google " Biblioteca não carregada: Motivo: imagem não encontrada ". Foi com essa mensagem que ocorreu a queda.



Não consegui encontrar uma solução mais elegante e fui forçado a duplicar a conexão do pod com o chat no aplicativo principal:



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


Portanto, o Cocoapods permite que o aplicativo veja a biblioteca vinculada dinamicamente na inicialização e quando o projeto é compilado.



Outro problema eram os recursos, que eu tinha esquecido com segurança e nunca tinha visto qualquer menção a esse aspecto a ser lembrado. O aplicativo travou ao tentar registrar o arquivo xib da célula: "Não foi possível carregar o NIB no pacote" .



O construtor de init(nibName:bundle:)classe UINibpadrão procura um recurso no módulo de aplicativo principal. Naturalmente, você não sabe nada sobre isso quando o desenvolvimento é realizado em um projeto monolítico.



A solução é especificar o pacote no qual a classe de recurso é definida ou deixar que o próprio compilador faça isso usando o construtor de init(for:)classeBundle... E, é claro, não se esqueça de que, no futuro, os recursos agora podem ser comuns a todos os módulos ou específicos a um módulo.



Se o módulo usa xibs, o Xcode irá, como de costume, oferecer botões e UIImageViewselecionar recursos gráficos de todo o projeto, mas em tempo de execução todos os recursos localizados em outros módulos não serão carregados. Carreguei imagens em código usando o construtor da init(named:in:compatibleWith:)classe UIImage, onde o segundo parâmetro Bundleé onde o arquivo de imagem está localizado.



As células em UITableViewe UICollectionViewagora também devem se registrar de maneira semelhante. E devemos lembrar que as classes Swift na representação de string também incluem o nome do módulo, e um método NSClassFromString()de retorno Objective-Cnil, então eu recomendo registrar células especificando não uma string, mas uma classe. Pois UITableViewvocê pode usar o seguinte 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)
    }
}


conclusões



Agora você não precisa se preocupar se uma solicitação de pull contém alterações na estrutura do projeto feitas em módulos diferentes, porque cada módulo tem seu próprio arquivo xcodeproj. Você pode distribuir o trabalho de forma que não precise gastar várias horas montando o arquivo do projeto. É útil ter uma arquitetura modular em equipes grandes e distribuídas. Como consequência, a velocidade de desenvolvimento deve aumentar, mas o oposto também é verdadeiro. Passei muito mais tempo no meu primeiro módulo do que se fosse criar um chat dentro de um monólito.



Das vantagens óbvias que a Apple também aponta, - a capacidade de reutilizar o código. Se o aplicativo tiver destinos diferentes (extensões de aplicativo), esta é a abordagem mais acessível. Talvez o chat não seja o melhor exemplo. Eu deveria ter começado traçando a camada de rede, mas sejamos honestos conosco, esta é uma estrada muito longa e perigosa que é melhor dividida em pequenas seções. E como nos últimos dois anos esta foi a introdução de um segundo serviço para organizar o suporte técnico, eu queria implementá-lo sem apresentá-lo. Onde estão as garantias de que o terceiro não aparecerá em breve?



Um efeito sutil ao projetar um módulo são interfaces mais inteligentes e limpas. O desenvolvedor deve projetar as classes de forma que certas propriedades e métodos sejam acessíveis de fora. Inevitavelmente, você tem que pensar sobre o que esconder e como fazer o módulo para que ele possa ser facilmente usado novamente.



All Articles