ChatGPT解决这个技术问题 Extra ChatGPT

什么是“执行周围”成语?

我听说过的这个“Execute Around”成语(或类似的)是什么?为什么我可以使用它,为什么我不想使用它?

我没注意到是你,大头。否则我的回答可能会更讽刺;)
所以这基本上是一个方面吧?如果不是,它有什么不同?

J
Jon Skeet

基本上,在这种模式下,您编写一个方法来做总是需要的事情,例如资源分配和清理,并让调用者传入“我们想要对资源做什么”。例如:

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 个流。

尽管“分配和清理”是给出的典型示例,但还有很多其他可能的示例 - 事务处理、日志记录、执行一些具有更多权限的代码等。它基本上有点像 template method pattern,但没有继承。


这是确定性的。 Java 中的终结器不是确定性地调用的。同样正如我在上一段中所说,它不仅用于资源分配和清理。它可能根本不需要创建新对象。它通常是“初始化和拆除”,但这可能不是资源分配。
所以就像在 C 中你有一个函数,你传入一个函数指针来做一些工作?
另外,Jon,您指的是 Java 中的闭包——它仍然没有(除非我错过了它)。您所描述的是匿名内部类-它们并不完全相同。真正的闭包支持(如已提议的那样 - 请参阅我的博客)将大大简化该语法。
@Phil:我认为这是程度问题。 Java 匿名内部类可以在有限的意义上访问它们周围的环境——所以虽然它们不是“完整的”闭包,但我会说它们是“有限的”闭包。我当然希望在 Java 中看到正确的闭包,尽管已检查(续)
Java 7 添加了 try-with-resource,Java 8 添加了 lambdas。我知道这是一个古老的问题/答案,但我想向五年半后查看这个问题的任何人指出这一点。这两种语言工具都将有助于解决这个模式被发明来解决的问题。
e
e.James

当你发现自己不得不做这样的事情时,使用 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)

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

查看 this post 中的 C# 示例,查看 this article 中的 C++ 示例。


B
Ben Liblit

另请参阅 Code Sandwiches,它在许多编程语言中调查了这种结构,并提供了一些有趣的研究想法。关于为什么要使用它的具体问题,上面的论文提供了一些具体的例子:

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

然后:

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

论文没有探讨为什么不使用这个成语,但它确实描述了为什么这个成语在没有语言层面的帮助下很容易出错:

有缺陷的代码三明治最常出现在存在异常及其相关的不可见控制流的情况下。事实上,管理代码三明治的特殊语言特性主要出现在支持异常的语言中。然而,异常并不是代码三明治缺陷的唯一原因。每当对主体代码进行更改时,可能会出现绕过后续代码的新控制路径。在最简单的情况下,维护者只需在三明治的主体中添加一个 return 语句即可引入新的缺陷,这可能会导致静默错误。当正文代码很大并且前后分开很远时,这样的错误很难从视觉上检测到。


好点,azurefrag。我已经修改并扩展了我的答案,因此它本身就更像是一个独立的答案。感谢您提出这个建议。
B
Bill Karwin

Execute Around Method 是您将任意代码传递给方法的地方,该方法可以执行设置和/或拆卸代码并在两者之间执行您的代码。

Java 不是我选择使用的语言。传递闭包(或 lambda 表达式)作为参数更时尚。虽然对象可以说是 equivalent to closures

在我看来,Execute Around 方法有点像 Inversion of Control(依赖注入),您可以在每次调用该方法时临时更改。

但它也可以被解释为控制耦合的一个例子(通过它的参数告诉方法做什么,在这种情况下是字面意思)。


佚名

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

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

执行周围的想法是,如果您可以分解样板代码,那就更好了。这样可以节省一些打字时间,但原因更深。这是这里的不重复 (DRY) 原则——您将代码隔离到一个位置,因此如果有错误或者您需要更改它,或者您只是想了解它,它都在一个位置。

不过,这种分解有点棘手的是,您有“之前”和“之后”部分都需要查看的引用。在 JDBC 示例中,这将包括 Connection 和 (Prepared)Statement。因此,为了处理这个问题,您基本上用样板代码“包装”您的目标代码。

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


B
BenKoshy

我会像对一个四岁的孩子一样解释:

示例 1

圣诞老人要进城了。他的精灵在他背后编写任何他们想要的代码,除非他们改变,否则事情会变得有点重复:

获取包装纸获取超级任天堂。把它包起来。

或这个:

获取包装纸获取芭比娃娃。把它包起来。

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

到处执行有助于解决这个问题。并有助于消除代码。第 1 步和第 3 步基本上是不变的,允许第 2 步是唯一变化的部分。

示例 #2

如果你还是不明白,这里有另一个例子:想想三明治:外面的面包总是一样的,但里面的东西会根据你选择的沙子的类型而变化(例如火腿、奶酪、果酱、花生酱等)。面包总是在外面,你不需要为你创造的每一种沙子重复十亿次。

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


+ 想像力:D
B
Brian

这让我想起了strategy design pattern。请注意,我指向的链接包含该模式的 Java 代码。

显然,可以通过制作初始化和清理代码并传入一个策略来执行“执行周围”,然后总是将其包装在初始化和清理代码中。

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

这种技术不仅仅用于初始化和清理。当您想要更轻松地调用您的函数时,它也很有用(例如,您可以在向导中使用它,以便“下一个”和“上一个”按钮不需要巨大的案例语句来决定要做什么去下一页/上一页。


F
Florin

如果你想要时髦的成语,这里是:

//-- 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(); }

如果我的打开失败(比如获取可重入锁),则调用关闭(比如尽管匹配打开失败但释放可重入锁)。