我的错误经历

我的错误经历


错误清单


  1. 万能类MCManager
  2. 发明屏幕之间的导航
  3. 几乎没有继承
  4. 我们自己生产的建筑或继续创造自行车
  5. 具有灵魂MVP的MVVM
  6. 导航或路由器和导航曲率的第二次尝试
  7. 永久经理

包括我自己在内的许多人都写了在给定情况下如何做正确的事情,如何正确编写代码,如何应用体系结构解决方案等。但是,我想分享一下我对如何做错了的经验以及得出的结论。根据他的错误做出的。 这些很可能是遵循开发人员道路的每个人的常见错误,或者可能是新的。 我只想分享我的经验并阅读其他人的评论。

万能类MCManager


在IT领域(尤其是iOS开发)的第一年工作之后,我决定我已经是一名架构师,并且已经准备好进行创建。 即使那样,我还是凭直觉理解到有必要将业务逻辑与表示层分开。 但是,我关于如何执行此操作的想法的质量远非现实。

我搬到了一个新的工作场所,在那里我被指派为现有项目独立开发新功能。 这类似于在Instagram上录制视频的模拟,在录制过程中,用户将手指按住按钮,然后将视频的多个片段连接在一起。 最初,决定将此功能作为一个单独的项目,或者以示例形式进行。 据我了解,这是我的架构问题的根源,这一问题持续了一年多。

将来,此样本将发展为用于录制和编辑视频的功能完善的应用程序。 有趣的是,样本最初有一个名称,是MC前缀的缩写。 尽管该项目很快就被重命名,但是仍保留了Objective-C,MC中名称约定所要求的前缀。 因此,万能的MCManager类就诞生了。



由于这是一个示例,并且一开始功能很简单,所以我决定一个经理类就足够了。 正如我前面提到的,功能包括录制带有开始/停止选项的视频片段,并将这些片段进一步组合为整个视频。 在那一刻,我可以命名我的第一个错误-MCManager类的名称。 MCManager,Karl! 类名称应向其他开发人员说出其用途,功能以及如何使用? 是的,绝对没有! 这是在附录中,附录的名称甚至不包含字母M和他的母亲C.

录像是一项小服务,管理文件系统中视频文件的存储是第二项服务,也是将几个视频合并为一个的附加服务。 这三项独立服务的工作,决定合并为一名经理。 这个想法很崇高,使用立面模式可以为业务逻辑创建一个简单的界面,并隐藏各种组件交互中所有不必要的细节。 在初始阶段,即使是这样的外观类名称也没有引起任何怀疑,尤其是在样本中。
但是客户喜欢这个演示,很快样本就变成了完整的应用程序。 您可以证明没有足够的时间进行重构,客户不想重做工作代码,但是坦率地说,那一刻,我本人以为自己已经建立了出色的体系结构。 实际上,将业务逻辑和表示分离的想法是成功的。 该体系结构是单例MCManager的一类,它是数十个服务和其他管理器的基础。 是的,它也是一个单例,可以从应用程序的各个角度使用。

人们已经可以了解整个灾难的规模。 一类包含数千行代码的类,这些代码很难阅读也很难维护。 我已经没有提到突出显示单个功能以将其转移到另一个应用程序的可能性,这在移动开发中很常见。
一段时间后,我为自己得出的结论不是创建具有晦涩名称的通用类。 我意识到逻辑需要分解,而不是为所有内容创建通用接口。 实际上,这是如果您不遵守SOLID原则之一(接口隔离原则)将会发生的情况的示例。

发明屏幕之间的导航


逻辑和接口的分离并不是让我担心上述项目的唯一问题。 我不会说那一刻我打算将屏幕代码和导航代码分开,但事实证明,我想出了自己的自行车进行导航。



该样本只有三个屏幕:一个带有录制视频表的菜单,一个录制屏幕和一个后处理屏幕。 为了不在乎导航堆栈包含重复的ViewController,我决定不使用UINavigationController。 我添加了RootViewcontroller,细心的读者已经猜到它是MCRootViewController,它被设置为项目设置中的主要控件。 同时,根控制器不是应用程序屏幕之一,它只是呈现了所需的UIViewController。 似乎这还不够,所以根控制器也是所代表的所有控制器的委托。 结果,在每个时刻,层次结构中只有两个vc,并且所有导航都是使用委托模式实现的。

