2011-08-31 25 views
21

Tôi muốn một số lời khuyên về một kỹ thuật tôi va vào. Nó có thể dễ dàng được hiểu bằng cách nhìn vào các đoạn mã, nhưng tôi ghi lại nó một chút trong các đoạn sau đây.lời khuyên về lồng nhau Java cố gắng/cuối cùng mã bánh sandwich


Sử dụng thành ngữ "Mã Sandwich" là phổ biến để đối phó với quản lý tài nguyên. Được sử dụng cho thành ngữ RAII của C++, tôi chuyển sang Java và thấy quản lý tài nguyên ngoại lệ an toàn của mình dẫn đến mã lồng nhau sâu sắc, trong đó tôi có một thời gian thực sự khó khăn trong việc kiểm soát luồng thông thường thông thường.

Rõ ràng (java data access: is this good style of java data access code, or is it too much try finally?, Java io ugly try-finally block và nhiều thứ khác) Tôi không đơn độc.

tôi đã cố gắng giải pháp khác nhau để đối phó với điều này:

  1. duy trì trạng thái chương trình một cách rõ ràng: resource1aquired, fileopened ..., và dọn dẹp có điều kiện: if (resource1acquired) resource1.cleanup() ... Nhưng tôi Shun sao chép chương trình nhà nước trong rõ ràng biến - thời gian chạy biết trạng thái và tôi không muốn quan tâm đến nó.

  2. quấn mỗi khối lồng nhau trong chức năng - kết quả trong thậm chí khó khăn hơn để theo dòng điều khiển, và làm cho tên hàm thực sự lúng túng: runResource1Acquired(r1), runFileOpened(r1, file) ...

Và cuối cùng tôi đến một thành ngữ cũng (theo lý thuyết) được hỗ trợ bởi một số research paper on code sandwiches:


Thay vì điều này:

// (pseudocode) 
try { 
    connection = DBusConnection.SessionBus(); // may throw, needs cleanup 
    try { 
     exported = false; 
     connection.export("/MyObject", myObject); // may throw, needs cleanup 
     exported = true; 
      //... more try{}finally{} nested blocks 
    } finally { 
     if(exported) connection.unExport("/MyObject"); 
    } 
} finally { 
    if (connection != null) connection.disconnect(); 
} 

Sử dụng công cụ trợ giúp, bạn có thể đến một cấu trúc tuyến tính hơn, trong đó mã bù trừ nằm ngay bên cạnh người khởi tạo.

class Compensation { 
    public void compensate(){}; 
} 
compensations = new Stack<Compensation>(); 

Và mã lồng nhau trở thành tuyến tính:

