写点什么

如何实现 SwiftUI 微服务?

  • 2019-12-03
  • 本文字数:5993 字

    阅读完需:约 20 分钟

如何实现SwiftUI微服务?

在 SwiftUI 中实现应用程序架构的现代方法有哪些?本文从 SwiftUI 的发展过程切入,进而对 SwiftUI 的前沿状况进行了分析,并解答了一些关于 SwiftUI 微服务的问题。


SwiftUI 在 iOS、macOS 和其他所有苹果设备上,为应用程序开发带来了一种新的声明式、状态驱动和基于组件的方法。与此同时,我们的应用程序架构方法也应该向前发展了。但在我们前进之前,先来简要回顾一下历史和现在的前沿状况。

Model View Controller

开发 iOS 应用程序的经典方法是站在 MVC(模型-视图-控制器)的肩膀之上的。在 MVC 中,控制器(controller)在模型(model)和代表我们接口的各种视图(view)之间来回传递信息。



在 iOS 中,控制器将自身显示为单个对象,UIViewController。视图控制器管理所有用户交互和状态更改——包括信息加载、操作和数据更新,它还处理用户在我们的应用中各个屏幕和页面之间的来回跳转。


这种方法意味着控制器在我们的应用程序体系结构中负担着过于繁重的任务。实际上,它的作用是如此之大,以至于人们普遍称其为"巨型视图控制器"(Massive View Controller)。


显然,这种方法并不是最佳的。

Model-View-ViewModel

为了取代巨型视图控制器,人们提出了很多解决方案,其中大多数方案都能归结为某种形式的模型-视图-视图模型(MVVM)。


在这种方法中,模型和视图以及视图控制器仍然存在。但应用程序的内部结构、数据处理和业务逻辑已经从视图控制器提取出来,移到了视图模型(ViewModel)中。


为什么这样做呢?一方面来说它简化了视图控制器,但之所以从视图控制器中提取所有逻辑,主要目的是让这些逻辑可测试。我们可以实例化视图模型,并向其提供信息并调用其方法,还能直接观察被视图模型呈现给视图控制器的状态更改。



由于视图控制器的工作已简化为,只把这些状态更改传递给构成我们应用程序的视图,因此我们可以确信,只要视图模型的输出正确,我们的应用程序也将正确。


这种方法有多种变体:模型-视图-呈现器(MVP,model-view-presenter);VIPER;Clean。但它们都是基于相同的基本概念,主要区别在于它们如何在一组组件之间划分职责。


但所有人都认同一件事,那就是视图控制器应该尽可能地简单。

SwiftUI

苹果公司显然同意这一点,并在 WWDC 19 大会上推出了 SwiftUI,其一大特性就是取消了大多数用户定义和托管视图的控制器。在 SwiftUI 中,你可以使用一种简单的语法来声明你的用户界面。


此外,该接口完全由任意给定时间点的应用程序状态驱动。更改应用程序状态时,应用程序界面将立即更新以反映这些更改。苹果将​​此概念称为“单一事实来源”(Single Source of Truth)。



WWDC19“通过 SwiftUI 的数据流”讲座


但是,应用程序的任何给定部分都应有一个单一事实来源,并不一定意味着整个应用程序也应该有一个单一事实来源。


搞糊涂了?下面具体解释。

Composition

正如我在文章“SwiftUI中的View Composition”中所写的那样,苹果鼓励你将视图分解为许多小的、紧凑的、独立的组件,其中每个视图控制用户界面的一个特定部分。


我们再来看一下那篇文章中的一个组件,是一个收藏按钮(下图右上),用于指示给定项目应该已经被记录了,并显示在应用的“收藏夹”(favorites)列表中。



收藏按钮背后的代码如下:


struct FavoritesButton: View {    let item: MenuItem    @EnvironmentObject var favorites: FavoritesService    var imageName: String {        favorites.isFavorite(item) ? "star.fill" : "star"    }    var body: some View {        Image(systemName: imageName)            .foregroundColor(.accentColor)            .scaleEffect(1.2)            .onTapGesture {                self.favorites.toggleFavorite(self.item)            }    }}
复制代码


收藏按钮的界面和行为是完全自包含的,可以用在我们应用程序中任何视图的任何位置。如屏幕截图所示,我们甚至可以将其放入导航栏中。


struct DetailView: View {    let item: MenuItem    var body: some View {        ScrollView(.vertical) {            VStack {                ...            }        }        .navigationBarTitle("Details", displayMode: .inline)        .navigationBarItems(trailing: FavoritesButton(item: item))    }}
复制代码