外观:每个屏幕都有自己的委托协议,其中指定了导航方法,并且根控制器实现了这些方法并更改了屏幕。 RootViewController散布当前的控制器,创建一个新的控制器并展示它,同时可以将信息从一个屏幕传输到另一个屏幕。 幸运的是,业务逻辑处于最酷的单例类中,因此没有任何屏幕可以存储任何内容,并且可以轻松地销毁。 再次实现了良好的意图,尽管这种实现只停留在两条腿上,有时绊倒了。

您可能会猜到,如果需要从录像屏幕返回主菜单,则该方法称为:

- (void)cancel; 

或类似的东西,并且根控制器已经在做所有肮脏的工作。
结果,MCRootViewController成为了MCManager的类似物,但是在屏幕之间的导航中,随着应用程序的增长和新功能的添加,添加了新的屏幕。

自行车工厂工作异常顺利,我继续忽略有关移动应用程序体系结构的文章。 但是我从未放弃将导航与屏幕分离的想法。

优点是屏幕是独立的,可以重复使用,但这并不准确。 但是缺点包括难以维护此类。 当您需要通过滚动浏览先前选择的屏幕来返回时,缺少一叠屏幕的问题。 屏幕之间转换的复杂逻辑是,根控制器影响了业务逻辑的一部分,以便正确显示新屏幕。

通常,您不应该以这种方式在应用程序中实现所有导航,因为我的MCRootViewController违反了Open-Closed Principle原则。 扩展几乎是不可能的,并且必须不断对类本身进行所有更改。
我开始阅读有关移动应用程序屏幕之间导航的更多信息,熟悉路由器和协调器等方法。 由于要分享一些东西,我稍后再写路由器。

几乎没有继承


我不仅要分享自己的珍珠,还想分享别人不得不面对的有趣方法和解决方案,在我创作杰作的同一个地方,他们委托我完成一个简单的任务。 任务是将另一个项目的屏幕添加到我的项目中。 正如我们决定使用PM进行的那样,经过一番浅析浅析和一点思考,这应该花了两三个小时而不是更多,因为这有什么问题,您只需要向应用程序中添加现成的屏幕类即可。 实际上,已经为我们完成了所有工作,我们需要执行ctrl + c和ctrl + v。 这只是一个细微的差别,编写此应用程序的开发人员非常喜欢继承。



我很快找到了我需要的ViewController,很幸运,逻辑和表示没有分离。 当控制器包含所有必需的代码时,这是一种很好的旧方法。 我将其复制到我的项目中,并开始弄清楚如何使其工作。 我发现的第一件事是我需要的控制器是从另一个控制器继承的。 一件普通的事情,很期待的事情。 由于我没有太多时间,所以我找到了所需的课程并将其拖到我的项目中。 我想,现在它应该可以工作了,而且我从来没有错过!

我需要的类不仅有许多自定义类的变量,这些变量也需要复制到我的项目中,因此它们每个都继承了一些东西。 反过来,基类要么是继承的,要么是包含具有自定义类型的字段,正如许多人可能已经猜到的那样,它们继承了一些东西,但是不幸的是,这些不是NSObject,UIViewController或UIView。 因此,该项目中有三分之一的不必要项目迁移到我这里。

由于预计没有太多时间来完成此任务,因此我没有看到其他方法可以简单地添加xCode轻松地启动我的项目所需的必要类。 结果,只花了两三个小时,因为最后我不得不像真正的雕刻家一样,深入研究继承体系的整个网络。

结果,我得出的结论是,所有美好的事物都应该适度,即使像继承这样的“奇妙”事物也应如此。 然后,我开始了解继承的弊端。 我自己总结说,如果我想创建可重用的模块,则应该使它们更加独立。

我们自己生产的建筑或继续创造自行车


搬到一个新的工作场所并开始一个新项目,我考虑了架构设计中的所有可用经验并继续进行创作。 自然,我继续忽略已经发明的架构,但与此同时,我坚持坚持“分而治之”的原则。

Swift出现不久,所以我研究了Objective-c的可能性。 我决定使用语言功能进行依赖注入。 我受到lib扩展工具的启发;我什至不记得它的名字。

