你的鼠标是一个数据库 (2012) - 浅谈 Rx 和异步数据流
Current Issue Past Issues Topics
2012年3月27日 Volume 10, issue 3
PDF
你的鼠标是一个数据库
Web 和移动应用正日益由异步和实时流服务以及推送通知组成。
Erik Meijer
当下 IT 行业最热门的流行语之一是 "big data",但这个 "big" 在某种程度上是一种用词不当:big data 不仅仅关乎容量,还关乎速度和多样性:4
• 数据的 volume 范围很广,从存储在传统 RDMS (关系数据库管理系统) 的封闭世界中的少量项目,到分布在大型机器集群或整个万维网上的大量项目。
• 数据的 velocity 范围很广,从消费者同步地从源拉取数据(负速度),到源异步地将数据推送到其客户端(正速度),速度各异,从毫秒级延迟的基于推送的股票报价流到应用程序每月从中央存储库中提取的参考数据。
• 数据的 variety 范围很广,从具有外键/主键关系的 SQL 风格的关系元组到具有键值指针的 coSQL 风格的对象或图,甚至像视频和音乐这样的二进制数据。
如果我们在 volume、velocity 和 variety 这三个维度上绘制 big data 的设计空间图,那么我们会得到如图 1 所示的 big-data 立方体。立方体的八个角中的每一个都对应于一种 (众所周知的) 数据库技术。例如,传统的 RDMS 位于后上角,坐标为 (small, pull, fk/pk),这意味着数据集很小;它假定一个完全受数据库控制的封闭世界,客户端在发出查询后同步地从数据库中拉取行,并且数据模型基于 Codd 的关系模型。基于 Hadoop 的系统(如 HBase)位于前左角,坐标为 (big, pull, fk/pk)。数据模型仍然是具有行、列和主键的矩形,结果仍然由客户端从存储中拉取,但数据使用某种分区方案存储在机器集群上。
当从上平面移动到下平面时,数据模型从具有主键和外键的行更改为对象和指针。在左下角,坐标为 (small, pull, k/v) 的是传统的 O/R (对象/关系) 映射解决方案,例如 LINQ to SQL 和 Hibernate,它们在关系数据库之上放置了一个 OO (面向对象) 装饰层。在立方体的前面是 LINQ to Objects,坐标为 (big, pull, k/v)。它使用 IEnumerable<T>
接口虚拟化实际数据源,该接口允许动态生成无限的项目集合。在右侧,立方体从批量处理变为流式传输或实时数据,其中数据源异步地将数据推送到其客户端。具有行和列数据模型的流式数据库系统,如 Percolator、StreamBase 和 StreamInsight 占据右上轴。
最后,在右下角,坐标为 (big, push, k/v) 的是 Rx (Reactive Extensions),或者有时被称为 LINQ to Events,这是本文的主题。
Rx 的目标是协调和编排基于事件的异步计算,如低延迟传感器流、Twitter 和社交媒体状态更新、SMS 消息、GPS 坐标、鼠标移动和其他 UI 事件、Web sockets 以及使用标准面向对象编程语言(如 Java、C# 或 Visual Basic)对 Web 服务的低延迟调用。
有很多方法可以推导出 Rx,其中一些涉及范畴论并诉诸数学对偶性,但本文展示了每个开发人员都可以通过将标准 JDK (Java Development Kit) Future<T>
接口与 GWT (Google Web Toolkit) AsyncCallBack<T>
接口交叉来发明 Rx,以创建成对的接口 IObservable<T>
和 IObserver<T>
,它们使用类型 T
的值对异步数据流进行建模。这对应于众所周知的 Subject/Observer 设计模式。然后,本文展示了如何通过将 UI 事件和 Web 服务公开为异步数据流并使用流畅的 API 组合它们来编写一个简单的 Ajax 风格的应用程序。
交叉 Futures 和 Callbacks
GWT 开发人员文档包含一个略带歉意的部分,名为 "Getting Used to Asynchronous Calls",3 其中解释说,虽然异步调用乍一看对开发人员来说是残酷和不自然的,但它们是防止 UI 锁定并允许客户端拥有多个并发未完成服务器请求的必要之恶。
在 GWT 中,同步方法的异步对应方法,例如 Person[] getPeople(...)
,它进行同步的跨网络调用并阻塞直到它返回一个 Person
数组,将返回 void
并采用一个额外的回调参数 void getPeople(..., AsyncCallback<Person[]> callback)
。回调接口有两个方法:void
onFailure(Throwable error)
,当异步调用抛出异常时调用;以及 void
onSuccess(T result)
,当异步调用成功返回一个结果值时调用。给定一个异步函数,如 getPeople
,一个调用通常传递一个匿名接口实现,该实现分别处理成功和失败回调,如下所示:
service.getPeople(startRow, maxRows, new AsyncCallback<Person[]>() { void onFailure(Throwable error) { _...code to handle failure..._ } void onSuccess(Person[] result) { _...code to handle success..._ } });
虽然 GWT 对异步的承诺值得称赞,但 GWT 错过了一个巨大的机会,即没有进一步细化和统一整个框架中的异步编程模型。例如,用于进行直接 HTTP 调用的RequestBuilder
类使用RequestCallback
接口,该接口有两个方法onError
和onResponseReceived
,它们实际上与先前讨论的AsyncCallback
接口的方法同构。
AsyncCallback
和 RequestCallback
接口都假定异步调用一次性交付它们的结果。然而,在该示例中,以流式方式增量返回 Person
数组的元素是完全合理的,特别是当结果集很大甚至无限时。您可以通过允许多次调用 onSuccess
方法来异步流式传输回结果,每次调用都用于结果数组的每个额外块,并通过添加一个方法 void onCompleted()
,该方法在所有块都已成功交付时调用。让我们调用这个派生接口 Observer<T>
,以表明它可以观察多个 T
值,然后完成,并反映标准的 Java 非泛型 observer
接口。
使用 Observer<T>
代替 AsyncCallback<T>
,异步计算与其客户端之间可能的交互序列是:(a) 在 _n_ ≥ 0
值之后成功终止;(b) 在 _i_
值之后不成功终止;或 (c) 永远不会完成的无限值流,如图 2 所示。
将回调直接作为参数传递给异步方法的另一个缺点是,一旦发出调用,撤销回调的调用是很棘手的,只留下你一个 void
在手中。例如,假设函数 getPeople
每分钟流式传输回已注册营销推广的人员姓名,但您只对接收前一千个姓名感兴趣。如果您在发出调用并收到 void
时没有预料到这种模式,您以后如何实现这一点?即使异步调用最多交付一个值,您也可能选择稍后忽略或取消调用,方法是在特定时间间隔内未收到结果后超时。同样,如果在将回调传递到 getPeople
时预料到这一点,这是可能的,但您以后无法改变主意。
这些小故障是异步计算和流未被视为可以从方法返回、存储在变量中的一流值等的事实的症状。下一节展示了如何通过引入一个额外的容器接口来制作异步数据流,该接口表示异步计算本身,并在其上注册要通知计算结果的回调。现在,异步方法可以返回一个表示挂起的异步计算或流的值,而不仅仅是 void
,您可以像对待任何其他常规值一样对待它。特别是,它允许您在发出调用后改变主意,并随意过滤、操作或转换计算。
Java SDK 已经以 Future<T>
接口的形式提供了(单次)异步计算作为一流值,其主要方法 T get()
检索计算结果,并在底层计算尚未终止时阻塞:
interface Future<T> { boolean cancel(boolean mayInterruptIfRunning); T get(); T get(long timeout, TimeUnit unit); boolean isCancelled(); boolean isDone(); }
请注意,原则上,Future<T>
可以用于生成多个值。在这种情况下,只要isDone()
不为 true,每次调用get()
都会阻塞并在下一个值可用时返回该值。这类似于iterable
接口。在本文的其余部分,假定异步计算返回多个结果流。
虽然 futures 确实提供了异步计算的一流表示,但 get
方法是阻塞的。幸运的是,您可以通过向 T get()
方法提供 Observer<T>
类型的回调来使 JDK 接口 Future<T>
非阻塞(引入该接口是为了扩展 GWT 的 AsyncCallback<T>
接口)。请注意,不再需要阻塞的 isCancelled
和 isDone
方法,因为该信息也通过回调传输。为简单起见,忽略 get
的第二个重载,因为您可以稍后轻松地重建它。应用这些更改后,Future<T>
接口的非阻塞版本如下所示:
interface Future<T> { boolean cancel(boolean mayInterruptIfRunning); void get(Observer<T> callback); }
您尚未完成重构。与其通过cancel
方法取消整个 future,不如只取消每个观察者的特定未完成的get
调用更有意义。这可以通过让get
返回一个表示可取消资源的接口来实现。此外,由于您已经调用了get
,因此无需指定mayInterruptIfRunning
参数,因为计算已经在该点运行,您可以决定是否调用cancel
来编码 Boolean。最后,您可以通过返回void
而不是Boolean
来使cancel
方法非阻塞。您可以尝试使cancel
返回一个Future<boolean>
,但这样您会陷入无休止的异步递归兔子洞。事实证明,java.io.Closable
接口完全符合要求,从而导致Future<T>
的以下变异:interface Future<T> { Closable get(Observer<T> callback); }
请注意,调用订阅返回的Closable
接口的close()
方法可能实际上并没有取消底层计算,因为单个 observable 可能有多个观察者(例如,释放对鼠标移动的订阅,不应阻止您的鼠标工作)。但是,由于该特定观察者未收到任何进一步的值通知,因此从它的角度来看,计算已终止。如果需要,实现IObservable<T>
的类可以以某种其他方式取消计算。
.NET 没有 Future<T>
和 Observer<T>
,而是具有标准的 IObservable<T>
和 IObserver<T>
接口;并且没有 Closable
,而是具有 IDisposable
。类型为 IObservable<T>
(或 Observer<T>
,具体取决于您喜欢的编程语言) 的值表示类型为 T
的值的异步数据流或事件流。
interface IObservable<T> { IDisposable Subscribe(IObserver<T> observer); } interface IObserver<T> { void OnNext(T value); void OnError(Exception error); void OnCompleted(); } interface IDisposable { void Dispose(); }
更仔细地检查生成的接口三位一体会发现,经典的 Subject/Observer 接口的泛型变体2 用于发布/订阅模式,这是面向对象程序员在处理基于事件的系统几十年来的工具箱中的主要内容。JDK 1.0 已经通过 (非泛型)Observable
类和Observer
接口支持此模式。在 .NET 中,Rx 库支持该模式。
Rx 库对 IObserver<T>
和 IObservable<T>
接口做出了一些额外的行为假设,这些假设没有通过它们的 (语法) 类型签名来表达:
- 对
IObserver<T>
接口的实例的调用序列应遵循正则表达式OnNext(t)* (OnCompleted() | OnError(e))?
。换句话说,在零个或多个OnNext
调用之后,可以选择调用OnCompleted
或OnError
中的一个。 - 可以假定
IObserver<T>
的实现是同步的;从概念上讲,它们在锁下运行,类似于常规的 .NET 事件处理程序或 reactor 模式。9 - 与观察者关联的所有资源都应在调用
OnError
或OnCompleted
时清理。特别是,一旦流完成,观察者的Subscribe
调用返回的订阅将由 observable 处理。实际上,这是通过关闭OnError
和OnCompleted
方法的实现中的Subscribe
返回的IDisposable
来实现的。 - 当订阅在外部被处理时,
IObservable<T>
流应尽_最大努力_尝试停止该订阅的所有未完成工作。任何已经在进行中的工作可能仍然会完成,因为它并不总是可以安全地中止进行中的工作,但不应向未订阅的观察者发出信号。
此约定确保可以轻松地推理和证明运算符和用户代码的正确性。
流畅地创建、组合和使用异步数据流
要在 Java 中创建 Observable<T>
的实例,您将使用匿名内部类并定义一个抽象基类 ObservableBase<T>
,该基类负责强制执行 Rx 约定。通过提供 subscribe
方法的实现来专门化它:
Observable<T> observable = new ObservableBase<T>() { Closable subscribe(Observer<T> observer) { ... } };
由于 .NET 缺少匿名接口,因此它使用工厂方法Observable.Create
从实现Subscribe
函数的Func<IObservable<T>, IDisposable>
类型的匿名委托创建一个新的 observable 实例:IObservable<T> observable = Observable.Create<T>( IObserver<T> observer => { ... } );
就像在 Java 解决方案中一样,Create
方法返回的具体类型强制执行所需的 Rx 行为。
一旦您拥有一个表示异步数据流的单个接口,您就可以将现有的基于事件和回调的抽象(如 GUI 控件)公开为异步数据流的源。例如,您可以使用以下美味的 token 拼盘将 Java 中 TextField
控件的 text-changed 事件包装为异步数据流:
Observable<string> TextChanges(JTextField tf){ return new ObservableBase<string>(){ Closable subscribe(Observer<string> o){ DocumentListener l = new DocumentListener(){ void changedUpdate(DocumentEvent e { o.OnNext(tf.getText());}; tf.addDocumentListener (l); return new Closable() { void close(){tf.removeDocumentListener(l);}}}}}
每次触发changedUpdate
事件时,类型为Observable<string>
的相应异步数据流都会向其订阅者推送一个新的字符串,表示文本字段的当前内容。
同样,您可以通过将它们包装为观察者来将具有 setter 的对象公开为异步数据流的接收器。例如,通过在每次调用 onNext
时将 listData
属性设置为给定的数组,将 javax.swing.JList<T>
列表公开为 Observer<T[]>
:
Observer<T[]> ObserveChanges(javax.swing.JList<T> list){ return new ObserverBase<T[]>() { void onNext(T[] values){ list.setListData(values); }}}
因此,您可以将 UI 控件、鼠标、文本字段或按钮视为流式数据库,每次底层控件触发事件时,它都会生成无限的值集合。相反,具有可设置属性的对象(如列表和标签)可以用作此类异步数据流的观察者。
由 IObservable<T>
接口(或 Java 中的 Observable<T>
)表示的异步数据流的行为类似于类型为 T
的值的常规集合,只不过它们是基于推送或流式的,而不是通常的基于拉取的集合,如实现 IEnumerable<T>
接口(或 Java 中的 iterable<T>
) 的数组和列表。这意味着您可以使用标准查询运算符的流畅 API 将异步数据流连接在一起,以高度可组合和声明的方式创建复杂的事件处理系统。
例如,Where
运算符采用 Func<S,bool>
类型的谓词,并过滤掉谓词不成立的所有值,输入类型为 IObservable<S>
的 observable 集合,这与在基于拉取的 IEnumerable<T>
集合上工作的表亲完全相同。图 3 说明了这一点。
使用此运算符,您可以清理公开为
IObservable<string>
流的文本字段输入,并使用查询表达式 input.Where(s=>!string.IsNullOrEmpty(s))
删除所有空字符串和 null 字符串。
在具有 lambda 表达式和 defender 方法的 Java 8 中,代码与此处显示的 C# 代码非常相似,只需对 lambda 使用 ->
而不是 =>
,以及变量名称的不同大小写。然而,即使没有这些即将推出的 Java 语言功能,您也可以近似于 Java 中的流畅接口——就像在 FlumeJava1 或 Reactive4Java8 中一样——使用标准查询运算符来操作事件流。例如,通过将运算符作为 ObservableBase<T>
上的方法,您可以将过滤器示例编写为:
input.Where<T>(new Func<string,T>{ Invoke(string s){ return !(s == null || s.length() == 0); }}
然而,为了避免我们所有人输入过多,接下来的几个示例仅以 C# 提供,即使没有任何特定于 C# 或 .NET 的内容。
Select
运算符采用转换函数 Func<S,T>
来转换类型为 IObservable<S>
的输入数据流中的每个值。这将生成类型为 IObservable<T>
的新的异步结果流,再次与基于 IEnumerable<T>
的版本完全相同,如图 4 所示。
SelectMany
运算符通常用于将两个数据流连接在一起,无论是基于拉取还是基于推送。SelectMany
采用类型为 IObservable<S>
的源流和类型为 Func<S, IObservable<T>>
的充气函数,并从原始源流中的每个元素生成一个包含零个、一个或多个元素的新的嵌套流。然后,它将所有中间异步数据流合并到类型为 IObservable<T>
的单个输出流中,如图 5 所示。
SelectMany
运算符清楚地显示了 IObservable<T>
的异步性质与 IEnumerable<T>
的同步性质之间的区别。如图 5 所示,源流上的值是异步出现的,即使您仍在从先前的充气函数生成值。在 IEnumerable<T>
的情况下,仅在生成了来自充气函数的所有值之后才从源流中拉取下一个值(即,输出流是所有后续充气函数生成的流的串联,而不是非确定性交织),如图 6 所示。
有时,使用更顺序的模式生成异步流的输出流很方便。如图 7 所示,Switch
运算符采用嵌套的异步数据流 IObservable<IObservable<T>>
并生成到目前为止收到的最新内部异步数据流的元素。这将生成类型为 IObservable<T>
的新的非嵌套异步数据流。它允许后来的流覆盖早期的流,始终产生“最新可能的结果”,非常像滚动新闻提要。
此时,坚定的读者可能会尝试创建他或她自己的
Switch
实现,事实证明,这非常棘手,尤其是在处理所有边缘情况的同时还满足 Rx 约定的情况下。
使用先前介绍的流畅 API,您可以用几行代码编写原型 Ajax “字典建议”程序。假设您有一个字典查找 Web 服务,给定一个单词,通过以下方法异步返回该单词的补全数组:
IObservable<string[]> Completions(string word){ ... }
还假设使用这些帮助程序,您已将 GUI 文本字段input
公开为IObservable<string>
,以在每次输入更改时生成文本字段的值,并且您已将标签output
包装为IObserver<string[]>
以显示字符串数组的异步数据流。然后,您可以连接一个管道,该管道为键入到文本字段中的每个部分单词异步调用Completions
服务,但仅在标签上显示最新结果:TextChanges(input) .Select(word=>Completions(word)) .Switch() .Subscribe(ObserveChanges(output));
Switch
运算符的效果是,每次响应于输入的更改而向Completions
发出另一个异步调用时,结果都会切换为接收此最新调用的输出,如图 8 所示,并且来自所有仍在挂起的先前调用的结果将被忽略。这不仅仅是性能优化。如果不使用
Switch
,则会对Completion
服务发出多个未完成的请求,并且由于流是异步的,因此结果可能会以任意顺序返回,从而可能使用较旧请求的结果更新 UI。
可以通过在查询中插入更多运算符来改进此基本字典示例。第一个运算符是 IObservable<T> DistinctUntilChanged(IObservable<T> source)
,它确保异步数据流仅包含不同的连续元素——换句话说,它会删除等效的 adjunct 元素。对于该示例,这确保仅在输入实际更改时才调用 Completions
。
其次,如果用户的输入速度快于您进行 Web 服务调用的速度,那么很多工作都会白费,因为您会触发许多请求,而这些请求仅会在输入再次更改之前立即取消,而之前的结果尚未返回。相反,您希望使用运算符 IObservable<T> Throttle(IObservable<T> source, TimeSpan delay)
至少等待自上次更改以来的 N
毫秒。throttle 运算符通过忽略在指定延迟之前跟随另一个值的值来采样异步数据流,如图 9 所示。
throttle 运算符会丢弃以太高速率传入的事件;但是,人们可以轻松定义其他运算符,这些运算符在(翻滚)窗口中聚合事件或以特定间隔采样输入流。
此处介绍的最终 Ajax 程序是一个数据流管道,仅当 input
在过去 10 毫秒内没有触发新的不同值时才调用字典服务;如果在先前值的完成返回之前出现新的输入值,则忽略该服务的结果:
TextChanges(input) .DistinctUntilChanged() .Throttle(TimeSpan.FromMilliSeconds(10)) .Select(word=>Completions(word)) .Switch() .Subscribe(ObserveChanges(output));
当然,IObservable<T>
接口不仅限于 UI 事件和异步计算,而且同样适用于任何基于推送的数据流,如推文、股票代码、GPS 位置等,当然也适用于我们从 GWT 开始的那种异步服务调用。例如,您可以将过滤给定主题标签的 Twitter 流建模为方法:IObservable<Tweet> TweetsByHashTag(string hashtag, ...)
这会将(无限)推文流推送到调用该函数的客户端。此外,向内看(而不是向外看),观察者是异步 I/O 和协处理器操作(如 DSP(数字信号处理器)和 GPU(图形处理单元)中的操作)的“完成端口”的自然表达。
展示我 (Co)Monads!
到目前为止,我们已经能够避免“M”字(以及“L”字),但再也无法隐藏它了。如果我们忽略诸如异常、终止和取消订阅之类的操作问题,并将事情归结为本质,则 IObservable<T>
和 IObserver<T>
接口表示 (T->())->()
类型的函数,它是 continuation monad,所有 monad 的母亲,也是一个 co-monad。
从历史上看,我们并非通过本文中执行的重构来发现 Rx 接口。相反,我们将来自 Wikipedia 的_分类对偶性_的定义字面意义地应用于基于拉取的集合的 IEnumerable<T>
和 IEnumerator<T>
接口,从而通过交换所有方法签名的参数和结果来完全机械地导出 IObservable<T>
和 IObserver<T>
接口,在此过程中没有受到任何操作直觉的引导。
请注意,我们的异步数据流模型对时间没有做出任何特殊的假设。这使得该方法不同于函数式编程中的典型反应式编程方法,如 Fran 或 FlapJax,它们强调(连续的)随时间变化的值(称为行为),以及基于 SQL 的复杂事件处理系统,如 StreamBase 和 StreamInsight,它们还在其语义模型中强调时间。相反,时钟和计时器被视为类型为 IObservable<DateTimeOffset>
的常规异步数据流。我们通过另一个接口 IScheduler
(此处略有简化)来参数化并发性和逻辑时钟,该接口表示具有本地时间概念的执行上下文,可以在未来安排在该时间上执行工作:
interface IScheduler { DateTimeOffset Now { get; } IDisposable Schedule(Action work, TimeSpan dueTime) }
Java 程序员会立即看到与executor
接口的对应关系,该接口在 Java SDK 中扮演着抽象精确的并发引入的相同角色。
结论
Web 和移动应用正日益由异步和实时流服务和推送通知组成,这是大数据的一种特殊形式,其中数据具有正速度。本文展示了如何将异步数据流公开为类型为 IObservable<T>
的基于推送的集合(与类型为 IEnumerable<T>
的基于拉取的集合相反),以及如何使用 Rx 库提供的流畅 API 运算符查询异步数据流。这个流行的库可用于 .NET 和 JavaScript(包括用于流行框架(如 JQuery 和 Node)的绑定),并且还随 Windows Phone 的 ROM 一起提供。F# 的一流事件基于 Rx,并且社区为其他语言(如 Dart7 或 Haskell6)创建了替代实现。
要了解有关 LINQ 的一般信息和 Rx 的详细信息,请阅读简短的教科书_Programming Reactive Extensions and LINQ_ 5
参考文献
- Chambers, C., Raniwala, A., Perry, F., Adams, S., Henry, R. R., Bradshaw, R., Weizenbaum, N. 2010. FlumeJava: easy, efficient, data- parallel pipelines. Proceedings of the ACM SIGPLAN Conference on Programming Design and Implementation ; https://dl.acm.org/citation.cfm?id=1806638.
- Eugster, P. Th., Felber, P. A., Guerraiou, R., Kermarrec, A-M. 2003. The many faces of publish/ subscribe. ACM Computing Surveys 35(2):114-131; https://dl.acm.org/citation.cfm?id=857076.857078.
- Google Web Toolkit. 2007. Getting used to asynchronous calls; http://www.gwtapps.com/doc/html/com.google.gwt.doc.DeveloperGuide.RemoteProcedureCalls.GettingUsedToAsyncCalls.html.
- Laney, D. 2001. 3D data management: Controlling data volume, velocity, and variety. Application Delivery Strategies; http://blogs.gartner.com/doug-laney/files/2012/01/ad949-3D-Data-Management-Controlling-Data-Volume-Velocity-and-Variety.pdf.
- Liberty, J., Betts, P. 2011. Programming Reactive Extensions and LINQ. New York: Apress; http://www.apress.com/9781430237471.
- Reactive-bacon; https://github.com/raimohanska/reactive-bacon.
- Reactive-Dart; https://github.com/prujohn/Reactive-Dart.
- Reactive4java; https://code.google.com/p/reactive4java/.
- Wikipedia. Reactor pattern; https://en.wikipedia.org/wiki/Reactor_pattern.
喜欢还是讨厌?请告诉我们 feedback@queue.acm.org
ERIK MEIJER (emeijer@microsoft.com) 在过去 15 年里一直致力于“Democratizing the Cloud”。他可能最出名的是他在 Haskell 语言方面的工作以及他对 LINQ 和 Rx Framework 的贡献。
© 2012 ACM 1542-7730/12/0300 $10.00
 最初发表于 Queue vol. 10, no. 3 — 在 ACM Digital Library 中评论这篇文章
更多相关文章:
Shylaja Nukala, Vivek Rau - Why SRE Documents Matter SRE(站点可靠性工程)是一种工作职能、一种思维方式和一套工程方法,用于使 Web 产品和服务可靠地运行。SRE 在软件开发和系统工程的交叉点运作,以解决运营问题并设计解决方案,从而以可扩展、可靠和高效的方式设计、构建和运行大型分布式系统。成熟的 SRE 团队可能拥有与许多 SRE 功能相关的明确定义的文档体系。
Taylor Savage - Componentizing the Web 当今软件工程中没有哪项任务能像 Web 开发那样艰巨。Web 应用程序的典型规范可能如下所示:该应用程序必须跨各种浏览器工作。它必须以 60 fps 的速度运行动画。它必须对触摸立即响应。它必须符合一组特定的设计原则和规范。它必须适用于几乎所有可以想象到的屏幕尺寸,从电视和 30 英寸显示器到手机和手表表面。它必须经过精心设计,并且在长期内可维护。
Arie van Deursen - Beyond Page Objects: Testing Web Applications with State Objects Web 应用程序的端到端测试通常涉及通过诸如 Selenium WebDriver 之类的框架与 Web 页面进行棘手的交互。隐藏此类 Web 页面复杂性的推荐方法是使用页面对象,但是首先要回答以下问题:测试 Web 应用程序时应创建哪些页面对象?您应该在页面对象中包含哪些操作?给定您的页面对象,您应该指定哪些测试场景?
Rich Harris - Dismantling the Barriers to Entry 一场战争正在 Web 开发领域进行。一方面是一群工具制造者和工具用户,他们热衷于破坏坏的旧想法(在这种环境中,“旧”意味着任何在 Hacker News 上首次亮相超过一个月的内容)以及关于 transpilers 等的激烈辩论。
© ACM, Inc. 保留所有权利。