什么是“环顾四周”惯用语?


Answers:


147

基本上是在这种模式下,您编写一种方法来执行始终需要执行的操作,例如资源分配和清理,并使调用者传递“我们想对资源进行的操作”。例如:

public interface InputStreamAction
{
    void useStream(InputStream stream) throws IOException;
}

// Somewhere else    

public void executeWithFile(String filename, InputStreamAction action)
    throws IOException
{
    InputStream stream = new FileInputStream(filename);
    try {
        action.useStream(stream);
    } finally {
        stream.close();
    }
}

// Calling it
executeWithFile("filename.txt", new InputStreamAction()
{
    public void useStream(InputStream stream) throws IOException
    {
        // Code to use the stream goes here
    }
});

// Calling it with Java 8 Lambda Expression:
executeWithFile("filename.txt", s -> System.out.println(s.read()));

// Or with Java 8 Method reference:
executeWithFile("filename.txt", ClassName::methodName);

调用代码无需担心打开/清理的一面,它将由处理executeWithFile

坦白讲,这在Java中很痛苦,因为闭包是如此罗,,从Java 8 lambda表达式开始可以像在许多其他语言中一样实现(例如C#lambda表达式或Groovy),并且这种特殊情况自Java 7使用try-with-resourcesAutoClosable流处理。

尽管“分配和清理”是给出的典型示例,但还有许多其他可能的示例-事务处理,日志记录,以更多特权执行某些代码等。它基本上有点类似于模板方法模式,但没有继承。


4
这是确定性的。Java中的终结器不是确定性的。就像我在上一段中所说的那样,它不仅用于资源分配和清理。可能根本不需要创建新对象。通常是“初始化和拆除”,但这可能不是资源分配。
乔恩·斯基特

3
因此,就像在C中,您有一个函数要传入函数指针来执行某些工作一样?
Paul Tomblin

3
另外,乔恩(Jon),您指的是Java中的闭包-它仍然没有闭包(除非我错过了它)。您所描述的是匿名内部类-它们不是一回事。真正的闭包支持(已建议-请参阅我的博客)将大大简化该语法。
philsquared

8
@Phil:我认为这是一个程度的问题。Java匿名内部类只能在有限的意义上访问其周围的环境-因此,尽管它们不是“完全”闭包,但它们却是“有限”闭包。我当然希望看到Java正确的闭包,尽管已选中(续)
Jon Skeet

4
Java 7添加了try-with-resource,Java 8添加了lambda。我知道这是一个古老的问题/答案,但我想在五年半后向所有提出此问题的人指出。这两种语言工具都将有助于解决发明这种模式所要解决的问题。

45

当您发现自己必须执行以下操作时,将使用Execute Around惯用法:

//... chunk of init/preparation code ...
task A
//... chunk of cleanup/finishing code ...

//... chunk of identical init/preparation code ...
task B
//... chunk of identical cleanup/finishing code ...

//... chunk of identical init/preparation code ...
task C
//... chunk of identical cleanup/finishing code ...

//... and so on.

为了避免重复执行总是围绕您的实际任务执行的所有这些冗余代码,您将创建一个自动处理它的类:

//pseudo-code:
class DoTask()
{
    do(task T)
    {
        // .. chunk of prep code
        // execute task T
        // .. chunk of cleanup code
    }
};

DoTask.do(task A)
DoTask.do(task B)
DoTask.do(task C)

这种习惯用法将所有复杂的冗余代码移到一个位置,并使您的主程序更具可读性(并且易于维护!)

看看这篇文章中的C#示例,以及这篇文章中的C ++示例。


7

一种执行方法周围是你传递任意代码的方法,该方法可以进行安装和/或拆卸的代码和执行之间的代码。

Java不是我会选择使用的语言。传递闭包(或lambda表达式)作为参数会更时尚。尽管对象可以等同于闭包

在我看来,Execute Around方法有点像控制反转(依赖注入),您每次调用该方法时都可以临时更改。

但这也可以解释为控制耦合的一个示例(通过这种方法告诉方法用其参数做什么)。


7

我在这里看到您有一个Java标记,因此即使该模式不是特定于平台的,我也将以Java为例。

这个想法是,有时在运行代码之前和之后,您的代码总是包含相同的样板。JDBC是一个很好的例子。在运行实际查询和处理结果集之前,您总是会抓住一个连接并创建一个语句(或准备好的语句),然后最后总是进行相同的样板清理-关闭该语句和连接。

execute-around的想法是,最好将样板代码排除在外。这样可以节省一些键入时间,但原因更深层。这是这里的“不重复自己”(DRY)原则-您将代码隔离到一个位置,这样,如果存在错误或需要更改它,或者只是想了解它,那么一切都集中在一个地方。

使用这种分解的方法有些棘手的是,您需要同时看到“之前”和“之后”部分的引用。在JDBC示例中,这将包括Connection和(Prepared)Statement。因此,要处理该问题,您实质上是用样板代码“包装”您的目标代码。

您可能熟悉Java中的一些常见情况。一种是servlet过滤器。另一个是关于建议的AOP。第三个是Spring中的各种xxxTemplate类。在每种情况下,您都有一些包装器对象,您的“有趣”代码(例如JDBC查询和结果集处理)将注入其中。包装对象执行“之前”部分,调用有趣的代码,然后执行“之后”部分。


7

另请参阅Code Sandwiches,它通过许多编程语言对这种构造进行了调查,并提供了一些有趣的研究思路。关于为什么可能使用它的特定问题,以上论文提供了一些具体示例:

每当程序操纵共享资源时,就会出现这种情况。用于锁,套接字,文件或数据库连接的API可能需要程序来显式关闭或释放它先前获取的资源。在没有垃圾回收的语言中,程序员负责在使用前分配内存,并在使用后释放内存。通常,各种编程任务要求程序进行更改,在该更改的上下文中运行,然后撤消更改。我们称这种情况为三明治。

然后:

代码三明治出现在许多编程情况下。有几个常见的示例与稀有资源的获取和释放有关,例如锁,文件描述符或套接字连接。在更一般的情况下,程序状态的任何临时更改都可能需要代码三明治。例如,基于GUI的程序可能会暂时忽略用户输入,或者OS内核可能会暂时禁用硬件中断。在这些情况下,如果无法恢复到较早的状态,将导致严重的错误。

本文没有探讨为什么使用这个习惯用法,但是确实描述了为什么没有语言级别的帮助,该习惯用法很容易出错:

有缺陷的代码夹在出现异常及其关联的不可见控制流时最常出现。实际上,用于管理代码三明治的特殊语言功能主要出现在支持异常的语言中。

但是,异常不是导致代码三明治缺陷的唯一原因。时改变了由身体码,新的控制路径可能出现的旁路的代码。在最简单的情况下,维护人员只需return在三明治的主体上添加一条语句即可引入新的缺陷,这可能会导致无声错误。当身体 代码是大和之前之后被广泛分离,这种错误可能很难在视觉上检测。


好点,Azurefrag。我已经修改并扩展了我的答案,因此它本身实际上更像是一个独立的答案。感谢您提出建议。
Ben Liblit 2014年

4

我将向四岁的孩子解释一下:

例子1

圣诞老人要来镇上。精灵们会在背后隐藏任何想要的代码,除非他们改变,否则事情会变得有些重复:

  1. 获取包装纸
  2. 获取超级任天堂
  3. 把它包起来。

或这个:

  1. 获取包装纸
  2. 获取芭比娃娃
  3. 把它包起来。

.... ad恶心一百万次,有一百万种不同的礼物:请注意,唯一不同的是步骤2。如果第二步是唯一的不同,那么圣诞老人为什么要复制代码,即为什么他要复制步骤1和3百万次?一百万个礼物意味着他不必要地重复步骤1和3一百万次。

四处执行有助于解决该问题。并有助于消除代码。步骤1和3基本上是恒定的,因此步骤2是唯一更改的部分。

范例#2

如果您仍然不明白,请举另一个例子:想想一个沙漏:外面的面包总是一样的,但是里面的面包却根据您选择的沙漏的类型(例如火腿,奶酪,果酱,花生酱等)。面包总是放在外面,您无需为正在制作的每种类型的沙子重复十亿次。

现在,如果您阅读以上说明,也许您会发现它更容易理解。我希望这个解释对您有所帮助。


+想像力:D
先生。刺猬

3

这让我想起了策略设计模式。注意,我指向的链接包括该模式的Java代码。

显然,可以通过编写初始化和清除代码并传入策略来执行“ Execute Around”,然后将其始终包裹在初始化和清除代码中。

与用于减少代码重复的任何技术一样,在至少需要2种情况(甚至3种情况)(YAGNI原则)之前,您不应该使用它。请记住,删除代码重复可以减少维护(减少代码副本意味着减少在每个副本之间复制修订的时间),但同时也增加了维护(更多的总代码)。因此,此技巧的代价是要添加更多代码。

这种类型的技术不仅对初始化和清除有用。当您想更轻松地调用函数时,它也很有用(例如,您可以在向导中使用它,以便“下一个”和“上一个”按钮不需要大写的case语句来决定要执行的操作下一页/上一页。


0

如果您想使用时髦的习惯用语,这里是:

//-- the target class
class Resource { 
    def open () { // sensitive operation }
    def close () { // sensitive operation }
    //-- target method
    def doWork() { println "working";} }

//-- the execute around code
def static use (closure) {
    def res = new Resource();
    try { 
        res.open();
        closure(res)
    } finally {
        res.close();
    }
}

//-- using the code
Resource.use { res -> res.doWork(); }

如果我的打开失败(例如获得重入锁),则调用关闭(例如尽管匹配的打开失败也释放一个重入锁)。
Tom Hawtin-大头钉
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.