Minha experiência de erros
Lista de erros
- Classe onipotente MCManager
- Inventando nossa navegação entre telas
- Nunca há muita herança
- Arquitetura de nossa própria produção ou continuar a criar bicicletas
- MVVM com alma MVP
- A segunda tentativa com navegação ou roteador e a curvatura da navegação
- Gerente persistente
Muitas pessoas, inclusive eu, escrevem como fazer a coisa certa em uma determinada situação, como escrever código corretamente, como aplicar soluções arquiteturais etc. Mas gostaria de compartilhar minha experiência de como isso foi feito incorretamente e as conclusões que feito com base em seus erros. Muito provavelmente, serão erros comuns de todos que seguem o caminho do desenvolvedor, ou talvez algo seja novo. Eu só quero compartilhar minha experiência e ler os comentários de outros caras.
Classe onipotente MCManager
Após o primeiro ano de trabalho em TI e, mais especificamente, o desenvolvimento do iOS, decidi que já era arquiteto o suficiente e pronto para criar. Mesmo assim, compreendi intuitivamente que era necessário separar a lógica de negócios da camada de apresentação. Mas a qualidade da minha ideia de como fazer isso estava longe da realidade.
Eu mudei para um novo local de trabalho, onde fui designado para desenvolver independentemente um novo recurso para um projeto existente. Era um análogo para gravar vídeos no Instagram, onde a gravação é realizada enquanto o usuário pressiona o botão no botão e, em seguida, vários fragmentos do vídeo são conectados juntos. Inicialmente, foi decidido fazer esse recurso como um projeto separado, ou melhor, na forma de uma amostra. Isso, pelo que entendi, começa a fonte dos meus problemas arquitetônicos, que duraram mais de um ano.
No futuro, esse exemplo se tornou um aplicativo completo para gravação e edição de vídeos. É engraçado que, inicialmente, a amostra tivesse um nome, cuja abreviação foi a qual o prefixo MC foi coletado. Embora o projeto tenha sido renomeado em breve, mas o prefixo, conforme exigido pela convenção de nomes no Objective-C, o MC permaneceu. Então, a poderosa classe MCManager nasceu.

Como era uma amostra e, a princípio, a funcionalidade era simples, decidi que uma classe de gerente seria suficiente. A funcionalidade, como mencionei anteriormente, incluía a gravação de um fragmento de vídeo com opções de início / parada e a combinação adicional desses fragmentos em um vídeo inteiro. E, nesse exato momento, posso nomear meu primeiro erro - o nome da classe MCManager. MCManager, Karl! O que um nome de classe deve dizer a outros desenvolvedores sobre seu objetivo, seus recursos e como usá-lo? Certo, absolutamente nada! E isso está no apêndice, cujo nome nem contém as letras M e, sua mãe, C. Embora esse não seja o meu principal erro, uma vez que a turma com o nome partidário fez tudo, tudo na palavra é absolutamente tudo, que foi o principal erro.
A gravação de vídeo é um serviço pequeno, o gerenciamento do armazenamento de arquivos de vídeo no sistema de arquivos é o segundo e adicional serviço para combinar vários vídeos em um. O trabalho desses três serviços independentes, foi decidido combinar em um gerente. A idéia era nobre, usando o padrão de fachada, para criar uma interface simples para a lógica de negócios e ocultar todos os detalhes desnecessários sobre a interação de vários componentes. Nos estágios iniciais, mesmo o nome de uma classe de fachada não levantou suspeitas, principalmente na amostra.
Mas o cliente gostou da demonstração e logo a amostra se transformou em um aplicativo completo. Você pode justificar que não havia tempo suficiente para refatorar, que o cliente não queria refazer o código de trabalho, mas, francamente, naquele momento, eu próprio pensei que havia estabelecido uma excelente arquitetura. Na verdade, a ideia de separar a lógica e a apresentação dos negócios foi um sucesso. A arquitetura era uma classe do MCManager único, que era a fachada de uma dúzia de serviços e outros gerentes. Sim, também era um singleton, disponível em todos os cantos do aplicativo.
Já se pode entender a escala de todo o desastre. Uma classe com vários milhares de linhas de código difíceis de ler e de manter. Já estou em silêncio sobre a possibilidade de destacar recursos individuais para transferi-los para outro aplicativo, o que é bastante comum no desenvolvimento móvel.
As conclusões que tirei depois de um tempo não são para criar classes universais com nomes obscuros. Percebi que a lógica precisa ser dividida em pedaços e não criar uma interface universal para tudo. De fato, este era um exemplo do que aconteceria se você não seguisse um dos princípios do SOLID, o Princípio de Segregação de Interface.
Inventando nossa navegação entre telas
A separação da lógica e da interface não é o único problema que me preocupou com o projeto acima. Não vou dizer que naquele momento eu ia separar o código da tela e o código de navegação, mas aconteceu que eu criei minha bicicleta para navegação.

