Development of mobile applications

Мой опыт ошибок

Мой опыт ошибок


Список ошибок


  1. Всемогущий класс MCManager
  2. Изобретаем свою навигацию между экранами
  3. Наследования много не бывает
  4. Архитектура собственного производства или продолжаем творить велосипеды
  5. MVVM с душой MVP
  6. Вторая попытка с навигацией или Router и кривизна навигации
  7. Персистент манагер

Многие, и я, в том числе, пишут как правильно поступать в той или иной ситуации, как правильно писать код, как правильно применять архитектурные решения и т. д. Но я хотел бы поделиться своим опытом как было сделано неправильно и выводами, которые я сделал на основе своих ошибок. Скорей всего это будут распространенные ошибки всех, кто идет по пути разработчика, а может что-то будет и вновинку. Я хочу просто поделиться своим опытом и почитать комментарии других ребят.

Всемогущий класс MCManager


После первого года работы в IT, а конкретнее iOS разработки, я решил, что я уже достаточно архитектор и вполне готов творить. Уже тогда я интуитивно понимал что нужно разделять бизнес логику от слоя представления. Но качество моего представления о том, как это нужно делать, было далеко от действительности.

Я перешёл на новое место работы, где мне поручили самостоятельно разрабатывать новую фичу для существующего проекта. Это был аналог записи видео в Инстаграмм, где запись ведётся пока пользователь удерживает палец на кнопке, а затем несколько фрагментов видео соединялись воедино. Изначально было принято решение делать эту фичу как отдельный проект, а точнее в виде сэмпла. В этом, как я понимаю, и начинается исток моих архитектурных проблем, которые растянулись более чем на год.

В дальнейшем этот семпл перерос в полноценное приложение по записи и редактировании видео. Забавно, что изначально у сэмпла было название, с аббревиатуры которого и был собран префикс MC. Хотя проект вскоре переименовали, но префикс как того требовал name convention в Objective-C, остался MC. Так и родился всемогущий класс MCManager.



Так как это был сэмпл и поначалу функционал был несложным, то я решил, что одного класса-менеджера будет достаточно. Функционал, как я уже упомянул ранее, включал в себя запись видеофрагмента с опциями старт/стоп и дальнейшее объединение этих фрагментов в целое видео. И в этот же самый момент я могу назвать свою первую ошибку — название класса MCManager. MCManager, Карл! Что такое название класса должно говорить другим разработчикам о его назначении, о его возможностях, о том, как его использовать? Правильно, абсолютно ничего! И это в приложении, название которого даже не содержит букв M и, мать его, C. Хотя, это не главная моя ошибка, так как класс с партизанским названием, делал все, все от слова совсем все, что и являлось ключевой ошибкой.

Запись видео — это один небольшой сервис, управление хранением видеофайлов в файловой системе — второй и дополнительно сервис по объединению нескольких видео в одно. Работу этих трех независимых сервисов было решено объединить в одном менеджере. Задумка была благородной, используя паттерн фасад, создать простой интерфейс для бизнес-логики и скрыть все ненужные подробности по взаимодействию различных компонентов. На начальных этапах даже название такого класса-фасада не вызывало никаких подозрений, тем более в сэмпле.
Но демка понравилась заказчику и вскоре сэмпл превратился в полноценное приложение. Можно оправдываться, что не хватало времени на рефакторинг, что заказчик не хотел переделывать работающий код, но откровенно говоря, я и сам в тот момент считал, что заложил отличную архитектуру. По правде говоря, задумка по разделению бизнес-логики и представления, удалась. Архитектура представляла собой один класс синглтон MCManager, который был фасадом уже для пары десятков сервисов и других менеджеров. Да, это был ещё и синглтон, который был доступен со всех уголков приложения.

Уже можно понять масштабы всего бедствия. Класс, с нескольких тысяч строк кода, который трудно читать и трудно поддерживать. Я уже молчу про возможности выделения отдельных фич с целью переноса их в другое приложение, что довольно распространено в мобильной разработке.
Выводы, которые я сделал для себя уже спустя время — не создавать универсальные классы с непонятными названиями. Я понял, что логику нужно разбивать на части и не создавать универсальный интерфейс для всего. По сути это был пример того, что будет если не придерживаться одного из принципов SOLID, Interface Segregation Principle (Принцип разделения интерфейса).

Изобретаем свою навигацию между экранами


Разделение логики и интерфейса, это не единственный вопрос, который меня волновал на вышеупомянутом проекте. Не скажу, что в тот момент я был намерен разделить код экранов и код навигации, но так вышло что для навигации я придумал свой велосипед.



