Using the Strangler Fig with Mobile Apps
将 Strangler Fig 与移动应用程序结合使用
A case study of gradually modernizing an established mobile application
逐步对已建立的移动应用程序进行现代化的案例研究
Incremental replacement of a legacy mobile application is a challenging concept to articulate
and execute. However, we believe by making the investment in the pre-requisites of legacy modernization,
it is posible to yield benefits in the long term. This article explores the Strangler Fig
pattern and how it can be applied to mobile applications. We chart the journey of an enterprise who refused
to accept the high cost and risk associated with a full rewrite of their mobile application. By incrementally
developing their new mobile app alongside a modular architecture, they were able
to achieve significant uplifts in their delivery metrics.
传统移动应用程序的增量替换是一个具有挑战性的概念,难以阐明和执行。但是,我们相信,通过对遗留现代化的先决条件进行投资,有可能产生长期收益。本文探讨了 Strangler Fig 模式以及如何将其应用于移动应用程序。我们描绘了一家企业的旅程,该企业拒绝接受与完全重写其移动应用程序相关的高成本和风险。通过逐步开发新的移动应用程序以及模块化架构,他们能够显著提高交付指标。
30 October 2024 30 十月 2024
In this article we aim to show why taking an incremental approach to
legacy mobile application modernization can be preferable to the classical
'rewrite from scratch' methodology. Thoughtworks has the benefit of working with
large enterprise clients that are dependent on their in-house mobile
applications for their core business. We see many of them asking their
applications to do more and evolve faster, while at the same time, we see an
increasing rejection of reputationally damaging high risk releases.
在本文中,我们旨在说明为什么采用增量方法实现传统移动应用程序现代化比经典的“从头开始重写”方法更可取。Thoughtworks 的优势在于与依赖其内部移动应用程序实现其核心业务的大型企业客户合作。我们看到他们中的许多人要求他们的应用程序做得更多、发展得更快,与此同时,我们看到越来越多的人拒绝损害声誉的高风险发布。
As a solution, this article proposes alternative methods of legacy
modernization that are based in Domain Driven Design and hinge on the
application of the Strangler Fig pattern. While these concepts are far from
new, we believe that their usage in mobile applications are novel. We feel
that despite incurring a larger temporary overhead from their usage, this is
an acceptable tradeoff. We assert how the methodology is used to combat the
aforementioned attitudinal shifts in legacy mobile application development
while gaining a platform to lower risk and drive incremental value
delivery.
作为解决方案,本文提出了基于域驱动设计并取决于 Strangler Fig 模式应用的遗留现代化替代方法。虽然这些概念远非新概念,但我们相信它们在移动应用程序中的使用是新颖的。我们认为,尽管使用它们会产生更大的临时开销,但这是一个可以接受的权衡。我们断言如何使用该方法来对抗上述传统移动应用程序开发中的态度转变,同时获得一个降低风险并推动增量价值交付的平台。
We discuss how this works in theory, diving into both the architecture
and code. We also recount how this worked in practice when it was trialled on
a large, legacy mobile application at one of Thoughtworks’ enterprise
clients. We highlight how the pattern enabled our client to rapidly build,
test and productionize a modernized subset of domain functionalities inside
an existing legacy application.
我们将讨论理论上的工作原理,深入研究架构和代码。我们还讲述了在 Thoughtworks 的一个企业客户的一个大型传统移动应用程序上试用时,它在实践中是如何工作的。我们重点介绍了该模式如何使我们的客户能够在现有的遗留应用程序中快速构建、测试和生产化现代化的域功能子集。
We move on to evaluate the effectiveness of the trial by highlighting the business
facing benefits such as a signficantly faster time to value and a 50% reduced median cycle
time. We also touch on other expected benefits that should be used to
measure the success of this methodology.
我们继续评估试验的有效性,强调业务面临的好处,例如显著加快价值实现时间和缩短 50% 的中位周期时间。我们还谈到了应该用来衡量这种方法成功与否的其他预期收益。
The Problem with Mobile Legacy Modernization
移动遗留现代化的问题
As applications age and grow, they tend to deteriorate both in quality
and performance. Features take longer to get to market while outages
and rolled back releases become more severe and frequent. There is a
nuanced complexity to be understood about the reasons why this
occurs both at the code and organizational level.
To summarize though, at some point, an
organization will grow tired of the poor outcomes from their
software and start the process of legacy replacement. The decision
to replace may be made based on multiple factors, including (but not limited to)
cost/benefit analysis, risk analysis, or opportunity cost. Eventually a legacy modernization strategy will be chosen.
This will be dependent on the organization’s attitude to risk. For
example, a complex, high availability system may demand a more
incremental or interstitial approach to legacy
replacement/displacement than a simpler, less business critical one.
随着应用程序的老化和增长,它们的质量和性能往往会下降。功能需要更长的时间才能进入市场,而中断和回滚版本变得更加严重和频繁。关于在代码和组织级别发生这种情况的原因,需要了解一种微妙的复杂性。总而言之,在某些时候,组织会厌倦其软件的不良结果,并开始替换遗留软件的过程。更换的决定可能基于多种因素做出,包括(但不限于)成本/收益分析、风险分析或机会成本。最终将选择遗留式现代化策略。这将取决于组织对风险的态度。例如,一个复杂的高可用性系统可能需要一种更渐进式或间隙化的方法来处理遗留的替换/替换,而不是更简单、业务关键性较低的方法。
In the case of mobile application modernization, those decisions have
in recent memory been reasonably clear cut. A mobile application was
often designed to do an individual thing- Apple’s “There’s an app for
that” still rings out loud and clear in people’s minds 15 years after
the initial batch of advertisements. That message was one that was taken
to heart by organizations and startups alike: If you need to do
something, write an app to do it. If you need to do something else, write
another app to do that. This example struck me when I was
pruning the apps on my phone a couple of years ago. At the time I noticed I
had several apps from the manufacturer of my car; an older one and a newer
one. I also had two apps from my bank; one showed my checking account,
another that analyzed and illustrated my spending habits. I had three apps
from Samsung for various IoT devices, and at least two from Philips that
controlled my toothbrush and light bulbs. The point I’m laboring here is
that a mobile application was never allowed to get so complicated,
that it couldn’t be torn down, split out or started from scratch again.
在移动应用程序现代化的情况下,这些决策在最近的记忆中已经相当明确。移动应用程序通常旨在执行单个操作 - Apple 的“There's an app for that”在第一批广告发布 15 年后仍然在人们的脑海中响亮而清晰。“这个信息被组织和初创公司牢记在心: 如果你需要做某事,就写一个应用程序来做这件事。如果您需要执行其他操作,请编写另一个应用程序来执行此操作。 几年前,当我修剪手机上的应用程序时,这个例子让我印象深刻。当时我注意到我有几个来自我汽车制造商的应用程序;一个旧的和一个新的。我的银行也有两个应用程序;一个展示了我的支票账户,另一个分析和说明了我的消费习惯。我有 3 款来自 Samsung 的应用程序,适用于各种 IoT 设备,至少还有 2 款来自 Philips 的应用程序,用于控制我的牙刷和灯泡。我在这里要强调的是,移动应用程序从来没有被允许变得如此复杂,以至于它不能被拆除、拆分或重新开始。
But what happens when this isn’t the case? Surely not all apps are
created equal? Many believe that the mobile experience of the future
will be centered around so-called
“super-apps”; apps where you can pay, socialize, shop, call,
message, and game, all under one application. To some degree this has
already happened in China with “do-everything” applications like
‘WeChat’ and ‘AliPay’- we see the mobile device and its operating
system as more of a vehicle to allow the running of these gigantic
pieces of software. Comments from industry indicate a realization
that the West
is not quite as far along as China in this regard. But while not
at the super-app, there is no doubt that complexity of the mobile
app experience as a whole has increased significantly in recent
years. Take the example of YouTube, when first installed, back in
the early 2010’s, the application could play videos and not much
else. Opening the application today one is presented with “Videos”
and “Shorts”, a news feed, controllable categories, subscriptions,
not to mention a content editing and publishing studio. Similarly
with the Uber app, the user is asked if they want to order food.
Google Maps can show a 3D view of a street and Amazon now recommends
scrollable product-recommendation mood boards. These extra features
have certainly enriched a user’s experience but they also make the
traditional build, use, rebuild technique much more difficult.
但是,如果情况并非如此,会发生什么呢?肯定不是所有应用程序都是一样的吗?许多人认为,未来的移动体验将以所谓的 “超级应用程序”为中心;您可以在一个应用程序下进行支付、社交、购物、通话、消息和游戏的应用程序。在某种程度上,这种情况已经在中国发生,例如“微信”和“支付宝”——我们将移动设备及其操作系统更多地视为允许运行这些巨大软件的工具。来自工业界的评论表明,人们意识到 西方在这方面的进步不如中国。但是,尽管不是超级应用,但毫无疑问,近年来,整个移动应用体验的复杂性显著增加。以 YouTube 为例,在 2010 年代初首次安装时,该应用程序可以播放视频,而不能播放其他任何内容。今天打开应用程序,可以看到“视频”和“短裤”、新闻提要、可控类别、订阅,更不用说内容编辑和发布工作室了。与 Uber 应用程序类似,系统会询问用户是否要订购食物。谷歌地图可以显示街道的 3D 视图,亚马逊现在推荐可滚动的产品推荐情绪板。这些额外的功能无疑丰富了用户体验,但它们也使传统的构建、使用、重建技术变得更加困难。
This difficulty can be explained by considering some of the existing
common problems of mobile application development:
这种困难可以通过考虑移动应用程序开发的一些现有常见问题来解释:
- Massive View Controllers/Activities/Fragments
海量视图控制器/活动/片段 - Direct manipulation of UI elements
直接操作 UI 元素 - Platform specific code 特定于平台的代码
- Poor Separation of Concerns
关注点分离不佳 - Limited Testability 可测试性有限
With discipline, these problems can be managed early on. However, with
a large application that has grown chaotically inline with the business it
supports, incremental change will be difficult regardless. The solution then, as
before, is to build new and release all at once. But what if you only want
to add a new feature, or modernize an existing domain? What if you want to
test your new feature with a small group of users ahead of time while
serving everyone else the old experience? What if you’re happy with your
app store reviews and don’t want to risk impacting them?
通过纪律,这些问题可以及早得到管理。但是,由于大型应用程序已经与它所支持的业务无序地发展,因此无论如何,增量更改都将是困难的。然后,像以前一样,解决方案是同时构建 new 和 release。但是,如果您只想添加新功能或对现有域进行现代化改造,该怎么办?如果您想提前与一小群用户一起测试新功能,同时为其他人提供旧体验,该怎么办?如果您对应用商店的评论感到满意并且不想冒险影响它们怎么办?
Taking an incremental approach to app replacement then is the key to
avoiding the pitfalls associated with ‘big bang releases’. The Strangler
Fig pattern is often used to rebuild a legacy application in
place: a new system is gradually created around the edges of an old
one through frequent releases. This pattern is well known, but
not widely used in a mobile context. We believe the reason for this is that there are several prerequisites that need to be in
place before diving headfirst into the pattern.
因此,采用增量方法来替换应用程序是避免与“大爆炸版本”相关的陷阱的关键。 Strangler Fig 模式 通常用于就地重建遗留应用程序:通过频繁的发布,在旧系统的边缘逐渐创建一个新系统。此模式是众所周知的,但在移动环境中并未广泛使用。我们认为这样做的原因是,在一头扎进 pattern 之前,需要满足几个先决条件。
In their article on Patterns
of Legacy Displacement, the authors describe four broad
categories (prerequisites) used to help break a legacy problem into
smaller, deliverable parts:
在他们关于 遗留位移模式的文章中,作者描述了四大类(先决条件),用于帮助将遗留问题分解为更小的、可交付的部分:
- Understand the outcomes you want to achieve
了解您想要实现的结果 - Decide how to break the problem up into smaller parts
决定如何将问题分解为更小的部分 - Successfully deliver the parts
成功交付零件 - Change the organization to allow this to happen on an ongoing
basis
更改组织以允许这种情况持续发生
Only in the third point, can we envisage the invocation of the Strangler Fig
pattern. Doing so without an understanding of why, what or how it might
continue in the future is a recipe for failure.
只有在第三点中,我们才能设想 Strangler Fig 模式的调用。在不了解未来为什么、什么或如何继续的情况下这样做是失败的根源。
Going forward, the article charts how Thoughtworks was able to help one
of its enterprise clients expand its existing mobile legacy modernization
efforts into a successful experiment that demonstrated the value behind
the use of the Strangler Fig pattern in a mobile context.
展望未来,本文描绘了 Thoughtworks 如何帮助其企业客户将其现有的移动遗留现代化工作扩展到一个成功的实验中,展示了在移动环境中使用 Strangler Fig 模式背后的价值。
Satisfying the Prerequisites
满足先决条件
At this point, it seems appropriate to introduce the client that
inspired the writing of this article – a globally distributed business
with an established retail organization that had embraced mobile
applications for many years. Our client had realized the benefits an
app brought to provide a self-service experience for their
products. They had quickly expanded and developed their app domains to allow millions
of customers to take full advantage of all the products they sold.
在这一点上,介绍激发本文写作灵感的客户似乎是合适的 - 一家全球分布式企业,拥有一家成熟的零售组织,多年来一直采用移动应用程序。我们的客户已经意识到应用程序为他们的产品提供自助服务体验带来的好处。他们迅速扩展和开发了自己的应用域,让数百万客户能够充分利用他们销售的所有产品。
The organization had already spent a significant amount of time and
effort modernizing its mobile applications in its smaller
sub-brands. Responding to a lack of reuse/significant duplication of
efforts, high
cognitive load in app teams and slow feature delivery, the
organization chose a mobile technology stack that leveraged a
Modular Micro-app architecture. This strategy had been largely
successful for them, enabling proliferation of features common to
the organization (e.g. ‘login/registration/auth’ or ‘grocery shopping’)
across different brands and territories, in a fraction of the time it
would have taken to write them all individually.
该组织已经花费了大量时间和精力对其较小子品牌的移动应用程序进行现代化改造。为了应对缺乏重用/大量重复工作、应用团队 的高认知负荷 和功能交付缓慢等问题,该组织选择了利用模块化微应用架构的移动技术堆栈。这一策略对他们来说在很大程度上是成功的,使组织通用的功能(例如“登录/注册/身份验证”或“杂货店购物”)能够在不同品牌和地区扩散,而所需时间只是单独编写它们所花费的时间的一小部分。
The diagram above is a simplified representation of the modular
architecture the organization had successfully implemented. React
Native was used due to its ability to entirely encapsulate a
domain’s bounded context within an importable component. Each
component was underpinned by its own backend
for frontend (BFF) that came with the infrastructure as code to
instantiate and run it. The host apps, shown above as UK and US,
were simply containers that provided the app specific configuration
and theming to the individual micro-apps. This ‘full slice’ of
functionality has the advantages of both allowing re-use and
reducing complexity by abstracting application domains to micro-apps
managed by individual teams. We speak in depth about the results of
this architecture in the already referenced article on ‘Linking
Modular Architecture’.
上图是组织成功实施的模块化架构的简化表示。使用 React Native 是因为它能够将域的边界上下文完全封装在可导入组件中。每个组件都由其自己的 前端后端 (BFF) 为基础,该后端随基础设施即代码一起提供,用于实例化和运行它。主机应用程序(如上所示为 UK 和 US)只是为各个微应用程序提供应用程序特定配置和主题的容器。这种“完整切片”功能的优势在于,既允许重用,又通过将应用程序域抽象到由各个团队管理的微应用程序来降低复杂性。我们在已经引用的 '链接模块化架构' 的文章中深入讨论了这种架构的结果。
As touched upon earlier, the organization’s mobile estate was made up of
a number of smaller sub-brands that served similar products in other
territories. With the modular architecture pattern tried and tested, the
organization wanted to focus efforts on its 'home-territory' mobile
application (serving its main brand). Their main mobile app was much
larger in terms of feature richness, revenue and user volumes to that of
the sub brands. The app had been gaining features and users over many
years of product development. This steady but significant growth had
brought success in terms of how well-regarded their software was on both
Google and Apple stores. However, it also started to show the
characteristic signs of deterioration. Change frequency in the application
had moved from days to months, resulting in a large product backlog and
frustrated stakeholders who wanted an application that could evolve as
fast as their products did. Their long release cycle was related to risk
aversion: Any outage in the application was a serious loss of revenue to
the organization and also caused their customers distress due to the
essential nature of the products they sold. Changes were always tested
exhaustively before being put live.
如前所述,该组织的移动资产由许多较小的子品牌组成,这些子品牌在其他地区提供类似产品。在对模块化架构模式进行了试验和测试后,该组织希望将精力集中在其“主区域”移动应用程序(为其主要品牌提供服务)上。他们的主要移动应用在功能丰富度、收入和用户量方面比子品牌大得多。在多年的产品开发过程中,该应用程序一直在获得功能和用户。这种稳定但显着的增长在他们的软件在 Google 和 Apple 商店中的评价方面取得了成功。然而,它也开始显示出恶化的特征性迹象。应用程序中的更改频率已从几天变为数月,导致大量产品积压,并使利益相关者感到沮丧,他们希望应用程序能够像其产品一样快速发展。他们的长发布周期与风险规避有关:应用程序中的任何中断都会给组织带来严重的收入损失,并且由于他们销售的产品的基本性质,还会给客户带来痛苦。在上线之前,更改总是经过详尽的测试。
The organization first considered a rewrite of the entire application
and were shocked by the cost and duration of such a project. The potential
negative reception of a ‘big bang’ new release to their app store
customers also caused concerns in the levels of risk they could accept.
Suggestions of alpha and beta user groups were considered unacceptable
given the huge volumes of users the organization was serving. In this
instance, a modernization effort similar to that seen in their sub-brands
was believed to be of considerably higher cost and risk.
该组织最初考虑重写整个应用程序,并对此类项目的成本和持续时间感到震惊。应用商店客户对“大爆炸”新版本的潜在负面接受也引起了人们对他们可以接受的风险水平的担忧。鉴于该组织所服务的用户量巨大,Alpha 和 Beta 用户组的建议被认为是不可接受的。在这种情况下,类似于其子品牌中的现代化工作被认为具有相当高的成本和风险。
Thoughtworks suggested an initial proof of concept that built on the
successes of the reusability already seen with a modular
architecture. We addressed the organization’s big bang risk aversion
by suggesting the Strangler
Fig pattern to incrementally replace individual domains. By
leveraging both techniques together we were able to give the
organization the ability to reuse production-ready domains from
their modernized mobile apps inside their legacy app experience. The
idea was to deliver value into the hands of customers much sooner
with less duplication than in a full rewrite. Our focus was not on
delivering the most beautiful or cohesive full app experience (-not
quite yet anyway). It was about obtaining confidence both in the
stability of the iterative replacement pattern and also in how well
the new product was being received. These pieces of information
allowed the organization to make more informed product decisions
early on in the modernization process. This ensured the finished product
had been extensively used and molded by the actual end users.
Thoughtworks 提出了一个初步的概念验证,它建立在模块化架构已经看到的可重用性的成功之上。我们通过建议 Strangler Fig 模式 逐步替换单个域来解决组织的大爆炸式风险厌恶问题。通过结合使用这两种技术,我们能够让组织在其旧版应用程序体验中重用其现代化移动应用程序中的生产就绪域。我们的想法是比完全重写更快地将价值交付到客户手中,减少重复。我们的重点不是提供最漂亮或最有凝聚力的完整应用程序体验(至少现在还不完全是)。这是为了获得对迭代替换模式的稳定性以及新产品的接受程度的信心。这些信息使组织能够在现代化过程的早期做出更明智的产品决策。这确保了成品被实际的最终用户广泛使用和成型。
Strangler Fig and Micro-apps
Strangler Fig 和 Micro-apps
So how far did we get with the proof of concept and more importantly
how did we actually do this? Taking the learnings from Modular Micro-app
architecture (described above), we theorized the design to be as follows:
那么,我们在概念验证方面走了多远,更重要的是,我们实际上是如何做到的呢?从模块化微应用架构(如上所述)中汲取经验,我们将设计理论化如下:
The initial state of the application involved the identification of
domains and their navigation routes (Decide how to break the problem into
smaller parts). We focused our efforts on finding navigation entry points
to domains, we called them our ‘points of interception’. Those familiar
with mobile application development will know that navigation is generally
a well encapsulated concern, meaning that we could be confident that we
could always direct our users to the experience of our choosing.
应用程序的初始状态涉及域及其导航路线的识别 (决定如何将问题分解为更小的部分)。我们专注于寻找域的导航入口点,我们称它们为我们的“拦截点”。熟悉移动应用程序开发的人会知道,导航通常是一个封装良好的问题,这意味着我们可以确信我们始终可以将用户引导到我们选择的体验。
Once we identified our ‘points of interception’, we selected a domain
for incremental replacement/retirement. In the example above we focus on
the Grocery domain within the existing application. The ‘new‘ Grocery domain,
was a micro-app that was already being used within the sub-brand apps. The
key to implementation of the Strangler Fig pattern involved embedding an
entire React Native application inside the existing legacy application.
The team took the opportunity to follow the good modularity practices that
the framework encourages and built Grocery as an encapsulated component. This
meant that as we added more domains to our Strangler Fig Embedded
Application, we could control their enablement on an individual level.
一旦我们确定了我们的“拦截点”,我们就选择了一个用于增量替换/停用的域。在上面的示例中,我们重点介绍现有应用程序中的 Grocery 域。“新”杂货领域是一个微应用,已经在子品牌应用内使用。实现 Strangler Fig 模式的关键是将整个 React Native 应用程序嵌入到现有的遗留应用程序中。该团队借此机会遵循了框架鼓励的良好模块化实践,并将 Grocery 构建为封装组件。这意味着,随着我们向 Strangler Fig Embedded 应用程序添加更多域,我们可以在个人层面上控制它们的启用。
As per the diagram, in the legacy app, Grocery functionality was
underpinned by a monolithic backend. When we imported the New Grocery
Micro-app, it was configured to use that same monolithic backend. As
mentioned previously, each micro-app came with its own Backend for
Frontend (BFF). In this instance, the BFF was used as an anti-corruption
layer; creating an isolating layer to maintain the same domain model as
the frontend. The BFF talked to the existing monolith through the same
interfaces the legacy mobile application did. Translation between both
monolith and micro-app happened in both directions as necessary. This
allowed the new module’s frontend not to be constrained by the legacy API
as it developed.
根据该图,在旧版应用程序中,Grocery 功能由整体式后端支撑。当我们导入 New Grocery Micro-app 时,它被配置为使用相同的整体式后端。如前所述,每个微应用都有自己的前端后端 (BFF)。在本例中,BFF 被用作防腐层;创建一个隔离层来维护与前端相同的域模型。BFF 通过与旧版移动应用程序相同的界面与现有单体式应用进行对话。Monolith 和 Micro-App 之间的转换根据需要双向进行。这使得新模块的前端在开发过程中不受遗留 API 的限制。
We continued the inside out replacement of the old application by
repeating the process again on the next prioritized domain. Although out
of scope for this proof of concept, the intention was that the process
shown be repeated until the native application is eventually just a shell
containing the new React Native application. This then would allow the removal of the
old native application entirely, leaving the new one in its place. The new
application is already tested with the existing customer base, the
business has confidence in its resilience under load, developers find it
easier to develop features and most importantly, unacceptable risks
associated with a typical big bang release were negated.
我们通过在下一个优先域上再次重复该过程,继续对旧应用程序进行由内而外的替换。虽然超出了这个概念验证的范围,但其目的是重复显示的过程,直到本机应用程序最终只是一个包含新 React Native 应用程序的 shell。然后,这将允许完全删除旧的本机应用程序,将新的本机应用程序保留在其位置。新应用程序已经在现有客户群中进行了测试,企业对其在负载下的弹性充满信心,开发人员发现开发功能更容易,最重要的是,与典型的大爆炸版本相关的不可接受的风险被消除了。
Diving Deeper… 深入研究...
So far we’ve presented a very broad set of diagrams to
illustrate our Mobile Strangler Fig concept. However, there are
still many
outstanding implementation-focused questions in order to take theory
into
practice.
到目前为止,我们已经提供了一组非常广泛的图表来说明我们的 Mobile Strangler Fig 概念。然而,为了将理论付诸实践,仍有许多悬而未决的以实施为重点的问题。
Implanting the Strangler Fig
植入 Strangler Fig
A good start might be, how did we abstract the complexity of
building both native and non-native codebases?
一个好的开始可能是,我们如何抽象出构建原生和非原生代码库的复杂性?
Starting with the repository structure, we turned our original native
application structure inside out. By inverting the control
of the native application to a React Native (RN) application
we avoided significant duplication associated with nesting
our RN directory twice inside each mobile operating system’s
folder. In fact, the react-native init
default
template gave a structure to embed our iOS and Android
subfolders.
从存储库结构开始,我们将原始的本机应用程序结构彻底改造。通过将本机应用程序的控制权反转为 React Native (RN) 应用程序,我们避免了与在每个移动操作系统的文件夹中嵌套 RN 目录两次相关的大量重复。事实上, react-native init
默认模板提供了一个嵌入我们的 iOS 和 Android 子文件夹的结构。
From a developer perspective, the code was largely unchanged. The
legacy application’s two operating-system-separated teams were able to
target their original directories, only this time it was within a single
repository. The diagram below is a generalized representation (that is,
applicable to both iOS and Android) of the current pipeline from the
Client as we understood:
从开发人员的角度来看,代码基本没有变化。遗留应用程序的两个操作系统分开的团队能够定位其原始目录,只是这次它位于单个存储库中。下图是 Client 中当前管道的通用表示形式(即,适用于 iOS 和 Android),正如我们所理解的:
Bi-Directional Communication using the Native Bridge
We’ve already touched on navigation with our previously mentioned ‘points of interception’. It is worth looking deeper into how we facilitated communication and the transfer of control between native and React Native as it would be easy to oversimplify this area.
The React Native ‘Bridge’ enables communication between both worlds. Its purpose is to serve as the message queue for instructions like rendering views, calling native functions, event handlers, passing values etc. Examples of properties passed across the bridge would be isCartOpen or sessionDuration. While an example of a bridge function call might be js invocations of the device’s native geolocation module.
The diagram above also references the concept of a ‘React Native Micro App’. We introduced this concept earlier in the article when we described our app in terms of journeys. To recap though, a micro-app is a self-contained encapsulation of UI and functionality related to a single domain. A React Native app may be made up of many micro-apps similar to the micro frontend pattern. In addition to those advantages we have already discussed, it also allows us to have a greater degree of control over how our Strangler Fig application grows and is interacted with. For example, in a situation where we have more confidence in one of our new journeys than another we are afforded the option to divert a larger proportion of traffic to one micro-app without impacting another.
Bringing both concepts together, we utilized the bridge to seamlessly move our users back and forth across experiences. The ability to pass information allowed us to preserve any immediate state or action from the UI that needed to persevere across experiences. This was particularly useful in our case as it helped us to decouple domains at appropriate fracture points without worrying whether we would lose any local state when we crossed the bridge.
Handling Sensitive Data
So far we’ve discussed moving between legacy and new codebases as atomic entities. We’ve touched on how local state can be shared across the bridge, but what about more sensitive data? Having recently replaced their login and registration (auth) process in their other customer-facing React Native apps with a modular, configurable, brand agnostic one, the client was keen for us to reuse that experience. We set ourselves the task of integrating this experience as an initial demonstration of the Strangler Fig pattern in action.
We leveraged the techniques already discussed to implant the Strangler Fig: i.e. the new authentication journey on the React Native side. When a customer successfully logged in or registered, we needed to ensure that if they moved away from the new experience (back into the legacy journey), their authentication status was preserved no matter where they were.
For this, we utilized the native module code calling side of the
bridge. The diagram above explains how we achieved this by
using a React Native library that served as a wrapper to
save authentication data to the Android
EncryptedSharedPreferences or iOS Keychain after a
successful login. Due to the flexible structure of the data
inside the keystore, it allowed us to seamlessly share the
(re)authentication process irrespective of whether
the user was in the native or non-native experience. It also
gave us a pattern for the secure sharing of any sensitive
data between experiences.
为此,我们利用了桥的原生模块代码调用端。上图解释了我们如何通过使用 React Native 库来实现这一点,该库用作包装器,在成功登录后将身份验证数据保存到 Android EncryptedSharedPreferences 或 iOS 钥匙串中。由于密钥库内数据的灵活结构,它允许我们无缝共享(重新)身份验证过程,无论用户是原生体验还是非原生体验。它还为我们提供了一种在体验之间安全共享任何敏感数据的模式。
Regression Testing at Domain Boundaries
域边界的回归测试
An important part of a cutover strategy is the ability to know
from any vantage point (in our case, different teams working within the same app) whether a change made affected the
overall functionality of the system. The embedded app
pattern described above presents a unique challenge in this
regard around scalable testability of a multi-journey
experience. Moreover one that is managed by multiple teams
with numerous branching paths.
直接转换策略的一个重要部分是能够从任何有利位置(在我们的例子中,在同一个应用程序中工作的不同团队)了解所做的更改是否影响了系统的整体功能。上述嵌入式应用程序模式在这方面围绕多历程体验的可扩展可测试性提出了独特的挑战。此外,它由具有众多分支路径的多个团队管理。
The interaction diagram above shows an example journey flow
within the embedded app. One thing to notice is the amount
of branching complexity across a journey that is carrying
out just two concurrent experiments. We speak more on accidental complexity later in this section.
上面的交互图显示了嵌入式应用程序中的示例历程流程。需要注意的一点是,在仅执行两个并发实验的历程中,分支复杂性的数量。我们将在本节后面详细讨论意外复杂性。
The test
pyramid is a well known heuristic that recommends a
relationship between the cost of a test (maintenance and
writing) and its quantity in the system. Our client had kept
to the test pyramid and we found unit, subcutaneous and
journey-centric UI-driving tests when we examined their
code. The solution therefore was to continue to follow the
pattern: Expanding the number of tests across all layers and
also extending the suite of journey tests to incorporate the
jumping in and out of our embedded Strangler Fig app. But
there was a potential problem, ownership. We realized
that it would be unreasonable to tie the success of another
team’s build to code they did not write or were in control of.
We therefore proposed the following test strategy across
teams:
测试金字塔是一种众所周知的启发式方法,它建议测试的成本(维护和写入)与其在系统中的数量之间的关系。我们的客户一直遵循测试金字塔,当我们检查他们的代码时,我们发现了单元、皮下和以旅程为中心的 UI 驾驶测试。因此,解决方案是继续遵循以下模式:扩展所有层的测试数量,并扩展旅程测试套件,以纳入我们嵌入式 Strangler Fig 应用程序的跳入和跳出功能。但有一个潜在的问题,那就是所有权。我们意识到,将另一个团队构建的成功与他们没有编写或控制的代码联系起来是不合理的。因此,我们提出了以下跨团队测试策略:
Test Type 测试类型 | Native 本地 | React Native React 原生 |
---|---|---|
Unit 单位 | X | X |
Subcutaneous 皮下的 | X | X |
Legacy Journey 传承历程 | X | |
e2e Micro-app Journey e2e 微应用之旅 | X | |
Contract tests for interactions with ‘The Bridge’ (journeys with both legacy and micro-app components) 与“The Bridge”交互的合同测试(同时包含旧版和微应用组件的旅程) | X | X |
On the last table row, by contract we simply mean:
在表格的最后一行中,我们所说的 Contract 仅指:
If I interact with the bridge interface a particular way, I
expect a specific event to fire
如果我以特定方式与 bridge 接互,则预期会触发特定事件
For Native to RN interactions, these contracts act as blueprints
for micro-apps and enable unit testing with mocks. Mocks
simulate the behavior of the micro-app, ensuring it utilizes
the required context correctly.
对于 Native 到 RN 的交互,这些合同充当微应用的蓝图,并支持使用模拟进行单元测试。Mock 模拟微应用的行为,确保它正确利用所需的上下文。
The other way around (RN to Native) was similar. We identified
the Native functionality we wished to call through the
Bridge. RN then provided us with an object called
NativeModules which, when mocked, allowed us to assert
against the resulting context.
相反(RN 到 Native)是类似的。我们确定了希望通过 Bridge 调用的 Native 功能。然后,RN 为我们提供了一个名为 NativeModules 的对象,当模拟该对象时,它允许我们针对生成的上下文进行断言。
Defining these boundaries of responsibility meant that we could
limit the ‘regression-related’ cognitive load on teams through
‘hand-off’ points without compromising on overall app test
coverage.
定义这些责任边界意味着我们可以通过“交接”点来限制团队的“回归相关”认知负荷,而不会影响整体应用测试覆盖率。
This strategy was largely well received by both the native and
non-native teams. Where we did run into friction was the
complexity behind the implementation of the contract tests
across the bridge. The team running the legacy application
simply did not have the bandwidth to understand and write a
new category of tests. As a compromise, for the duration of
the PoC, all contract tests were written by the React Native
team. From this we learned that any interstitial state
required thought to be paid to the developer experience. In
our case, simply layering complexity to achieve our goals
was only part of the problem to be solved.
这种策略在很大程度上受到了原生和非原生团队的好评。我们确实遇到摩擦的地方是跨桥实施合同测试的复杂性。运行遗留应用程序的团队根本没有带宽来理解和编写新类别的测试。作为妥协,在 PoC 期间,所有合约测试均由 React Native 团队编写。从中我们了解到,任何间隙状态都需要考虑开发人员体验。在我们的案例中,简单地将复杂性分层以实现我们的目标只是要解决的问题的一部分。
Creating the Experiment 创建实验
Bringing everything together to form an experiment was the last
hurdle we had to overcome. We needed a means to be able to
demonstrate measurable success from two different
experiences and also have an ability to quickly backout and
revert a change if things were going wrong.
将所有东西放在一起形成一个实验是我们必须克服的最后一个障碍。我们需要一种方法,能够从两种不同的经历中展示可衡量的成功,并且能够在出现问题时快速退出和撤销更改。
The organization had an existing integration with an
experimentation tool, so out of ease, we chose it as our
tool for metric capture and experiment measurement. For experiment
user selection, we decided device level user selection (IMEI
number) would be more representative. This was due to the
potential for multiple device usage across a single account
skewing the results.
该组织已经与实验工具集成,因此出于轻松,我们选择它作为指标捕获和实验测量的工具。对于实验用户选择,我们认为设备级别的用户选择(IMEI 号码)将更具代表性。这是因为单个账户中的多个设备使用可能会使结果产生偏差。
We also utilized the feature
flagging component of the experimentation tool to allow us to ‘turn off’ the experiment (revert to
native app only) without the need for a release; greatly
reducing the time taken to recover should any outage occur.
我们还利用了实验工具的功能标记组件,允许我们“关闭”实验(仅恢复到原生应用程序),而无需发布;大大减少了发生任何中断时恢复所需的时间。
Significant Revisions 重大修订
30 October 2024: Published section on diving deeper
29 October 2024: Published up to Strangler Fig and Micro-Apps