Tag: 设计模式

符合条件概览

Swift 4.1已有几天推出,并且具有一个有趣的功能: 条件一致性 。 这是用于实现面向协议的编程以及API设计的新工具。 这篇文章是对这种崭新技术的快速介绍,并且肯定会在接下来的几周内进行深入探讨。 和往常一样,您可以在我的个人博客上找到原始帖子。 借助Swift,您可能会对实现扩展的所有不同方式感到困惑。 随着语言的成熟,出现了新的扩展可能性,开发人员必须选择合适的语言。 基本上,扩展允许扩展类型的行为而不必将其子类化: 进行实用程序功能或实现有效的工厂模式很有用。 对专用于泛型类型的基本扩展进行了改进: 例如,将sum()函数应用于Bool数组可能没有意义。 这种扩展允许开发人员将新功能引入现有类型,但使它们可以安全地同时使用。 当您设计供其他开发人员使用的API时,这非常有用。 一旦满足良好要求,他们就可以一次“免费”地受益于某些代码。 扩展不仅可以将行为添加到现有类型,还可以修改类型本身以使其符合协议。 这在存在异质性的地方带来了一致性。 如果不使String , Ints和Optionals符合相同的协议,就不可能将它们混合在一起。 但是,尽管Array的每个元素都是“ Resettable ”,但我们仍然必须对其进行解析(使用map )并在每个元素上调用“ reset() ”以实现全局操作。 有条件的一致性将大大帮助希望重置整个Array的开发人员无缝地执行此操作。 到目前为止,我们还没有谈到面向协议的编程。 该范例具有向协议添加默认行为的能力。 符合此协议的类型也将有益于此默认行为(或覆盖它)。 这是一种特殊的扩展名,并不是本文的重点。 因此,在这里我们不要引入过多的噪音,而只关注扩展具体类型的能力。 条件一致性是Swift 4.1随附的全新功能。 它是条件扩展和一致性扩展之间的混合。 如此,它继承了它们的两个关键原则: 带来了新功能,但使它们可以安全同时使用 在存在异质性的地方带来一致性 实际上,其背后的主要思想之一是:如果可以将行为应用于超集的每个元素,那么我们可以考虑将该行为也应用于超集本身: 由于Array的每个元素都是Resettable ,因此整个Array也是。 例如,这正是苹果公司对Equatable和Hashable所做的事情。 使用Swift 4.1时,等于数组的元素数组是等于数组的。 在我们的示例中,最大的功能是,我们可以在根Array中嵌入一个Resettables数组,并且仅使用一条语句“ resettableArray.reset() ”,即可重置整个数据结构。 这是处理递归的一种非常优雅的方法。 如我们所见,就API设计而言,与基本的“ 一致性扩展 ”相比,使用“ 条件一致性 ”具有很大的优势。 API设计人员将在他的框架中内部化他认为很聪明的代码(此处为reset()函数),以免费提供给符合相应要求的开发人员。 […]

软件构架:MVC设计模式