点击导航栏中的收藏按钮,当前项目会被标记为收藏状态。再点一下会移除收藏。无论如何,DetailView 都不了解按钮的内部细节或实现。

收藏服务

尽管收藏按钮界面背后的代码是自包含的,但视图的基本功能在内部依赖 FaovritesService,这是一个已定义的环境对象,已插入视图层次结构中的较高层级上。


FavoritesService 是一个 SwiftUI ObservableObject(可观察对象),它向我们的视图暴露一个发布的值和两个方法。一个是 isFavorite(item)方法,该方法确定该项目是否已收入收藏夹;另一个是 toggleFavorite(item),该方法根据收藏情况切换项目的状态。


请注意,从此处或应用程序中的任何位置调用 toggleFavorite(item)时,我们的收藏夹项目列表都会更新,进而依赖 FavoritesService 的任何视图都会被要求更新其视图表示。


class FavoritesService: ObservableObject {    @Published var items: [MenuItem] = []    func isFavorite(_ menuItem: MenuItem) -> Bool {        items.firstIndex(where: { $0.id == menuItem.id }) != nil    }    func toggleFavorite(_ menuItem: MenuItem) {        if let index = items.firstIndex(where: { $0.id == menuItem.id }) {            items.remove(at: index)        } else {            items.append(menuItem)        }    }}
复制代码


FavoritesService 是此特定视图的单一事实来源。它对于其他视图也可能是一个事实来源,但 FavoritesButton 不关心这个。


FavoritesService 还遵守“单一责任原则”。它的目的是管理收藏夹菜单项列表,仅此而已。

应用程序标签页

我们看一下另一种服务,是一个非常简单的服务。


enum AppTabs: Int {    case favorites    case menu    case order}class AppState: ObservableObject {    @Published var currentTab = AppTabs.favorites}
复制代码


我们在这里跟踪应用程序的当前标签页状态,这样就可以根据需要在程序中转到特定标签页。


struct AppTabView: View {    @EnvironmentObject var appState: AppState    var body: some View {        TabView(selection: $appState.currentTab) {            FavoritesView()                .tabItem {                    Image(systemName: "star")                    Text("Favorites")                    }                .tag(AppTabs.favorites)            ...            }    }}
复制代码

订购服务

还有一个服务。这里是来自同一应用的 OrderService,用于跟踪已订购的商品。


class OrderService: ObservableObject {    @Published var items = <a href="">MenuItem    var total: Int {        items.reduce(0) { $0 + $1.price }    }    func isInCart(_ menuItem: MenuItem) -> Bool {        items.firstIndex(where: { $0.id == menuItem.id }) != nil    }    func add(item: MenuItem) {        items.append(item)    }    func remove(item: MenuItem) {        if let index = items.firstIndex(of: item) {            items.remove(at: index)        }    }}</a href="">
复制代码

Redux

由于应用程序的每个组件都应该有一个单一事实来源,因此有人提议 SwiftUI 应转向 Redux 风格的状态模型,整个应用程序应该有一个单一事实来源


class AppState: ObservableObject {    @Published var currentTab = AppTabs.favorites    @Published var menuItems: [MenuItem] = []    @Published var favoriteItems: [MenuItem] = []    @Published var orderItems: [MenuItem] = []}
复制代码


或者,如果你想维护组件行之间的功能,则可以尝试以下操作:


class AppState: ObservableObject {    @Published var currentTab = AppTabs.favorites    @Published var menu = MenuService()    @Published var favorites = FavoritesService()    @Published var order = OrderService()}
复制代码


将全局 AppState 导入到各个需要数据的视图中,就完成了。

单一全局状态的利弊

单个 AppState 的优点主要在于简单性。如前所述,你只需要处理一个 environmentObject 导入即可。


但对我来说,它的缺点有很多。


首先,它们会影响性能。对应用程序状态进行单个更改(例如,将单个项目标记为收藏状态),现在需要遍历应用程序中的每个单一视图树并检查更改。为什么?因为每个视图依赖的单一环境对象都发出了信号,表示一个更新已经发生了。


对于较小的应用程序,这里的性能影响可能不大。但是对于更大的应用呢?


(应该注意,这也是大型 React/Redux Web 应用程序面临的问题。)

全局数据

我认为的第二大缺点涉及应用程序数据的全局暴露。


将 AppState 导入到单个视图中,然后所有内容都会暴露给所有人查看。既然如此,如果不仔细检查视图的每行代码,你如何确定特定视图可能正在访问或操纵的信息是什么?