Сэмпл имел всего три экрана: меню с таблицей записанных видео, экран записи и экран постобработки. Что бы не заботится о том что навигейшен стэк содержит в себе повторяющихся ViewController, я решил не использовать UINavigationController. Я добавил RootViewcontroller, внимательные читатели уже догадались, что это был MCRootViewController, который в настройках проекта устанавливался как основной. При этом рут контроллер не был одним из экранов приложения, он просто презентил нужный UIViewController. Как будто этого было мало, так рутовый контроллер был ещё и делегатом всех, представленных контроллеров. В итоге в каждый момент времени в иерархии было всего два vc, а вся навигация реализовывалась посредством паттерна делегат.

Как это выглядело: каждый экран имел свой протокол делегата, где обозначались методы навигации, а рутовый контроллер реализовывал эти методы и менял экраны. RootViewController диссмисил текущий контроллер, создавал новый и презентил его, при этом была возможность передачи информации с одного экрана на другой. Благо бизнес логика была в крутейшим классе синглтоне, поэтому никакой из экранов ничего не хранил и мог быть безболезненно уничтожен. Опять-таки благое намерение было реализовано, хотя и реализация прихрамывала на обе ноги, а иногда и спотыкалась.

Как можно догадаться, если нужно перейти с экрана записи видео обратно в главное меню, то вызывался метод:

- (void)cancel;

или что-то наподобие этого, а рутовый контроллер уже делает всю грязную работу.
В конечном итоге MCRootViewController, стал аналогом MCManager, но в навигации между экранами, так как с ростом приложения и добавлением нового функционала, добавлялись новые экраны.

Завод по производству велосипедов работал неумолимо, а я продолжал игнорировать статьи об архитектурах мобильных приложений. Но от идеи об отделении навигации от экранов я так и не отказался.

Достоинством было то, что экраны были независимые и их можно было переиспользовать, но это не точно. А вот к недостаткам можно отнести сложность в поддержании таких классов. Проблема с отсутствием стека экранов, когда нужно вернуться, пролистав ранее выбранные экраны. Сложная логика перехода между экранами, рутовый контроллер затрагивал часть бизнес-логики чтобы правильно отобразить новый экран.

В целом, не стоит всю навигацию в приложении реализовывать таким образом, так как мой MCRootViewController нарушал принцип Open-Closed Principle (Принцип открытости-закрытости). Его практически невозможно расширять, а все изменения нужно постоянно вносить в сам класс.
Я стал больше читать о навигации между экранами в мобильном приложении, познакомился с такими подходами как Router и Coordinator. Про Router напишу чуть позже, благо есть чем поделиться.

Наследования много не бывает


Также хочется поделиться не только своими перлами, но и чужими забавными подходами и решениями, с которыми мне приходилось сталкиваться.Там же, где я творил свои шедевры, мне поручили простое задание. Задание это заключалось в том, что нужно в мой проект добавить экран с другого проекта. Как определили мы с ПМом, после неглубокого анализа и недолгих размышлений — это должно было занять два-три часа и не более, ведь что здесь такого, нужно готовый класс экрана просто добавить в свое приложение. И правда, уже все сделано за нас, нужно сделать ctrl+c и ctrl+v. Вот только маленький нюанс, разработчик, который писал это приложение, уж очень любил наследование.



Я быстренько нашел нужный мне ViewController, мне повезло, что не было разделения логики и представления. Это был старый добрый подход, когда контроллер содержал в себе весь необходимый код. Я его скопировал в свой проект и начал разбираться, как его заставить работать. И первое, что я обнаружил, так это то что нужный мне контроллер наследует другой контроллер. Обычное дело, вполне себе ожидаемое событие. Так как времени у меня было не много, то я просто нашёл нужный мне класс и перетащил к себе в проект. Ну теперь должно заработать, подумал я и я никогда ещё так не ошибался!

Мало того, что нужный мне класс имел множество переменных кастомных классов, которые тоже нужно было скопировать к себе в проект, так каждый из них что-то да й наследовал. В свою очередь базовые классы тоже либо наследовались, либо содержали поля с кастомными типами, которые, как многие уже могли догадаться, что-то наследовали и это, к моему несчастью, не были NSObject, UIViewController или UIView. Таким образом ко мне в проект перекочевала добрая треть ненужного мне проекта.

Так как времени, ожидаемого на выполнение этой задачи было немного, я не видел другого выхода как просто добавлять необходимые классы, которые xCode требовал просто для безболезненного запуска своего проекта. В итоге, два-три часа немного затянулись, так как в конечном итоге мне пришлось вникать во всю эту паутину иерархии наследовании, как истинному скульптору, отсекая лишнее.

В итоге я пришёл к выводу, что всего хорошего должно быть в меру, даже такой «замечательной» штуки как наследование. Тогда же я начал понимать недостатки наследования. Я сделал для себя вывод, если я хочу делать переиспользуемые модули, то должен делать их более независимыми.

Архитектура собственного производства или продолжаем творить велосипеды