A amostra possuía apenas três telas: um menu com uma tabela de vídeos gravados, uma tela de gravação e uma tela de pós-processamento. Para não me importar com o fato de a pilha de navegação conter ViewControllers duplicados, decidi não usar o UINavigationController. Eu adicionei o RootViewcontroller, leitores atentos já imaginaram que era o MCRootViewController, que foi definido como o principal nas configurações do projeto. Ao mesmo tempo, o controlador raiz não era uma das telas do aplicativo, apenas apresentava o UIViewController desejado. Como se isso não bastasse, o controlador raiz também foi um representante de todos os controladores representados. Como resultado, a cada momento, havia apenas dois vc na hierarquia e toda a navegação era implementada usando o padrão de delegação.
Como parecia: cada tela tinha seu próprio protocolo de delegação, onde os métodos de navegação foram indicados, e o controlador raiz implementou esses métodos e alterou as telas. O RootViewController dissmisil o controlador atual, criou um novo e o apresentou, enquanto era possível transferir informações de uma tela para outra. Felizmente, a lógica de negócios estava na classe singleton mais legal; portanto, nenhuma das telas armazenava nada e podia ser destruída sem dor. Mais uma vez, uma boa intenção foi realizada, embora a realização mancasse nas duas pernas e às vezes tropeçasse.
Como você pode imaginar, se você precisar ir da tela de gravação de vídeo para o menu principal, o método foi chamado:
- (void)cancel;
ou algo assim, e o controlador raiz já está fazendo todo o trabalho sujo.
Como resultado, o MCRootViewController tornou-se um análogo do MCManager, mas na navegação entre telas, como no crescimento do aplicativo e na adição de novas funcionalidades, novas telas foram adicionadas.
A fábrica de bicicletas funcionou inexoravelmente e eu continuei ignorando artigos sobre arquiteturas de aplicativos móveis. Mas nunca desisti da ideia de separar a navegação das telas.
A vantagem era que as telas eram independentes e poderiam ser reutilizadas, mas isso não é exato. Mas as desvantagens incluem a dificuldade em manter essas classes. O problema com a falta de uma pilha de telas quando você precisa retornar rolando pelas telas selecionadas anteriormente. A lógica complexa de transição entre telas, o controlador raiz afetou parte da lógica de negócios para exibir corretamente uma nova tela.
Em geral, você não deve implementar toda a navegação no aplicativo dessa maneira, pois meu MCRootViewController violou o princípio do princípio aberto-fechado. É quase impossível expandir, e todas as mudanças devem ser feitas constantemente na própria classe.
Comecei a ler mais sobre a navegação entre telas em um aplicativo móvel, familiarizei-me com abordagens como roteador e coordenador. Escreverei sobre o roteador um pouco mais tarde, pois há algo para compartilhar.
Nunca há muita herança
Também quero compartilhar não apenas minhas pérolas, mas também as abordagens e soluções engraçadas de outras pessoas com as quais tive que lidar.No mesmo local em que criei minhas obras-primas, eles me confiaram uma tarefa simples. A tarefa era adicionar uma tela de outro projeto ao meu projeto. Como determinamos com o PM, após uma análise superficial e um pouco de reflexão, isso deve levar duas ou três horas e não mais, porque o que há de errado nisso, você só precisa adicionar uma classe de tela pronta ao seu aplicativo. De fato, tudo já foi feito por nós, precisamos fazer ctrl + ce ctrl + v. Aqui estão apenas uma pequena nuance, o desenvolvedor que escreveu este aplicativo realmente adorou herança.

