Các Execute xung quanh thành ngữ là gì?


151

Thành ngữ "Thực thi xung quanh" này là gì (hoặc tương tự) mà tôi đã được nghe về? Tại sao tôi có thể sử dụng nó và tại sao tôi không muốn sử dụng nó?


9
Tôi đã không nhận ra đó là bạn, tack. Nếu không, tôi có thể đã mỉa mai hơn trong câu trả lời của mình;)
Jon Skeet

1
Vì vậy, đây về cơ bản là một khía cạnh phải không? Nếu không, nó khác nhau như thế nào?
Lucas

Câu trả lời:


147

Về cơ bản, đó là mô hình nơi bạn viết một phương thức để thực hiện những việc luôn được yêu cầu, ví dụ như phân bổ và dọn dẹp tài nguyên, và làm cho người gọi chuyển qua "những gì chúng ta muốn làm với tài nguyên". Ví dụ:

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);

Mã cuộc gọi không cần phải lo lắng về phía mở / dọn dẹp - nó sẽ được chăm sóc bởi executeWithFile.

Điều này thực sự gây đau đớn trong Java vì các bao đóng quá dài dòng, bắt đầu bằng các biểu thức lambda Java 8 có thể được triển khai như trong nhiều ngôn ngữ khác (ví dụ: biểu thức C # lambda hoặc Groovy) và trường hợp đặc biệt này được xử lý từ Java 7 với các luồng try-with-resourcesAutoClosableluồng.

Mặc dù "phân bổ và dọn dẹp" là ví dụ điển hình được đưa ra, có rất nhiều ví dụ khác có thể có - xử lý giao dịch, ghi nhật ký, thực thi một số mã với nhiều đặc quyền hơn, v.v ... Về cơ bản, nó hơi giống mẫu phương thức mẫu nhưng không có sự kế thừa.


4
Đó là quyết định. Các công cụ hoàn thiện trong Java không được gọi là xác định. Như tôi đã nói trong đoạn cuối, nó không chỉ được sử dụng để phân bổ và dọn dẹp tài nguyên. Nó có thể không cần phải tạo ra một đối tượng mới. Nói chung là "khởi tạo và phá bỏ" nhưng đó có thể không phải là phân bổ tài nguyên.
Jon Skeet

3
Vì vậy, nó giống như trong C nơi bạn có một hàm mà bạn truyền vào một con trỏ hàm để thực hiện một số công việc?
Paul Tomblin

3
Ngoài ra, Jon, bạn đề cập đến các bao đóng trong Java - cái mà nó vẫn không có (trừ khi tôi bỏ lỡ nó). Những gì bạn mô tả là các lớp bên trong ẩn danh - không hoàn toàn giống nhau. Hỗ trợ đóng thực sự (như đã được đề xuất - xem blog của tôi) sẽ đơn giản hóa cú pháp đó đáng kể.
philsquared

8
@ Phil: Tôi nghĩ đó là vấn đề bằng cấp. Các lớp bên trong ẩn danh Java có quyền truy cập vào môi trường xung quanh theo nghĩa hạn chế - vì vậy trong khi chúng không đóng "đầy đủ" thì chúng sẽ đóng "giới hạn". Tôi chắc chắn muốn thấy các bao đóng thích hợp trong Java, mặc dù đã được kiểm tra (tiếp tục)
Jon Skeet

4
Java 7 đã thêm try-with-resource và Java 8 đã thêm lambdas. Tôi biết đây là một câu hỏi / câu trả lời cũ nhưng tôi muốn chỉ ra điều này cho bất cứ ai nhìn vào câu hỏi này năm năm rưỡi sau. Cả hai công cụ ngôn ngữ này sẽ giúp giải quyết vấn đề mà mẫu này được phát minh để khắc phục.

45

Thành ngữ xung quanh thành ngữ được sử dụng khi bạn thấy mình phải làm một cái gì đó như thế này:

//... 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.

Để tránh lặp lại tất cả các mã dự phòng này luôn được thực thi "xung quanh" các tác vụ thực tế của bạn, bạn sẽ tạo một lớp tự động chăm sóc nó:

//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)

Thành ngữ này di chuyển tất cả các mã dự phòng phức tạp vào một nơi và để chương trình chính của bạn dễ đọc hơn nhiều (và có thể duy trì!)

Hãy xem bài đăng này để lấy ví dụ về C # và bài viết này cho ví dụ về C ++.


7

Một Execute Khoảng Phương pháp là nơi bạn vượt qua mã tùy ý để một phương pháp, có thể thực hiện cài đặt và / hoặc teardown mã và thực thi mã của bạn ở giữa.

Java không phải là ngôn ngữ tôi chọn để thực hiện điều này. Nó hợp thời trang hơn để vượt qua một bao đóng (hoặc biểu thức lambda) làm đối số. Mặc dù các đối tượng được cho là tương đương với đóng cửa .

Dường như đối với tôi, Phương thức thực thi xung quanh giống như Inversion of Control (Dependency Injection) mà bạn có thể thay đổi ad hoc, mỗi khi bạn gọi phương thức.

Nhưng nó cũng có thể được hiểu là một ví dụ về Kiểm soát khớp nối (cho biết một phương thức phải làm gì bằng đối số của nó, theo nghĩa đen trong trường hợp này).


7

Tôi thấy bạn có một thẻ Java ở đây vì vậy tôi sẽ sử dụng Java làm ví dụ mặc dù mẫu không dành riêng cho nền tảng.

Ý tưởng là đôi khi bạn có mã luôn liên quan đến cùng một bản mẫu trước khi bạn chạy mã và sau khi bạn chạy mã. Một ví dụ điển hình là JDBC. Bạn luôn lấy một kết nối và tạo một câu lệnh (hoặc câu lệnh đã chuẩn bị) trước khi chạy truy vấn thực tế và xử lý tập kết quả, và sau đó bạn luôn thực hiện cùng một công cụ dọn dẹp soạn sẵn ở cuối - đóng câu lệnh và kết nối.

Ý tưởng với thực thi là tốt hơn nếu bạn có thể tìm ra mã soạn sẵn. Điều đó giúp bạn tiết kiệm một số gõ, nhưng lý do sâu hơn. Đây là nguyên tắc không lặp lại (DRY) ở đây - bạn tách mã thành một vị trí để nếu có lỗi hoặc bạn cần thay đổi hoặc bạn chỉ muốn hiểu nó, tất cả chỉ ở một nơi.

Mặc dù vậy, điều hơi rắc rối với loại bao thanh toán này là bạn có các tài liệu tham khảo mà cả hai phần "trước" và "sau" cần phải xem. Trong ví dụ JDBC, điều này sẽ bao gồm Tuyên bố kết nối và (đã chuẩn bị). Vì vậy, để xử lý về cơ bản bạn "bọc" mã mục tiêu của bạn bằng mã soạn sẵn.

Bạn có thể quen với một số trường hợp phổ biến trong Java. Một là bộ lọc servlet. Một cách khác là AOP xung quanh lời khuyên. Thứ ba là các lớp xxxTemplate khác nhau trong Spring. Trong mỗi trường hợp, bạn có một số đối tượng trình bao bọc mà mã "thú vị" của bạn (giả sử truy vấn JDBC và xử lý tập kết quả) được đưa vào. Đối tượng trình bao bọc thực hiện phần "trước", gọi mã thú vị và sau đó thực hiện phần "sau".


7

Xem thêm Code Sandwiches , khảo sát cấu trúc này trên nhiều ngôn ngữ lập trình và đưa ra một số ý tưởng nghiên cứu thú vị. Liên quan đến câu hỏi cụ thể tại sao người ta có thể sử dụng nó, bài viết trên cung cấp một số ví dụ cụ thể:

Những tình huống như vậy phát sinh bất cứ khi nào một chương trình thao túng tài nguyên được chia sẻ. API cho khóa, ổ cắm, tệp hoặc kết nối cơ sở dữ liệu có thể yêu cầu chương trình đóng hoặc giải phóng một cách rõ ràng tài nguyên mà nó đã mua trước đó. Trong một ngôn ngữ không có bộ sưu tập rác, lập trình viên có trách nhiệm phân bổ bộ nhớ trước khi sử dụng và giải phóng nó sau khi sử dụng. Nói chung, một loạt các tác vụ lập trình yêu cầu một chương trình thực hiện thay đổi, vận hành trong bối cảnh của sự thay đổi đó và sau đó hoàn tác thay đổi. Chúng tôi gọi những tình huống như vậy là bánh mì kẹp.

Và sau đó:

Bánh sandwich mã xuất hiện trong nhiều tình huống lập trình. Một số ví dụ phổ biến liên quan đến việc mua lại và giải phóng các tài nguyên khan hiếm, chẳng hạn như khóa, mô tả tệp hoặc kết nối ổ cắm. Trong các trường hợp tổng quát hơn, bất kỳ thay đổi tạm thời nào về trạng thái chương trình có thể yêu cầu mã sandwich. Ví dụ: chương trình dựa trên GUI có thể tạm thời bỏ qua đầu vào của người dùng hoặc nhân hệ điều hành có thể tạm thời vô hiệu hóa các ngắt phần cứng. Không khôi phục trạng thái sớm hơn trong những trường hợp này sẽ gây ra lỗi nghiêm trọng.

Bài viết không khám phá lý do tại sao không sử dụng thành ngữ này, nhưng nó mô tả lý do tại sao thành ngữ này dễ bị sai nếu không có trợ giúp ở cấp độ ngôn ngữ:

Bánh sandwich mã bị lỗi phát sinh thường xuyên nhất với sự có mặt của các ngoại lệ và dòng kiểm soát vô hình liên quan của chúng. Thật vậy, các tính năng ngôn ngữ đặc biệt để quản lý bánh sandwich mã phát sinh chủ yếu trong các ngôn ngữ hỗ trợ các ngoại lệ.

Tuy nhiên, ngoại lệ không phải là nguyên nhân duy nhất của bánh sandwich mã bị lỗi. Bất cứ khi nào thay đổi được thực hiện đối với mã cơ thể , các đường dẫn điều khiển mới có thể phát sinh bỏ qua mã sau . Trong trường hợp đơn giản nhất, người bảo trì chỉ cần thêm một returntuyên bố vào cơ thể của bánh sandwich để đưa ra một khiếm khuyết mới, điều này có thể dẫn đến các lỗi im lặng. Khi mã cơ thể lớn và trướcsau được phân tách rộng rãi, những lỗi như vậy có thể khó phát hiện bằng mắt.


Điểm tốt, azurefrag. Tôi đã sửa đổi và mở rộng câu trả lời của mình để nó thực sự giống như một câu trả lời độc lập theo đúng nghĩa của nó. Cảm ơn đã gợi ý điều này.
Ben Liblit

4

Tôi sẽ cố gắng giải thích, như tôi sẽ làm với một đứa trẻ bốn tuổi:

ví dụ 1

Santa đến thị trấn. Yêu tinh của anh ta mã hóa bất cứ điều gì họ muốn sau lưng, và trừ khi họ thay đổi mọi thứ sẽ có một chút lặp đi lặp lại:

  1. Nhận giấy gói
  2. Nhận Super Nintendo .
  3. Bọc nó.

Hoặc này:

  1. Nhận giấy gói
  2. Nhận búp bê Barbie .
  3. Bọc nó.

.... quảng cáo một triệu lần với một triệu món quà khác nhau: lưu ý rằng điều duy nhất khác biệt là bước 2. Nếu bước hai là điều duy nhất khác nhau, thì tại sao ông già Noel lại nhân đôi mã, tức là tại sao ông lại sao chép các bước 1 và 3 triệu lần? Một triệu món quà có nghĩa là anh ta không cần lặp lại các bước 1 và 3 một triệu lần.

Thực thi xung quanh giúp giải quyết vấn đề đó. và giúp loại bỏ mã. Bước 1 và 3 về cơ bản là không đổi, cho phép bước 2 là phần duy nhất thay đổi.

Ví dụ # 2

Nếu bạn vẫn không hiểu, đây là một ví dụ khác: hãy nghĩ về một chiếc sandwhich: bánh mì ở bên ngoài luôn giống nhau, nhưng những gì bên trong thay đổi tùy thuộc vào loại sandwhich bạn chọn (.eg ham, phô mai, mứt, bơ đậu phộng v.v.). Bánh mì luôn ở bên ngoài và bạn không cần phải lặp lại một tỷ lần cho mỗi loại cát bạn đang tạo.

Bây giờ nếu bạn đọc những lời giải thích ở trên, có lẽ bạn sẽ thấy dễ hiểu hơn. Tôi hy vọng lời giải thích này đã giúp bạn.


+ cho trí tưởng tượng: D
Thưa ngài. Nhím

3

Điều này nhắc nhở tôi về các mẫu thiết kế chiến lược . Lưu ý rằng liên kết mà tôi đã chỉ ra bao gồm mã Java cho mẫu.

Rõ ràng người ta có thể thực hiện "Thực thi xung quanh" bằng cách tạo mã khởi tạo và dọn dẹp và chỉ cần chuyển qua một chiến lược, sau đó sẽ luôn được bọc trong mã khởi tạo và mã dọn dẹp.

Như với bất kỳ kỹ thuật nào được sử dụng để giảm sự lặp lại mã, bạn không nên sử dụng nó cho đến khi bạn có ít nhất 2 trường hợp bạn cần, thậm chí là 3 (theo nguyên tắc YAGNI). Hãy nhớ rằng việc lặp lại mã loại bỏ làm giảm bảo trì (ít bản sao mã hơn có nghĩa là mất ít thời gian hơn để sửa lỗi trên mỗi bản sao), nhưng cũng tăng bảo trì (tổng số mã nhiều hơn). Vì vậy, chi phí của thủ thuật này là bạn đang thêm nhiều mã hơn.

Loại kỹ thuật này hữu ích cho nhiều thứ hơn là chỉ khởi tạo và dọn dẹp. Nó cũng tốt khi bạn muốn gọi các chức năng của mình dễ dàng hơn (ví dụ: bạn có thể sử dụng nó trong trình hướng dẫn để các nút "tiếp theo" và "trước đó" không cần các câu lệnh trường hợp khổng lồ để quyết định làm gì trang tiếp theo / trước.


0

Nếu bạn muốn thành ngữ Groovy, đây là:

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

Nếu mở của tôi không thành công (giả sử có được khóa reentrant), đóng sẽ được gọi (nói là giải phóng khóa reentrant mặc dù lỗi mở không khớp).
Tom Hawtin - tackline
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.