上面的 FavoritesButton 就是一个很好的反例。只要看一下代码的开头部分,我就能看出这段代码可以看到或更改的唯一内容就是 FavoritesService,因为这是从应用程序环境中导入的唯一对象。


此外,如果我想在其他应用程序中使用 FavoritesButton,也很容易看出来我还需要转移哪些内容到其他应用程序中。

测试

第三个缺点涉及测试。我们将代码分解为视图模型和服务的主要动机之一,就是让代码更容易测试。


在 SwiftUI 中,我们的应用程序完全由其状态控制。因此,如果我们将该状态放入模型或服务,并且该状态由于用户触发的操作而更改,那么在测试中我们就可以触发这些操作并观察状态的变化。


如果状态针对每个可能的更改或动作能正确更新,那么我们就能对我们的应用程序是否正确具有很高的信心。


但将所有状态放到一个容器中,就很难单独测试各个模型或服务。这样做很适合集成测试,但不适合单元测试。


即便如此,因为全局状态的关系,出现未测试内容并因此产生预期之外的副作用的可能性也非常高。“哦,我没意识到它也在改变那个变量!”

Late binding

正如我在 view-composition 那篇文章中所指出的,另一个 SwiftUI 最佳实践是将状态绑定到层次结构中尽可能低的位置上。



WWDC 19“通过 SwiftUI 的数据流”讲座


当我们绑定在层次结构中较低的位置时,由于任意给定更新只会影响视图树的一小部分,因而极大地减少了所需的接口更新和重新渲染的次数。


所有这些都显著提高了应用程序性能。


在上面的例子中,FavoritesService 直接绑定到需要它的对象上。要显示收藏按钮的 DetailView 既不知道也不在乎此事。当然,应该有较高级别的事物来提供它,但这是其他事物的另一种职责。

为什么是服务而不是视图模型?

有人可能会问为什么我们称它们为服务,而不只是视图模型。


在这里,关键的区别因素在于,视图模型通常是为驱动单个屏幕、页面或视图而编写的,并且该视图拥有该视图模型。


另一方面,服务会被注入应用程序环境中视图层次结构的某个级别,供较低级别的元素使用,从而在 SwiftUI 应用程序中的多个视图和组件之间共享。只要这个级别持续存在,对应的服务就会持续存在。


实际上,当在 SceneDelegate 中创建初始内容视图时,往往会创建许多服务并将它们注入到视图层次结构的最顶层。