try { 
    connection = DBusConnection.SessionBus(); // may throw, needs cleanup 
    compensations.push(new Compensation(){ public void compensate() { 
     connection.disconnect(); 
    }); 

    connection.export("/MyObject", myObject); // may throw, needs cleanup 
    compensations.push(new Compensation(){ public void compensate() { 
     connection.unExport("/MyObject"); 
    }); 

    // unfolded try{}finally{} code 

} finally { 
    while(!compensations.empty()) 
     compensations.pop().compensate(); 
} 

Tôi rất vui mừng: dù có bao nhiêu con đường đặc biệt, các dòng điều khiển vẫn tuyến tính, và các mã ngẫu nhiên là trực tiếp vào mã nguồn gốc. Trên hết, nó không cần phương thức closeQuietly bị hạn chế giả tạo, khiến cho nó linh hoạt hơn (tức là không chỉ đối tượng Closeable, mà còn là Disconnectable, Rollbackable và bất kỳ thứ gì khác).

Nhưng ...

Tôi không đề cập đến kỹ thuật này ở nơi khác. Vì vậy, đây là câu hỏi:


Kỹ thuật này có hợp lệ không? Bạn thấy gì trong đó?

Cảm ơn rất nhiều.

+1

Bạn dường như không được xử lý trường hợp một ngoại lệ có thể được ném từ bên trong một phương thức bù(). Điều này sẽ ngăn chặn việc đền bù tiếp theo khi chạy. –

+0

@Kevin: thực sự - quá nhiều thành phần C++ ở đây: destructors không được ném, và tôi dính vào thành ngữ đó ở đây. Nó phụ thuộc vào việc thực thi 'bù đắp' để xử lý các ngoại lệ. – xtofl

+1

@xtofl - điều đó có vẻ hơi mâu thuẫn với bạn bằng cách sử dụng 'try-finally' ở tất cả - bạn chỉ có thể chấp nhận thành ngữ đó với chính mã đó, và không bận tâm đến một khối' cuối cùng'. Bên cạnh đó, nhiều 'Compensators' tự nhiên sẽ ném ngoại lệ (ví dụ:' SQLException' để đóng tài nguyên DB); mỗi lần thực hiện ** có ** để bắt và nuốt tất cả các ngoại lệ riêng lẻ, thay vì khối 'cuối cùng' xử lý nó một cách nhất quán ở một nơi. Nó không phải là tùy chọn cho họ để làm điều này (một ngoại lệ thời gian chạy sẽ vi phạm đặc điểm kỹ thuật của họ, và điều đó có thể xảy ra bất cứ lúc nào) vì vậy bạn chỉ cần được khuyến khích sao chép-dán. –

Trả lời

2

Thật tuyệt.

No complaints lớn, những điều nhỏ ra khỏi đỉnh đầu của tôi:

  • một hiệu suất ít gánh nặng
  • bạn cần phải thực hiện một số điều final cho Bồi thường nhìn thấy chúng. Có thể điều này ngăn cản một số trường hợp sử dụng:
  • bạn nên nắm bắt các trường hợp ngoại lệ trong khi bù trừ và tiếp tục chạy Bồi thường bất kể số nào
  • (bit được tìm nạp) bạn vô tình làm trống hàng đợi Bồi thường khi chạy do lỗi lập trình. Lỗi lập trình OTOH có thể xảy ra. Và với hàng đợi Bồi thường của bạn, bạn nhận được "điều kiện cuối cùng là chặn".
  • không đẩy quá xa. Trong một phương pháp duy nhất có vẻ như không sao (nhưng sau đó bạn có thể không cần quá nhiều thử/cuối cùng khối anyway), nhưng không vượt qua hàng đợi bồi thường lên và xuống ngăn xếp cuộc gọi.
  • nó trì hoãn bồi thường cho "ngoài cùng cuối cùng" có thể là một vấn đề cho những thứ cần được làm sạch sớm
  • nó chỉ có ý nghĩa khi bạn cần khối thử chỉ cho khối cuối cùng. Nếu bạn có một khối catch anyway, bạn có thể chỉ cần thêm cuối cùng ngay tại đó.
+0

Cảm ơn. Những gì bạn có ý nghĩa bởi "cuối cùng có điều kiện khối"? – xtofl

+0

Vâng, bạn có thể nói 'if (something) componensation.push (extraCompensation)'. – Thilo

0

Bạn có biết bạn thực sự cần sự phức tạp này không. Điều gì xảy ra khi bạn cố gắng unExport cái gì đó không được xuất khẩu?

// one try finally to rule them all. 
try { 
    connection = DBusConnection.SessionBus(); // may throw, needs cleanup 
    connection.export("/MyObject", myObject); // may throw, needs cleanup 
    // more things which could throw an exception 

} finally { 
    // unwind more things which could have thrown an exception 

    try { connection.unExport("/MyObject"); } catch(Exception ignored) { } 
    if (connection != null) connection.disconnect(); 
} 

Sử dụng phương pháp helper bạn có thể làm

unExport(connection, "/MyObject"); 
disconnect(connection); 

Tôi đã có thể nghĩ ngắt kết nối có nghĩa là bạn không cần phải unExport các nguồn lực kết nối đang sử dụng.

+1

Sau đó, anh ta có thể chạy unExport, mặc dù xuất không chạy. Không phải trong ví dụ đơn giản ở trên, nhưng bạn có được ý tưởng. (Có thể được xử lý bởi rất nhiều booleans). – Thilo

+0

Thật vậy: giải pháp này chỉ hữu ích khi độ phức tạp thêm vào thực sự bổ sung khả năng đọc mã. Có lẽ ví dụ này không đủ đặc biệt :). – xtofl

+0

@Thilo, "Sau đó, anh ấy có thể chạy unExport, mặc dù xuất không chạy." bạn có biết đây là vấn đề không? –

3

Tôi thích cách tiếp cận, nhưng hãy xem một vài giới hạn.

Đầu tiên là trong bản gốc, một cú ném vào khối cuối cùng không ảnh hưởng đến các khối sau này. Trong cuộc biểu tình của bạn, một cú ném vào hành động unexport sẽ dừng việc bù trừ kết nối xảy ra. Thứ hai là ngôn ngữ phức tạp bởi sự xấu xí của các lớp ẩn danh của Java, bao gồm sự cần thiết phải giới thiệu một đống biến 'cuối cùng' để chúng có thể được nhìn thấy bởi các bộ bù. Đây không phải là lỗi của bạn, nhưng tôi tự hỏi liệu việc chữa trị có tệ hơn bệnh không.

Nhưng nhìn chung, tôi thích cách tiếp cận và nó khá dễ thương.

1

Bạn nên xem xét các tài nguyên thử nghiệm, được giới thiệu lần đầu trong Java 7. Điều này sẽ làm giảm sự lồng ghép cần thiết.

+1

Tôi đã làm. Chúng hạn chế các tài nguyên cho những người triển khai 'Closeable', đó là imho quá hạn chế. (Và tôi chưa sử dụng Java 7, nhưng đó là một vấn đề nhỏ :) – xtofl

+0

Bạn có thể có thể gửi một lỗi trong trình theo dõi lỗi của DBus. Imho không có lý do gì mà lớp này không thực hiện Closeable. – soc

3

Cách tôi xem, những gì bạn muốn là giao dịch. Bạn bồi thường là giao dịch, thực hiện một chút khác nhau. Tôi cho rằng bạn không làm việc với các tài nguyên JPA hoặc bất kỳ tài nguyên nào khác hỗ trợ các giao dịch và rollback, bởi vì sau đó nó sẽ khá đơn giản để sử dụng JTA (API giao dịch Java). Ngoài ra, tôi cho rằng tài nguyên của bạn không được phát triển bởi bạn, bởi vì một lần nữa, bạn chỉ có thể yêu cầu họ triển khai các giao diện chính xác từ JTA và sử dụng các giao dịch với họ.

Vì vậy, tôi thích cách tiếp cận của bạn, nhưng những gì tôi sẽ làm là ẩn sự phức tạp của popping và đền bù từ khách hàng. Ngoài ra, bạn có thể chuyển các giao dịch xung quanh một cách minh bạch.

Như vậy (xem ra, mã xấu xí trước):

public class Transaction { 
    private Stack<Compensation> compensations = new Stack<Compensation>(); 

    public Transaction addCompensation(Compensation compensation) { 
     this.compensations.add(compensation); 
    } 

    public void rollback() { 
     while(!compensations.empty()) 
     compensations.pop().compensate(); 
    } 
} 
+0

... mã xấu? Tôi có thể làm xấu hơn :) Đây là ít nhất một cách tốt đẹp để bọc kỹ thuật này vào một lớp có thể tái sử dụng. – xtofl