最重要的是:在BaseViewController基类中,我添加了BaseViewModel类字段。 因此,对于每个屏幕,我都创建了自己的控制器,该控制器继承了基本的控制器,并为控制器添加了与ViewModel进行交互的协议。 然后魔术来了。 我重新定义了viewModel属性,并增加了对所需协议的支持。 反过来,我为实现此协议的特定屏幕创建了一个新的ViewModel类。 结果,在ViewDidLoad方法的BaseViewController中,我检查了模型协议的类型,检查了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是业务逻辑的一部分,尽管现在我知道它仍然是表示层。 在这个实验中,我获得了丰富的经验。

具有灵魂MVP的MVVM


已经积累了经验,我决定为自己选择一种特定的体系结构并遵循它,而不是发明自行车。 我开始阅读有关体系结构的更多信息,以详细研究当时流行的内容并决定使用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() { } } 


这并没有特别影响项目的质量,因为遵守了顺序和单一方法。 但是经验是非常宝贵的。 创造了自行车之后,我终于开始按照公认的体系结构进行设计。 除非演示者被称为演示者,否则可能会使第三方开发人员感到困惑。

我认为,将来有必要进行小型测试项目,以更详细地研究特定设计方法的本质。 可以这么说,首先要在实践中体会,然后进入战斗。 这就是我为自己得出的结论。

导航或路由器和导航曲率的第二次尝试


在同一个项目中,我英勇地天真地实现了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并执行了切换到该屏幕的逻辑。 但是,仍然有许多不足之处。
当需要在推送通知后使用特定屏幕打开应用程序时,就开始出现困难。 结果,在某些地方,我们在选择正确的屏幕时遇到了令人困惑的逻辑,并且在支持这种方法时遇到了更多困难。

我没有放弃实施路由器的想法,而是继续朝这个方向发展,获得了越来越多的经验。 第一次尝试失败后不要放弃任何东西。

永久经理


在我的实践中另一个有趣的经理班。 但是这个还比较年轻。 同样,开发过程包含反复试验,并且由于我们所有人(或者我们大多数人)始终处于开发过程中,因此错误总是会出现。

问题的实质是,应用程序具有必须不断挂起的服务,同时应在许多地方都可用。

示例:确定蓝牙的状态。 在我的应用程序中,在几个服务中,我需要了解蓝牙是打开还是关闭并订阅状态更新。 由于有几个这样的地方:几个屏幕,几个其他的业务逻辑管理器等,因此每个人都必须订阅CBPeripheralManager(或CBCentralManager)委托。

解决方案似乎很明显,我们创建了一个单独的类来监视蓝牙的状态,并通过Observer模式通知需要它的所有人。 但是随后出现了一个问题,谁将永久存储此服务? 此时,我想到的第一件事就是使其成为单例! 一切似乎都还可以!

但是到了现在,我的应用程序中已经积累了不止一种这样的服务。 我也不想在项目中制作100500个单调。

然后另一盏灯在我已经明亮的小头顶上亮了起来。 使一个单例存储所有此类服务,并在整个应用程序中提供对它们的访问。 因此,“常任经理”诞生了。 有了这个名字,我想了很久,就叫它PersistentManager,这是每个人都已经猜到的。

如您所见,我还有一种非常新颖的类命名方法。 我认为我需要在开发计划中加入有关类名的流行。

此实现中的关键问题是单例,该单例可在项目中的任何位置使用。 这就导致了这样一个事实,即使用其中一种永久性服务的管理者可以在其方法内部访问它,这一点并不明显。 当我在一个单独的演示项目中制作一个大型复杂功能并从主项目转移部分业务逻辑时,我第一次遇到了这个问题。 然后,我开始收到带有关于缺少服务的错误的消息。

我在此之后得出的结论是,您需要以没有隐藏依赖项的方式设计类。 初始化类时,必须将必要的服务作为参数传递,但不能使用单例(可从任何地方访问)。 更妙的是,使用协议值得做。
事实证明这是缺乏单例模式的另一种确认。

总结


而且,我不会停滞不前,而是要前进,掌握编程的新方法。 最主要的是移动,寻找和试验。 错误总是存在的,这是无法逃脱的。 但是,仅仅因为认识到自己的错误,人们才能获得质的发展。



在大多数情况下,问题是做很多事情的超类或类之间的错误依赖关系。 这表明有必要更有效地分解逻辑。

Source: https://habr.com/ru/post/zh-CN451996/


All Articles