软件体系结构是关于做出基本的结构选择,一旦实施,更改成本很高。 每个软件开发项目都经历多个阶段:概念,设计,开发,测试等等。 作为一名初级开发人员,对我而言显而易见的是,在开始编写任何代码之前,至关重要的是,全面规划应用程序的体系结构基础,以确保您的应用程序从一开始就具有模块化,稳定和可扩展的特性。 架构设计有助于在项目中尽早发现潜在问题,以便有机会在施工开始之前进行更改。 有效的架构模式可提高代码的可读性,因为它们需要考虑直到以后才变得可见的问题。 什么是建筑设计模式? 设计模式是针对软件设计中常见问题的可重用解决方案。 它们是旨在帮助我们编写易于理解,易于测试和易于重用的代码的模板。 设计模式有助于创建松耦合的代码,以便以后可以在代码中轻松更改或替换其组件。 使用设计模式构建应用程序确实可以为您带来回报。 开发人员非常了解我们的应用程序会发生变化:您可能想添加新功能,可能需要修复一些错误,您的雇主可能会要求您在视图控制器上快速更改配色方案,或者您可能需要更新您的代码以与新版本的iOS软件或新设备保持兼容。 架构设计模式是使您的应用程序能够应对这些更改的准则。 现在我们已经回答了“ 为什么? 让我们来看看“ 如何? ” iOS设计模式: iOS开发最常见的设计模式是: MVC , MPV , MVVN和Viper 。 我将仅介绍MVC和MVVN,因为这些是我目前最感兴趣的设计模式。 在此博客中,我将重点关注MVC,接下来是关于MVVN的后续博客。 什么是MVC? 模型视图控制器(MVC)由Trygve Reenskaug于1979年发明。它是iOS开发中最常见的面向对象设计模式。 MVC可以非常清楚地分离应用程序的数据-逻辑,视图和控制器。 MVC根据对象的一般角色对它们进行分类,并鼓励根据每个角色对代码进行清晰的分离。 每个对象都属于以下组之一: 该模型负责数据和逻辑:诸如持久性,模型对象,解析器和网络代码之类的内容都存放在这里。 视图负责负责模型的可视化表示的对象以及用户可以与之交互的控件; 想任何以“ UI”前缀开头的东西。 Controller充当应用程序的视图对象及其模型对象之间的中介。 控制器编排查看事件和模型更改。 现在,我们了解了MVC如何管理这三个阵营之间的通信,我们的工作是根据每个对象所扮演的角色来明确区分代码。 遵守MVC设计模式需要不断关注对象的位置,以避免潜在的陷阱。 以下是一些需要牢记的准则: 控制器可以与之对话并了解有关该模型的所有信息。 控制器的工作是从模型中获取所需信息以显示给用户。 控制器还可以通过我们在控制器中创建的插座直接与视图对话。 视图只是控制器的奴才 -保持视图简单! 模特和模特 永远都不要说话。 MVC的缺点: 设计模式没有万灵药。 在MVC中,视图和控制器紧密耦合。 iOS视图控制器类通常将同时包含UI逻辑和数据逻辑,这将使一个逻辑的修改影响另一个逻辑-创建MVC的一种“ M / […]

依赖注入和链接委托调用

可测试的代码很棒。 即使您实际上没有编写任何单元测试,也可以使您的生活更轻松。 您将获得易于维护和重构的模块化结构。 到处都使用单例并不是可测试的代码。 我们经常听到人们对此表示同意,但是随后他们提出了“委托地狱”的论点。 我需要从应用程序的多层一直发送一些数据。 您是否建议我将其注入所有这些对象或使用十二个链接的委托调用? 啊! 好吧,让我们一起看看。 问题: 在这里使用共享实例并不能使我们的代码更具可测试性,清晰性或结构性,但是很容易实现: 解决方案1:共享实例 似乎链委托调用是这里唯一的选择,但是这种解决方案看起来很丑陋。 首先,链接的委托呼叫非常脆弱-您可以轻松地忘记一个。 其次,所有与主题菜单无关的对象现在也知道那里发生了某些事情。 解决方案2:链接的委托呼叫 还有其他解决方案吗? 当然! 现在的问题是,我们正在将构造责任与业务逻辑责任混在一起。 当我们需要一个对象时,我们只需在我们的类中内联实例化它即可。 我们的调用图与实例化图几乎相同。 让我们尝试将它们分开! 想象一下,应用程序下面的某些层需要一个数据库才能工作。 您可以清楚地说明此依赖性,并且您的层会在构造函数中请求数据库。 现在,它上面的一层不会说“我需要一个数据库才能将它传递给下面的层”,而是说“我需要下面的一层”! 上面的层没有关于数据库的线索。 如果您应用中某处的某个对象需要数据库,则并不意味着您需要在整个类层次结构中传递该数据库。 这是这样的: 解决方案3:工厂 最后,如果您还没有看过,请观看MiškoHevery的这场“清洁代码”演讲。

情节提要和服务定位器