+1

điều duy nhất tôi bỏ lỡ ở đây là thử-catch gói cho 'compensationations.pop(). Bù();' để xử lý các trường hợp ngoại lệ có thể xảy ra ở đó như được chỉ ra trong vài câu trả lời trước đây – gnat

+1

Vâng, tôi cũng bỏ lỡ getters và setters, một đúng constructor, null tham số kiểm tra vv - khá xấu xí với khẩu vị của tôi. :) Nhưng vâng, gnat, mã để làm rollback nên phức tạp hơn, như kiểm tra các khoản bồi thường không hợp lệ hoặc không có chức năng và tương tự. Những gì bạn muốn làm nếu bồi thường thất bại là một câu chuyện hoàn toàn khác nhau: bạn có tiếp tục hay không? Các khoản bồi thường có phụ thuộc vào nhau không? Ném một ngoại lệ (yêu thích của tôi kể từ khi nhà nước ứng dụng của bạn là không xác định trong trường hợp như vậy)? Yêu cầu của bạn có thể cho bạn biết phải làm gì, vì vậy hãy hạ gục chính mình! :) – LeChe

3

Destructor như thiết bị cho Java, mà sẽ được gọi ở phần cuối của phạm vi từ vựng, là một chủ đề thú vị; nó được giải quyết tốt nhất ở cấp độ ngôn ngữ, tuy nhiên các ngôn ngữ czars không tìm thấy nó rất hấp dẫn.

Chỉ định hành động hủy diệt ngay lập tức sau khi hành động xây dựng đã được thảo luận (không có gì mới dưới ánh mặt trời). Một ví dụ là http://projectlombok.org/features/Cleanup.html

Một ví dụ khác, từ một cuộc thảo luận riêng:

{ 
    FileReader reader = new FileReader(source); 
    finally: reader.close(); // any statement 

    reader.read(); 
} 

này hoạt động bằng cách chuyển

{ 
    A 
    finally: 
     F 
    B 
} 

vào

{ 
    A 
    try 
    { 
     B 
    } 
    finally 
    { 
     F 
    } 
} 

Nếu Java 8 thêm đóng cửa, chúng tôi có thể triển khai tính năng này một cách ngắn gọn khi đóng:

auto_scope 
#{ 
    A; 
    on_exit #{ F; } 
    B; 
} 

Tuy nhiên, với việc đóng cửa, hầu hết các thư viện tài nguyên sẽ cung cấp dịch vụ của riêng thiết bị tự động dọn dẹp của họ, khách hàng không thực sự cần phải xử lý nó tự

File.open(fileName) #{ 

    read... 

}; // auto close 
+0

lombok ... trông giống như một số std :: boost cho java :) Cảm ơn gợi ý! – xtofl

Các vấn đề liên quan