Перейдя на новое место работы и начав новый проект, я учел весь имеющийся опыт в проектировании архитектуры и продолжил творить. Естественное, я продолжал игнорировать уже изобретенные архитектуры, но при этом я настойчиво придерживался принципа «разделяй и властвуй».

Это было незадолго до появления Swift, так что я углубился в возможности Objective-c. Я решил сделать Dependency injection, используя особенности языка. Вдохновила меня на это либка по расширению пропертей, уже даже не вспомню ее названия.

Суть следующая: в базовом классе BaseViewController я добавил поле класса BaseViewModel. Соответственно, для каждого экрана создавал свой контроллер, который наследовал базовые, и добавлял протокол для взаимодействия контроллера с viewModel. Дальше шла магия. Я переопределял проперти viewModel и добавлял поддержку нужного протокола. В свою очередь я создавал для конкретного экрана новый класс ViewModel, который реализовывал этот протокол. В итоге в BaseViewController в методе viewDidLoad проверял тип протокола модели, проверял список всех наследников у BaseViewModel, находил нужный мне класс и создавал viewModel нужного мне типа.

Пример базового 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


Пример базовой 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


Вспомогательные классы
#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;
}


Пример экрана логина
#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


Таким вот незамысловатым образом я делал ленивую инициализацию viewModel и слабую связанность представления с моделями посредством протоколов. При всём этом я в тот момент ничего ещё не знал об архитектуре MVP, хотя что-то подобное вырисовывалось у меня.
Навигация между экранами оставалась на усмотрении «viewModel», так как я добавил слабую ссылку на контроллер.

Вспоминая сейчас эту реализацию, я не могу сказать наверняка, что все было плохо. Идея по разделению слоев удалась, упростился момент создания и присвоения моделей в контроллер.
Но для себя я решил больше изучать уже готовые подходы и архитектуры, так как в ходе разработки приложения со своей собственной архитектурой, мне приходилось сталкиваться с множеством нюансов. Например, переиспользование экранов и моделей, наследование, сложные переходы между экранами. В тот момент мне казалось, что viewModel и была частью бизнес логики, хотя сейчас я понимаю, что это все еще слой представления. Я получил отличный опыт в ходе этого эксперимента.

MVVM с душой MVP


Уже набравшись опыта я решил выбрать для себя определенную архитектуру и следовать ей, вместо изобретения велосипедов. Я начал читать подробней об архитектурах, детально изучать популярные на тот момент и остановился на MVVM. Скажу честно, я сразу не понял ее сути, а выбрал потому, что понравилось название.

Я не сразу разобрался в сути архитектуры и связи между ViewModel и View (ViewController), но начал делать так как понимал. Глаза боятся, а руки неистово набирают код.

В своё оправдание добавлю, что на тот момент были очень сжаты сроки и времени на обдумывание и анализ созданного мною детища не хватало. Поэтому вместо биндингов я сделал прямые ссылки в ViewModel на соответствующий View. И уже в самой ViewModel делал настройку представления.

О MVP я имел такое же представление, как и о других архитектурах, поэтому свято верил в то, что заложил именно MVVM, в которой ViewModel оказались самыми настоящими презентероми.

Пример моей архитектуры “MVVM” и да, мне понравилась идея с RootViewController, который отвечает за самый высокий уровень навигации в приложении. О роутере написано ниже.
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
    }
    
    // MARK: - ViewModelProtocol
    
    func setup() {
        if AccountManager.shared.isLoggedIn() {
            router.route(to: RootRoutes.launch.rawValue, from: view, parameters: nil)
        } else {
            router.route(to: RootRoutes.loginregistartion.rawValue, from: view, parameters: nil)
        }
    }
    
    func backAction() {
        
    }
    
}


На качество проекта это особо не отразилось, так как соблюдался порядок и единый подход. А вот опыт оказался бесценным. После создаваемых мною велосипедов я наконец начал делать по общепринятой архитектуре. Разве что презентеры назывались не презентерами, что могло запутать стороннего разработчика.

Я решил, что в будущем стоит делать небольшие тестовые проекты, более детально вникать в суть того или иного подхода в проектировании. Так сказать, сначала пощупать на практике, а потом уже врываться в бой. Такой вот вывод я сделал для себя.

Вторая попытка с навигацией или Router и кривизна навигации


На том же проекте, где я доблестно и наивно внедрял MVVM, я решил попробовать и новый подход в навигации между экранами. Как я уже упоминал ранее, я все еще придерживался идеи по разделению экранов и логики перехода между ними.

Читая про MVVM, меня заинтересовал такой паттерн как Router. Опять-таки ознакомившись с описанием, я начал внедрять решение в свой проект.