关于如何精简视图控制器,进行依赖注入以及从不使用情节提要的文章很多。 我想写一点服务定位器模式如何提供一种轻量级的替代方案,例如使用Coordinators将事物连接在一起。 您仍然可以在情节提要中直观地查看应用程序流程,同时将配置与使用分开。 情节提要的一种未充分利用的技术是通过对象占位符将对象添加到视图控制器: 您可以像连接任何视图插座一样连接对象: 让我们继续定义一个简单的服务定位器: 类PostServiceLocator:NSObject { 懒惰的var dataSource:PostDataSource = { 返回PostDataSource() }() } 您可以为所有依赖项使用单个服务定位器,也可以为更复杂的应用程序使用,根据功能组创建单独的实例。 关于从情节提要中初始化对象的一件事要记住的是,它不会调用任何init()方法,而是通过awakeFromNib()方法调用。 我们的PostDataSource代码涉及更多。 首先,让我们创建一个协议: 协议TableViewDataSource { 关联类型ItemType var个项目:[ItemType] {get} func load(completion:(error:NSError?)-> Void) func tableView(tableView:UITableView,numberOfRowsInSection部分:Int)-> Int func tableView(tableView:UITableView,cellForRowAtIndexPath indexPath:NSIndexPath)-> UITableViewCell } 我们定义一个associatedType,以便我们可以添加协议扩展,该扩展知道如何从数据源中选择单个项目: 扩展TableViewDataSource { func itemInRow(row:Int)-> ItemType { 返回项目[行] } } 例如,这将允许从数据源中选择一个项目并将其传递给下一个视图控制器。 可以在github上找到完成实际工作的PostDataSource的代码,本文涉及太多。 那么我们如何在视图控制器中使用服务定位器呢? 我们将其连接如下: @IBOutlet私人var serviceLocator:PostServiceLocator! @IBOutlet私有var tableView:UITableView! 覆盖func viewDidLoad(){ […]

展示模型

让我们看一下MVC中的一个常见挑战,以及一个已有30年历史的解决方案。 我们将其与MVVM进行对比,然后进行权衡。 我们将以“ 邮件”收件箱为例。 前言:什么是MVC? 如果您听到以下内容,请阻止我: MVC太糟糕了! 我们有一个庞大的视图控制器,具有网络,集合视图布局和图像缓存。 我们离开了MVC,一切都已修复! 这不是针对MVC的罢工,而是我们的失败教训。 当控制器包含网络,存储或布局代码时,它不是MVC。 是“景观和泥浆球”。 该视图与演示有关,包括图形,布局和动画。 该模型涉及业务规则,包括与您的后端交谈。 控制器主要是位于视图和模型之间的胶水。 它设置场景,然后解释任一侧的事件。 在尝试新的设计模式之前,我们先弄清楚基础知识。 我们的MVC设计 模型:我们从MessageStore开始。 它具有fetchAllConversations()方法,该方法返回一个Conversation对象数组。 每个线程包含一个Message对象数组。 视图:具有MessageCell表视图单元格的表视图。 控制器:表格视图的数据源。 在幕后,当收到新消息时, MessageStore会收到推送通知,并检查REST端点是否有新消息。 或不。 它是模型背后抽象的实现细节。 我们要求收件箱中的每个对话都必须显示最新消息的预览。 当有新消息到达对话时,它会更新。 让我们逐步完成流程。 当MessageStore新消息时, MessageStore通知我们的控制器。 我们可以使用委托或NSNotificationCenter ,但这实际上取决于您。 类InboxViewController:UIViewController,UITableViewDataSource { var对话:[对话] = [] var messageStore:MessageStore! var tableView:UITableView! func sessionsDidUpdate(_ notification:Notification){ 对话= messageStore.fetchAllConversations() tableView.reloadData() } 覆盖viewDidLoad(){ super.viewDidLoad() //设置订户等 } } […]

设计iOS社交网络应用架构

最近,我发布了名为Socium的iOS社交网络应用模板。 在这里,我想与您分享我从开发中学到的知识。 功能清单 首先,我们需要写下要在应用程序中看到的功能列表。 例如,Socium与典型的社交网络应用程序一样,具有以下功能: 用户资料 用户帖子和评论 发表喜欢 浏览和搜索其他用户 私人实时聊天 追随者/追随者 推送通知 考虑到此列表,我们可以为项目选择一个后端。 后端 社交网络应用程序是一个复杂的系统,由客户端和服务器端组成。 Socium的服务器端建立在Parse Server之上。 Parse Server有许多优点:它是开源的,具有庞大的社区,并且具有出色的iOS SDK(顺便说一下,还有Android SDK)。 Parse Server需要VPS或Heroku之类的云应用程序平台,对于不熟悉后端内容的用户而言,可能难以设置和维护。 但是,我相信控制后端是一件好事。 即使需要一些其他技能。 毕竟,我从课程中学习。 数据库架构 现在,当我们知道要求时,就该写下所有必要的表及其之间的关系了。 这是我为Socium设计的架构: 让我们来看几个最有趣的实体(或Parse Server术语中的“类”)。 用户 / UserProfile 。 为了保护用户的电子邮件,我为公共用户配置文件创建了另一个类UserProfile,并与User类添加了一对一的关系。 任何人都可以看到UserProfile 。 用户仅对所有者和管理员可见。 UserProfile具有followings , followers和postLikes字段,它们分别是与UserProfile和Post实体的一对多关系。 他们全部 图片 。 我没有将图像直接存储在PFFile字段中,而是为此创建了一个单独的类。 因此,图像可以阵列和多对多关系存储。 如果我们想在照片中添加喜欢/不喜欢的功能,则可以提供更好的灵活性。 Post , PostComment , Conversation和Message实体非常不言自明。 客户端应用架构 定义数据库架构后,该考虑一下我们的iOS应用了。 […]

iOS设计模式

适配器,责任链,装饰器,门面 在本文中,我将介绍四种设计模式,您可以将它们与当前项目相关联,并且可以通过示例(例如可可如何适应这些模式)轻松理解。 让我们从适配器开始。 适配器设计模式将类的接口转换为客户端期望的另一个接口。 Adapter使类可以协同工作,否则由于接口不兼容而无法实现。 它将客户端与目标对象的类分离。 可可如何适应适配器设计模式? 通讯协定 协议是一种语言级别的(Objective-C)功能,可以定义作为适配器模式实例的接口。 协议本质上是一系列与类无关的方法声明。 (在Java中, 接口与协议同义。)如果希望客户端对象与另一个对象进行通信,但是对象的不兼容接口使此操作变得困难,则可以定义协议。 然后,另一个对象的类正式采用该协议,并通过实现该协议的一种或多种方法来“符合”该协议。 该协议可能需要一致的类来实现其某些方法,而其他类的实现则可选。 然后,客户端对象可以通过协议接口将消息发送到另一个对象。 注意:协议的设计与适配器模式的描述不完全匹配。 但这达到了模式的目标:允许具有其他不兼容接口的类一起工作。 用途和局限性 例如,Foundation框架包括NSObject , NSCopying和NSCoding协议,它们都是非常重要的协议。 AppKit协议包括NSDraggingInfo , NSTextInput和NSChangeSpelling 。 UIKit协议包括UITextInputTraits , UIWebViewDelegate和UITableViewDataSource 。 责任链设计模式通过为多个对象提供处理请求的机会,将请求的发送者与接收者分离。 模式将接收的对象链接在一起,并沿着链传递请求,直到对象处理它为止。 链中的每个对象要么处理请求,要么将请求传递给链中的下一个对象。 可可如何适应责任链设计模式? 响应链 应用程序框架包括称为响应者链的体系结构。 该链由一系列响应程序对象(即,从NSResponder继承的对象,或者在UIKit中,从UIResponder继承的UIResponder )组成,事件(例如,鼠标单击)或操作消息将沿着该UIResponder传递并(通常)最终得到处理。 如果给定的响应者对象不处理特定消息,则它将消息传递给链中的下一个响应者。 响应者对象在链中的顺序通常由视图层次结构决定,层次结构中从低层响应者到高级响应者的发展,最终以管理视图层次结构的窗口对象,窗口对象的委托,或全局应用程序对象。 响应者链上事件和操作消息的路径是不同的。 一个应用程序可以具有与窗口(甚至视图的局部层次结构)一样多的响应者链。 但是一次只能有一个响应者链处于活动状态,即与当前活动窗口关联的响应链。 注意:与响应者链密切相关的视图层次结构的设计适应了Composite模式。 用途和局限性 通过使用Interface Builder或以编程方式为程序构造用户界面时,您可以“免费”获得一个或多个响应者链。响应者链与视图层次结构紧密相关,在您创建视图时会自动获得它。对象是窗口内容视图的子视图。 如果您有一个自定义视图添加到视图层次结构,它将成为响应者链的一部分。 如果实现适当的NSResponder或UIResponder方法,则可以接收和处理事件和操作消息。 作为窗口对象或全局应用程序对象(AppKit中的NSApp )的委托的自定义对象也可以接收和处理这些消息。 装饰器设计模式动态地将附加职责附加到对象上。 装饰器为子类提供了灵活的替代方案,以扩展功能。 与子类一样,修饰器模式的改编允许您合并新行为而无需修改现有代码。 装饰器包装其行为扩展的类的对象。 它们实现与包装对象相同的接口,并在将任务委派给包装对象之前或之后添加自己的行为。 […]

SOLID Swift:第二部分

这是SOLID Swift系列的第二部分:在本文中,我们将研究Liskov替换和界面隔离 。 如果您想追随单一责任原则和开放/封闭原则 ,请随时阅读第一部分。 如简介中所述,第一部分是关于SOLID的前两个原则。 单一责任原则和开放/封闭原则已经是改进代码的两种重要方法! 接下来的两个: liskov替换和接口隔离 。 旁注:如第一部分所述,原则不仅是黑白的,还应根据您的情况,需求和要求进行调整。 非常清楚地表明违反了Liskov替代原则的危险信号之一是: if shape is Rectangle { … } else if shape is Square { … } 如果要添加新形状怎么办? 也许是Triangle ? 我们将不得不在列表中添加另一个 if。 另一个流行的例子更加微妙,可能不会马上被注意到。 想象一下具有width和height属性的Rectangle结构。 Square可以从Rectangle派生,因为正方形也具有宽度和高度。 但是,区别当然是对于正方形, width == height和矩形,该规则不适用。 如果我们要更新Rectangle.width ,那就好了,这对于Square.width因为Square.height尚未更改,因此违反了平方的规则。 同样,这违反了LSP。 这听起来像开/关原理吗? 没错,开放/封闭原则与LSP息息相关。 LSP走得更远。 LSP的基本规则是:派生类型(例如Square)必须完全可以替代其基本类型(在这种情况下为Rectangle)。 我们必须确保新的派生类型不会改变其行为的基础。 在开发过程中要记住的有关接口隔离的主要规则是:不应强迫客户端依赖于不使用的接口。 想象一下,我们有一个Employee ,它具有两个功能: lunch()和work()因为那是每个Employee所做的,对吧? 我们定义一个协议:可以通过func lunch()和func work() ,我们定义了class […]

iOS-SOLID原则第2页-开放/封闭原则

打开/关闭原理指出模块应该打开以进行扩展,而关闭则可以进行修改。 从本质上讲,在iOS中,这意味着应该通过扩展打开模块,无论是通过快速扩展(或Obj-C类别),子类还是通过依赖项注入(或可以提供的其他任何扩展模块)。 使用扩展模块的任何其他模块仍应能够使用它。 这与SRP原理非常吻合,因为您无需使用该模块更改其他模块中的任何内容。 我将演示第一个示例—在第一部分的ImageStorage类示例中进行子类化。 假设我们要添加一个新的ImageStorage类,它所做的只是保存带有黑色效果的图像。 我们要做的就是子类化ImageStorage并覆盖它的saveImage函数: 如你看到的 我们仍然可以像以前一样使用ImageStorage 。 它没有任何改变,因为我们以对扩展开放的方式编写了ImageStorage 。 它应保持关闭状态以进行修改,因为任何修改都可能使其他模块以我们不希望或期望的方式运行。 对于此示例,我们将修改ImageStorage类的工作方式。 我们将为其添加文件访问器。 从现在开始,所有文件I / O将成为访问者的工作。 (这也将给我们带来更高级别的职责分离。SRP FTW!) 首先,由于我们使用依赖注入,因此ImageStorage现在不能使用静态函数。 接下来,我们需要声明文件访问器协议: 现在,我们需要更改ImageStorage类。 我们将通过init()注入FilesAccessor,从现在开始,加载和保存功能将与FilesAccessor一起使用: 看看SRP效果如何? 现在,ImageStorage不再关心文件访问的责任。 现在,我们可以实现我们的DocumentDirectoryFileAccessor: 最后,我们需要修改ProfilePicManager以使其与具有特定FilesAccessor的特定ImageStorage一起使用 。 例如,我们可以这样做(第9行): 现在,我们可以欣赏代码的灵活性 我们可以走这行: let imageStorage = ImageStorage(访问器:DocumentDirectoryFilesAccessor()) 并替换为: let imageStorage = NoirImageStorage(访问器:RemoteServerFilesAccessor()) 然后,我们会将带有Noir效果的图像存储在服务器上 ( RemoteServerFilesAccessor将与某些服务层模块一起使用)。 视图控制器对此更改一无所知,并且扩展我们的ImagesStorage现在是无限的。 我们可以声明所需的任何访问器,并将它们注入到任何ImageStorage中 ,只要它们符合FileAccessor即可 。 这导致我们进入第3部分: Liskov替代原理。

iOS-SOLID原则第5页-依赖倒置原则

DIP —依赖反转原理,指出: A. 高级模块不应依赖于低级模块。 两者都应依赖 抽象 。 B. 抽象不应依赖细节。 细节应取决于抽象。 我将通过提供一些常用实现示例以及该原理说明的内容来尝试解释该原理。 假设您有一个UITableViewController,可显示附近的蓝牙设备。 我们将其称为NearestDevicesViewController。 VC及其逻辑是高级模块,它使用服务层中的低级模块, BLEClient或类似的东西。 常见的实现方式是NearestDevicesViewController具有BLEClient属性(并取决于BLEClient属性)。 这种方法根本不灵活。 更改BLEClient的工作方式或更改属性以容纳另一个类可能会破坏我们的NearDeviceDevicesViewController 。 更好的方法是使用NearestDevicesViewController需求的抽象。 我们将声明一些协议,例如DevicesProvider 。 该协议将宣布两者之间的“合同”。 视图控制器将持有某种符合该协议的类型的属性,从而使其依赖于抽象(协议),并且任何寻求帮助视图控制器的低层模块都将依赖于实现协议中提到的要求。 请注意,这与Liskov替代原则是很好的,因为此“合同”让我们在不更改程序的情况下,将设备提供程序替换为其他任何子类型) 该原理试图颠覆传统方法,即高级模块依赖于低级模块。 这种方法中的高级模块拥有抽象(通过确定协议的方法),然后这些低级模块就会遵循该抽象。 使较低级别依赖于较高级别模块的要求。 如上所述,这就是低层和高层模块都依赖抽象的地方(在本例中为协议): A. 高级模块不应依赖于低级模块。 两者都应依赖 抽象 。 在DevicesProvider的示例中,我们现在可以最大程度地享受灵活性。 例如,如果现在我们想将服务层BLEClient中的类更改为服务层GPSClient中的另一类,则可以通过依赖项注入来注入它,只要它符合DevicesProvider,并且一切都应该很好。 在这里看看我们如何解耦代码。 而不是这样: 我们现在有这个: 附近的设备视图控制器未与BLEClient耦合。 这意味着我们不必同时使用它们。 每个人都可以替换为另一个人,因为它们现在依赖于抽象而不是彼此依赖。 而已。 感谢您的支持,并在下面发表评论。 如果喜欢就拍手🙂