Encontrei rapidamente o ViewController de que precisava, tive sorte de não haver separação entre lógica e apresentação. Era uma boa abordagem antiga, quando o controlador continha todo o código necessário. Copiei para o meu projeto e comecei a descobrir como fazê-lo funcionar. E a primeira coisa que descobri é que o controlador de que preciso herda de outro controlador. Uma coisa comum, um evento bastante esperado. Como não tinha muito tempo, encontrei a classe de que precisava e a arrastei para o meu projeto. Bem, agora deveria funcionar, pensei, e nunca estive tão errado!
A classe de que eu precisava não só tinha muitas variáveis de classes personalizadas que também precisavam ser copiadas para o meu projeto, como cada uma delas herdou algo. Por sua vez, as classes base eram herdadas ou continham campos com tipos personalizados que, como muitos podem ter adivinhado, herdaram algo, e isso, infelizmente, não era NSObject, UIViewController ou UIView. Assim, um bom terço do projeto desnecessário migrou para mim no projeto.
Como não havia muito tempo para concluir esta tarefa, não havia outra maneira de simplesmente adicionar as classes necessárias que o xCode exigia simplesmente para iniciar meu projeto sem problemas. Como resultado, duas ou três horas se arrastaram um pouco, porque no final tive que mergulhar em toda a teia da hierarquia de herança, como um verdadeiro escultor, cortando o excesso.
Como resultado, cheguei à conclusão de que todas as coisas boas deveriam estar com moderação, mesmo uma coisa “maravilhosa” como herança. Então comecei a entender as desvantagens da herança. Concluí por mim mesmo, se eu quiser fazer módulos reutilizáveis, devo torná-los mais independentes.
Arquitetura de nossa própria produção ou continuar a criar bicicletas
Movendo-me para um novo local de trabalho e iniciando um novo projeto, levei em conta toda a experiência disponível no design da arquitetura e continuei a criar. Naturalmente, continuei ignorando as arquiteturas já inventadas, mas ao mesmo tempo segui persistentemente o princípio de “dividir e conquistar”.
Não demorou muito para o advento de Swift, então me aprofundou nas possibilidades do Objective-c. Eu decidi fazer a injeção de dependência usando os recursos de linguagem. Fui inspirado pela ferramenta de expansão da lib; nem me lembro do nome.
A linha inferior é: na classe base BaseViewController, adicionei um campo da classe BaseViewModel. Assim, para cada tela, criei meu próprio controlador, que herdou os básicos, e adicionei um protocolo para o controlador interagir com o viewModel. Então veio a magia. Redefini as propriedades do viewModel e adicionei suporte ao protocolo desejado. Por sua vez, criei uma nova classe ViewModel para uma tela específica que implementou esse protocolo. Como resultado, no BaseViewController no método viewDidLoad, verifiquei o tipo de protocolo do modelo, verifiquei a lista de todos os descendentes de BaseViewModel, localizei a classe necessária e criei o viewModel do tipo necessário.
Exemplo básico de ViewController #import <UIKit/UIKit.h> // MVC model #import "BaseMVCModel.h" @class BaseViewController; @protocol BaseViewControllerDelegate <NSObject> @required - (void)backFromNextViewController:(BaseViewController *)aNextViewController withOptions:(NSDictionary *)anOptionsDictionary; @end @interface BaseViewController : UIViewController <BaseViewControllerDelegate> @property (nonatomic, weak) BaseMVCModel *model; @property (nonatomic, assign) id<BaseViewControllerDelegate> prevViewController; - (void)backWithOptions:(NSDictionary *)anOptionsDictionary; + (void)setupUIStyle; @end import "BaseViewController.h" // Helpers #import "RuntimeHelper.h" @interface BaseViewController () @end @implementation BaseViewController + (void)setupUIStyle { } #pragma mark - #pragma mark Life cycle - (void)viewDidLoad { [super viewDidLoad]; self.model = [BaseMVCModel getModel:FindPropertyProtocol(@"model", [self class])]; } #pragma mark - #pragma mark Navigation - (void)backWithOptions:(NSDictionary *)anOptionsDictionary { if (self.prevViewController) { [self.prevViewController performSelector:@selector(backFromNextViewController:withOptions:) withObject:self withObject:anOptionsDictionary]; } } #pragma mark - #pragma mark Seque - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([segue.destinationViewController isKindOfClass:[BaseViewController class]] { ((BaseViewController *)segue.destinationViewController).prevViewController = self; } } #pragma mark - #pragma mark BaseViewControllerDelegate - (void)backFromNextViewController:(BaseViewController *)aNextViewController withOptions:(NSDictionary *)anOptionsDictionary { [self doesNotRecognizeSelector:_cmd]; } @end
Exemplo básico de ViewModel #import <Foundation/Foundation.h> @interface BaseMVCModel : NSObject @property (nonatomic, assign) id delegate; + (id)getModel:(NSString *)someProtocol; @end #import "BaseMVCModel.h" // IoC #import "IoCContainer.h" @implementation BaseMVCModel + (id)getModel:(NSString *)someProtocol { return [[IoCContainer sharedIoCContainer] getModel:NSProtocolFromString(someProtocol)]; } @end
Classes auxiliares #import <Foundation/Foundation.h> @interface IoCContainer : NSObject + (instancetype)sharedIoCContainer; - (id)getModel:(Protocol *)someProtocol; @end #import "IoCContainer.h" // Helpers #import "RuntimeHelper.h" // Models #import "BaseMVCModel.h" @interface IoCContainer () @property (nonatomic, strong) NSMutableSet *models; @end @implementation IoCContainer #pragma mark - #pragma mark Singleton + (instancetype)sharedIoCContainer { static IoCContainer *_sharedIoCContainer = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _sharedIoCContainer = [IoCContainer new]; }); return _sharedIoCContainer; } - (id)getModel:(Protocol *)someProtocol { if (!someProtocol) { return [BaseMVCModel new]; } NSArray *modelClasses = ClassGetSubclasses([BaseMVCModel class]); __block Class currentClass = NULL; [modelClasses enumerateObjectsUsingBlock:^(Class class, NSUInteger idx, BOOL *stop) { if ([class conformsToProtocol:someProtocol]) { currentClass = class; } }]; if (currentClass == nil) { return [BaseMVCModel new]; } __block BaseMVCModel *currentModel = nil; [self.models enumerateObjectsUsingBlock:^(id model, BOOL *stop) { if ([model isKindOfClass:currentClass]) { currentModel = model; } }]; if (!currentModel) { currentModel = [currentClass new]; [self.models addObject:currentModel]; } return currentModel; } - (NSMutableSet *)models { if (!_models) { _models = [NSMutableSet set]; } return _models; } @end #import <Foundation/Foundation.h> NSString * FindPropertyProtocol(NSString *propertyName, Class class); NSArray * ClassGetSubclasses(Class parentClass); #import "RuntimeHelper.h" #import <objc/runtime.h> #pragma mark - #pragma mark Functions NSString * FindPropertyProtocol(NSString *aPropertyName, Class class) { unsigned int propertyCount; objc_property_t *properties = class_copyPropertyList(class, &propertyCount); for (unsigned int i = 0; i < propertyCount; i++) { objc_property_t property = properties[i]; const char *propertyName = property_getName(property); if ([@(propertyName) isEqualToString:aPropertyName]) { const char *attrs = property_getAttributes(property); NSString* propertyAttributes = @(attrs); NSScanner *scanner = [NSScanner scannerWithString: propertyAttributes]; [scanner scanUpToString:@"<" intoString:NULL]; [scanner scanString:@"<" intoString:NULL]; NSString* protocolName = nil; [scanner scanUpToString:@">" intoString: &protocolName]; return protocolName; } } return nil; } NSArray * ClassGetSubclasses(Class parentClass) { int numClasses = objc_getClassList(NULL, 0); Class *classes = NULL; classes = (Class *)malloc(sizeof(Class) * numClasses); numClasses = objc_getClassList(classes, numClasses); NSMutableArray *result = [NSMutableArray array]; for (NSInteger i = 0; i < numClasses; i++) { Class superClass = classes[i]; do { superClass = class_getSuperclass(superClass); } while(superClass && superClass != parentClass); if (superClass == nil) { continue; } [result addObject:classes[i]]; } free(classes); return result; }
Exemplo da tela de login #import "BaseViewController.h" @protocol LoginProtocol <NSObject> @required - (void)login:(NSString *)aLoginString password:(NSString *)aPasswordString completionBlock:(DefaultCompletionBlock)aCompletionBlock; @end @interface LoginVC : BaseViewController @end #import "LoginVC.h" #import "UIViewController+Alert.h" #import "UIViewController+HUD.h" @interface LoginVC () @property id<LoginProtocol> model; @property (weak, nonatomic) IBOutlet UITextField *emailTF; @property (weak, nonatomic) IBOutlet UITextField *passTF; @end @implementation LoginVC @synthesize model = _model; #pragma mark - #pragma mark IBActions - (IBAction)loginAction:(id)sender { [self login]; } #pragma mark - #pragma mark UITextFieldDelegate - (BOOL)textFieldShouldReturn:(UITextField *)textField { if (textField == self.emailTF) { [self.passTF becomeFirstResponder]; } else { [self login]; } return YES; } #pragma mark - #pragma mark Login - (void)login { NSString *email = self.emailTF.text; NSString *pass = self.passTF.text; if (email.length == 0 || pass.length == 0) { [self showAlertOkWithMessage:@"Please, input info!"]; return; } __weak __typeof(self)weakSelf = self; [self showHUD]; [self.model login:self.emailTF.text password:self.passTF.text completionBlock:^(BOOL isDone, NSError *anError) { [weakSelf hideHUD]; if (isDone) { [weakSelf backWithOptions:nil]; } }]; } @end
De uma maneira tão direta, fiz uma inicialização lenta do viewModel e conectei mal a visualização aos modelos através de protocolos. Com tudo isso, naquele momento eu ainda não sabia nada sobre a arquitetura MVP, embora algo semelhante pairasse sobre mim.
A navegação entre as telas foi deixada ao critério de "viewModel", pois adicionei um link fraco ao controlador.
Lembrando essa implementação agora, não posso ter certeza de que tudo estava ruim. A idéia de separar as camadas foi um sucesso, o momento de criar e atribuir modelos ao controlador foi simplificado.
Mas, por mim mesmo, decidi aprender mais sobre abordagens e arquiteturas prontas, pois durante o desenvolvimento de um aplicativo com minha própria arquitetura, tive que lidar com muitas nuances. Por exemplo, reutilização de telas e modelos, herança, transições complexas entre telas. Naquele momento, pareceu-me que o viewModel fazia parte da lógica de negócios, embora agora eu entenda que ainda seja uma camada de apresentação. Eu tive uma ótima experiência durante esse experimento.
MVVM com alma MVP
Já tendo adquirido experiência, decidi escolher uma arquitetura específica para mim e segui-la, em vez de inventar bicicletas. Comecei a ler mais sobre arquiteturas, a estudar em detalhes populares na época e a instalar o MVVM. Francamente, não entendi imediatamente sua essência, mas a escolhi porque gostei do nome.
Não entendi imediatamente a essência da arquitetura e o relacionamento entre o ViewModel e o View (ViewController), mas comecei a fazer o que eu entendia. Os olhos estão com medo e as mãos estão furiosamente digitando o código.
Em minha defesa, acrescentarei que, naquela época, o tempo e o tempo para pensar e analisar a criação que eu criava eram muito restritos. Portanto, em vez de pastas, criei links diretos no ViewModel para a visualização correspondente. E já no ViewModel, fiz a configuração da apresentação.
Sobre o MVP, eu tinha a mesma idéia que sobre outras arquiteturas, por isso acreditava firmemente que era o MVVM, no qual o ViewModel acabou sendo os apresentadores mais reais.
Um exemplo da minha arquitetura “MVVM” e, sim, gostei da ideia com o RootViewController, responsável pelo mais alto nível de navegação no aplicativo. Sobre o roteador está escrito abaixo. import UIKit class RootViewController: UIViewController { var viewModel: RootViewModel? override func viewDidLoad() { super.viewDidLoad() let router = (UIApplication.shared.delegate as? AppDelegate)!.router viewModel = RootViewModel(with: self, router: router) viewModel?.setup() } } import UIKit protocol ViewModelProtocol: class { func setup() func backAction() } class RootViewModel: NSObject, ViewModelProtocol { unowned var router : RootRouter unowned var view: RootViewController init(with view: RootViewController, router: RootRouter) { self.view = view self.router = router }
Isso não afetou particularmente a qualidade do projeto, uma vez que a ordem e uma única abordagem foram respeitadas. Mas a experiência foi inestimável. Depois das bicicletas que criei, finalmente comecei a fazer de acordo com a arquitetura geralmente aceita. A menos que os apresentadores sejam chamados de apresentadores, o que pode confundir um desenvolvedor de terceiros.
Decidi que, no futuro, vale a pena fazer pequenos projetos de teste, para aprofundar a essência de uma abordagem específica no design com mais detalhes. Por assim dizer, primeiro sinta-se na prática e depois entre na batalha. Essa é a conclusão que tirei para mim.
A segunda tentativa com navegação ou roteador e a curvatura da navegação
No mesmo projeto, onde implementei o MVVM de forma valiosa e ingênua, decidi tentar uma nova abordagem na navegação entre telas. Como mencionei anteriormente, ainda aderi à idéia de separar telas e à lógica de transição entre elas.
Lendo sobre o MVVM, eu estava interessado em um padrão como o Roteador. Depois de revisar a descrição novamente, comecei a implementar a solução no meu projeto.
Exemplo de roteador import UIKit protocol Router: class { func route(to routeID: String, from view: UIViewController, parameters: Any?) func back(from view: UIViewController, parameters: Any?) } extension Router { func back(from view: UIViewController, parameters: Any?) { let navigationController: UINavigationController = checkNavigationController(for: view) navigationController.popViewController(animated: false) } } enum RootRoutes: String { case launch = "Launch" case loginregistartion = "LoginRegistartionRout" case mainmenu = "MainMenu" } class RootRouter: Router { var loginRegistartionRouter: LoginRegistartionRouter? var mainMenuRouter: MainMenuRouter?
A falta de experiência na implementação de tal padrão se fez sentir. Tudo parecia arrumado e claro, o roteador criou uma nova classe UIViewController, criou um ViewModel para ele e executou a lógica para mudar para essa tela. Mas, ainda assim, muitas deficiências se fizeram sentir.
Dificuldades começaram a surgir quando foi necessário abrir um aplicativo com uma certa tela após a notificação por push. Como resultado, em alguns lugares, obtivemos uma lógica confusa para escolher a tela certa e mais dificuldades em apoiar essa abordagem.
Não abandonei a idéia de implementar o Roteador, mas continuei nessa direção, ganhando cada vez mais experiência. Não desista de algo após a primeira tentativa falhada.
Gerente persistente
Outra classe de gerente interessante na minha prática. Mas este é relativamente jovem. Mesmo assim, o processo de desenvolvimento consiste em tentativa e erro e, como todos nós, bem ou a maioria de nós, estamos constantemente no processo de desenvolvimento, os erros sempre aparecem.
A essência do problema é esta: o aplicativo possui serviços que devem travar constantemente e, ao mesmo tempo, devem estar disponíveis em muitos lugares.
Exemplo: determinando o status do Bluetooth. No meu aplicativo, em vários serviços, preciso entender se o bluetooth está ativado ou desativado e assinar atualizações de status. Como existem vários locais: algumas telas, vários gerenciadores de lógica de negócios adicionais etc., torna-se necessário que cada um deles assine o delegado CBPeripheralManager (ou CBCentralManager).
A solução parece óbvia: criamos uma classe separada que monitora o status do bluetooth e notifica todos que precisam dele através do padrão Observer. Mas então surge a pergunta: quem armazenará permanentemente esse serviço? A primeira coisa que vem à mente neste momento é torná-lo um singleton! Tudo parece estar bem!
Mas aqui chega o momento em que mais de um serviço foi acumulado no meu aplicativo. Também não quero criar 100500 singletones no projeto.
E então outra luz se acendeu acima da minha cabecinha já brilhante. Faça um singleton que armazene todos esses serviços e forneça acesso a eles em todo o aplicativo. E assim nasceu o "gerente permanente". Com o nome, não pensei por muito tempo e o chamei, como todos já podiam adivinhar, PersistentManager.
Como você pode ver, também tenho uma abordagem muito original para nomear classes. Eu acho que preciso adicionar um modismo sobre o nome das classes no meu plano de desenvolvimento.
O principal problema nesta implementação é o singleton, que está disponível em qualquer lugar do projeto. E isso leva ao fato de os gerentes que usam um dos serviços permanentes acessarem seus métodos, o que não é óbvio. Pela primeira vez, me deparei com isso quando estava criando um grande recurso complexo em um projeto de demonstração separado e transferindo parte da lógica de negócios do projeto principal. Então comecei a receber mensagens com erros sobre serviços ausentes.
A conclusão que tirei depois disso é que você precisa criar suas classes de forma que não haja dependências ocultas. Os serviços necessários devem ser passados como parâmetros ao inicializar a classe, mas não usando um singleton, que pode ser acessado de qualquer lugar. E ainda mais lindamente, vale a pena usar protocolos.
Isso acabou por ser outra confirmação da falta de um padrão singleton.
Sumário
E isso, eu não fico parado, mas avanço, dominando novas abordagens na programação. O principal é mover, buscar e experimentar. Erros sempre serão, não há como escapar disso. Mas apenas por causa do reconhecimento dos próprios erros, é possível desenvolver qualitativamente.

Na maioria dos casos, os problemas são super classes que fazem muito ou dependências incorretas entre as classes. O que sugere que é necessário decompor a lógica com mais competência.