Пример роутера
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?
    
    // MARK: Router
    
    func route(to routeID: String, from view: UIViewController, parameters: Any?) {
        
        var rootView = view
        
        if view is EPLaunchViewController {
            rootView = (view.navigationController?.viewControllers.first)!
            view.navigationController?.popViewController(animated: false)
        }
        
        if routeID == RootRoutes.loginregistartion.rawValue {
            loginRegistartionRouter = LoginRegistartionRouter(with: self)
            loginRegistartionRouter?.route(to: LRRouteID.phoneNumber.rawValue, from: rootView, parameters: nil)
        } else if routeID == RootRoutes.mainmenu.rawValue {
            if mainMenuRouter == nil {
                mainMenuRouter = MainMenuRouter(with: self)
            }
            mainMenuRouter?.route(to: MMRouteID.start.rawValue, from: rootView, parameters: nil)
        } else if routeID == RootRoutes.launch.rawValue {
            let storyboard = UIStoryboard(name: "RootStoryboard", bundle: nil)
            let launchView = storyboard.instantiateViewController(withIdentifier: "LaunchViewController") as! LaunchViewController
            let navigationController: UINavigationController = checkNavigationController(for: view)
            launchView.viewModel = LaunchViewModel(with: launchView, router: self)
            navigationController.pushViewController(launchView, animated: false)
        }
    }    
}


Отсутствие опыта в реализации подобного паттерна дало о себе знать. Вроде бы все было аккуратно и понятно, роутер создавал новый класс UIViewController, создавал для него ViewModel и выполнял логику перехода на этот экран. Но все же, много недочетов давали о себе знать.
Сложности начали возникать, когда нужно было открывать приложение с определенным экраном после пуш уведомления. В итоге, местами получилась запутанная логика по выбору нужного экрана и дальнейшая сложность в поддержке подобного подхода.

Я не забросил идею реализации Router, а продолжил в этом направлении, набираясь все больше опыта. Не стоит отказываться от чего-то после первой неудавшейся попытки.

Персистент манагер


Ещё один интересный класс-манагер в моей практике. Но этот сравнительно молодой. Все таки процесс развития состоит из проб и ошибок, а так как все мы, ну или большинство из нас, постоянно находимся в процессе развития, то и ошибки появляются всегда.

Суть проблемы такова, в приложении есть сервисы, которые должны висеть постоянно и при этом должны быть доступны во многих местах.

Пример: определение статус Bluetooth. В моем приложении в нескольких сервисах нужно понимать включён или выключен блютуз и подписываться на обновления статуса. Так как таких мест несколько: пару экранов, несколько дополнительных менеджеров бизнес логики и прочее, то возникает необходимость в каждом из них подписываться на делегат CBPeripheralManager (или CBCentralManager).

Решение вроде очевидное, создаём отдельный класс, который следит за статусом блютуз и через паттерн Observer оповещать всех, кому это нужно. Но тогда возникает вопрос, кто будет постоянно хранить этот сервис? Первое, что приходит в голову в этот момент, так это сделать его синглтоном! Вроде все ОК!

Но тут возникает момент, что подобных сервисов в моем приложении накопилось более чем один. Делать 100500 синглтонов в проекте тоже не хочется.

И тут над моей и так светлой головушкой загорелась очередная лампочка. Сделать один синглтон, который будет хранить все подобные сервисы и обеспечивать доступ к ним по всему приложению. Так и родился «постоянный манагер». С названием я долго не мудрил и назвал его, как уже все могли догадаться, PersistentManager.

Как видно, у меня ещё и очень оригинальный подход к именованию классов. Думаю, нужно в моем плане развития добавить пунктик о названии классов.

Ключевой проблемой в такой реализации, является синглтон, который доступен в любом месте проекта. А это приводит к тому, что менеджеры, которые используют один из постоянных сервисов, обращаются к нему внутри своих методов, что является неочевидным. Впервые я столкнулся с этим, когда делал большую сложную фичу в отдельном демо-проекте и переносил часть бизнес логики с основного проекта. Тогда я и начал получать сообщения с ошибками о недостающих сервисах.

Вывод, который я сделал после этого, то что нужно проектировать свои классы таким образом, чтобы не было скрытых зависимостей. Нужные сервисы необходимо передавать как параметры при инициализации класса, но не использовать синглтон, к которому можно обратиться с любого места. А ещё более красиво это стоит сделать с использованием протоколов.
Это оказался очередным подтверждением недостатком паттерна синглтон.

Итог


И того, я не стою на месте, а двигаюсь вперед, осваивая новые подходы в программировании. Главное двигаться, искать и экспериментировать. Ошибки будут всегда, от этого никуда не деться. Вот только как раз таки за счёт осознания своих ошибок можно качественно развиваться.



В большинстве случаев, проблемами являются супер классы, которые делают очень многое, либо неправильные зависимости между классами. Что наталкивает на мысль, что нужно более грамотно декомпозировать логику.

+3
2k 22
Comments 3
Top of the day