func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {    // 创建SwiftUI视图,用来提供窗口内容。    let contentView = AppTabView()        .environmentObject(AppState())        .environmentObject(MenuService())        .environmentObject(MessageService())        .environmentObject(FavoritesService())        .environmentObject(OrderService())        .environmentObject(RatingsService())    // 使用一个UIHostingController作为窗口根视图控制器。    if let windowScene = scene as? UIWindowScene {        let window = UIWindow(windowScene: windowScene)        window.rootViewController = UIHostingController(rootView: contentView)        self.window = window        window.makeKeyAndVisible()    }}
复制代码


尽管更好的解决方案可能是使用一个系统服务修饰符,如《SwiftUI和缺少的环境对象》这篇文章中所述。


    let contentView = AppTabView()
复制代码


    .modifier(SystemServices())
复制代码


使用服务修饰符时如下所示:


struct SystemServices: ViewModifier {    private static var appState: AppState = AppState()    private static var menu = MenuService()    private static var messages = MessageService()    private static var favorites = FavoritesService()    private static var ratings = RatingsService()    private static var order = OrderService()    func body(content: Content) -> some View {        content            // defaults            .accentColor(.red)            // messages            .overlay(MessageOverlayView(), alignment: .top)            // services            .environmentObject(Self.appState)            .environmentObject(Self.menu)            .environmentObject(Self.messages)            .environmentObject(Self.favorites)            .environmentObject(Self.order)            .environmentObject(Self.ratings)    }}
复制代码


请注意,我们的 SystemServices 修饰符仅用来在需要时(例如当我们提供新的模态视图或动作表时)将服务注入 SwiftUI 环境。这就是为什么其成员是私有的原因。

SwiftUI 中的微服务

微服务架构的含义是将应用程序安排为一组松散耦合的服务。这些服务是细粒度的,它们之间的协议是轻量级的。


在微服务架构中,服务是可独立部署的。拿上面的 FavoritesService 的例子来说,我们看到了我们可以轻松地在另一个应用程序中重新部署这个服务和对应的接口组件。


最后,将它们称为"微"服务进一步强化了这样的理念,也就是说我们的服务应该小巧、定义明确,并且每个服务的实现都应针对性管理我们应用程序的某个方面。


单一事实来源。

结语

如果 SwiftUI 背后的主要目标是使用结构良好、独立且可复用的视图来构建应用程序,那么我们是否应该考虑以相同的方式实现内部服务架构?


这是我的观点,但如果你有其他意见,我也想听听。


注意:我的 iDine 应用程序源代码版本可从 GitHub 上的 iDine 仓库获取。它包括本文提到的示例


作者介绍:


Michael Long 是 CRi Solutions 的 iOS 高级首席工程师,这家公司是一流的 iOS、Android 以及移动公司和金融应用程序的开发商。


原文链接


https://medium.com/better-programming/swiftui-microservices-c7002228710


2019-12-03 14:365663

评论

发布
暂无评论
发现更多内容

中国程序员最容易发错的单词

happlyfox

GitHub 学习 程序人生 3月日更

重磅!Flutter中网络图片加载和缓存源码分析,BAT大厂面试总结

欢喜学安卓

android 程序员 面试 移动开发

舒畅,阿里大牛终于把困扰我多年的「Spring全家桶」讲明白了!十年IT老兵亲述Spring实战经验

Java架构之路

Java 程序员 架构 面试 编程语言

【得物技术】会议室巡检系统(哮天犬)部署分享

得物技术

分享 部署 巡检 得物技术 会议室

微服务指南

码语者

DevOps

DataPipeline通过华为鲲鹏兼容性认证,以自主科技创新推动中国信息产业进步

DataPipeline数见科技

大数据 数据融合

终于讲清楚了:深入理解Java 应用程序中 final 关键字的各种使用场景

老王说编程

Java final

想看新指标?教你轻松写prober插件

滴滴云

运维 滴滴夜莺 Obsuite prober插件

【邀请有礼】全球视频云创新挑战赛邀请有礼:参与 100% 获得 “壕” 礼,更有机会获得 JBL 音箱、Cherry 机械键盘

阿里云CloudImagine

阿里云 音视频 比赛

腾讯T2大牛手把手教你!2021新一波程序员跳槽季,算法太TM重要了

欢喜学安卓

android 程序员 面试 移动开发

萌新不看会后悔的C++基本类型总结(一)

花狗Fdog

当AI开始改造“文房四宝”:腾讯教育的脑洞与逻辑

脑极体

Nginx 模块系统:前篇

soulteary

nginx 动态模块

酷睿i7-10870H对比锐龙7 5800H游戏性能, 英特尔仍是游戏本CPU的更优选

E科讯

程序员之禅(三)

每天读本书

每天读本书

瓦力量化交易系统开发|瓦力炒币机器人软件APP开发

系统开发

农田治理效率低下还赔本?智慧农业力保粮食品质,效率事半功倍

一只数据鲸鱼

物联网 数据可视化 智慧城市 智慧农业 农业管理

CodeHub#4 启动报名| 荷小鱼:K12 在线教育应用的开发实践

蚂蚁集团移动开发平台 mPaaS

在线教育 mPaaS codehub 离线包

小树量化交易系统开发|小树炒币机器人软件APP开发

系统开发

从JVM底层原理分析数值交换那些事

Java 架构 JVM

2021“金三银四”刷爆朋友圈的“Java核心面试知识手册”这波Offer稳了

Java架构之路

Java 程序员 架构 面试 编程语言

“金三银四”面试别慌!最新阿里P8内部Java面试涨薪秘籍!全网最新已开源

Java架构之路

Java 程序员 架构 面试 编程语言

英特尔:i7-10870H 游戏性能超 R7 5800H,更强的 11 代酷睿 H 在后面

E科讯

华山版强势来袭!阿里巴巴Java性能优化2021年3月版(面试必备)

Java架构追梦

Java 阿里巴巴 架构 面试 性能优化

如何解决移动直播下的耳返延迟问题

融云 RongCloud

音视频 移动直播

基于 Wasm 和 ORAS 简化扩展服务网格功能

阿里巴巴云原生

Docker 容器 微服务 云原生 k8s

自动炒币机器人系统开发|自动炒币机器人APP软件开发

系统开发

Pano React Native SDK 来了!快速实现移动端音视频和白板

拍乐云Pano

flutter ios android RTC React Native

大赛报名|首次聚焦口罩场景!第三届 106 点关键点定位大赛开启

京东科技开发者

人工智能 深度学习 计算机视觉

CentOS安装Docker运行环境

wjchenge

Docker Centos 7

火币量化交易系统开发|火币炒币机器人软件APP开发

系统开发

如何实现SwiftUI微服务?_大前端_Michael Long_InfoQ精选文章