Buộc các nhà phát triển khác gọi phương thức sau khi hoàn thành công việc của họ


34

Trong một thư viện trong Java 7, tôi có một lớp cung cấp dịch vụ cho các lớp khác. Sau khi tạo một thể hiện của lớp dịch vụ này, một phương thức của nó có thể được gọi nhiều lần (hãy gọi nó là doWork()phương thức). Vì vậy, tôi không biết khi nào công việc của lớp dịch vụ được hoàn thành.

Vấn đề là lớp dịch vụ sử dụng các đối tượng nặng và nó sẽ giải phóng chúng. Tôi đặt phần này trong một phương thức (hãy gọi nó release()), nhưng không đảm bảo rằng các nhà phát triển khác sẽ sử dụng phương pháp này.

Có cách nào để buộc các nhà phát triển khác gọi phương thức này sau khi hoàn thành nhiệm vụ của lớp dịch vụ không? Tất nhiên tôi có thể làm tài liệu đó, nhưng tôi muốn ép buộc họ.

Lưu ý: Tôi không thể gọi release()phương thức trong doWork()phương thức, vì doWork() cần các đối tượng đó khi nó được gọi tiếp theo.


46
Những gì bạn đang làm có một hình thức khớp nối tạm thời và thường được coi là mùi mã. Bạn có thể muốn buộc bản thân phải đưa ra một thiết kế tốt hơn thay thế.
Frank

1
@Frank Tôi đồng ý, nếu thay đổi thiết kế của lớp bên dưới là một tùy chọn thì đó sẽ là lựa chọn tốt nhất. Nhưng tôi nghi ngờ đây là một gói bao quanh dịch vụ của người khác.
Dar Brett

Những loại đối tượng nặng mà Dịch vụ của bạn cần? Nếu lớp Dịch vụ không thể tự quản lý các tài nguyên đó một cách tự động (mà không yêu cầu khớp nối tạm thời) thì có lẽ các tài nguyên đó nên được quản lý ở nơi khác và được đưa vào Dịch vụ. Bạn đã viết bài kiểm tra đơn vị cho lớp Dịch vụ chưa?
ĐẾN TỪ

18
Nó rất "un-Java" để yêu cầu điều đó. Java là một ngôn ngữ với bộ sưu tập rác và bây giờ bạn nghĩ ra một số loại mâu thuẫn trong đó bạn yêu cầu các nhà phát triển "dọn dẹp".
Pieter B

22
@PieterB tại sao? AutoCloseableđã được thực hiện cho mục đích chính xác này.
BgrWorker

Câu trả lời:


48

Giải pháp thực tế là tạo ra lớp AutoCloseablevà cung cấp một finalize()phương thức làm điểm dừng (nếu thích hợp ... xem bên dưới!). Sau đó, bạn dựa vào người dùng của lớp để sử dụng tài nguyên thử hoặc gọi close()một cách rõ ràng.

Tất nhiên tôi có thể làm tài liệu đó, nhưng tôi muốn ép buộc họ.

Thật không may, không có cách nào 1 trong Java để buộc lập trình viên làm điều đúng đắn. Điều tốt nhất bạn có thể hy vọng làm là chọn cách sử dụng không chính xác trong máy phân tích mã tĩnh.


Về chủ đề quyết toán. Tính năng Java này có rất ít trường hợp sử dụng tốt . Nếu bạn dựa vào người hoàn thiện để dọn dẹp, bạn sẽ gặp phải vấn đề có thể mất một thời gian rất dài để việc dọn dẹp xảy ra. Bộ hoàn thiện sẽ chỉ được chạy sau khi GC quyết định rằng đối tượng không thể truy cập được nữa. Điều đó có thể không xảy ra cho đến khi JVM thực hiện một bộ sưu tập đầy đủ .

Do đó, nếu vấn đề bạn buộc phải giải quyết là lấy lại các tài nguyên cần được giải phóng sớm , thì bạn đã bỏ lỡ chiếc thuyền bằng cách sử dụng quyết toán.

Và chỉ trong trường hợp bạn không có những gì tôi đang nói ở trên ... gần như không bao giờ thích hợp để sử dụng bộ hoàn thiện trong mã sản xuất, và bạn không bao giờ nên dựa vào chúng!


1 - Có nhiều cách. Nếu bạn chuẩn bị "ẩn" các đối tượng dịch vụ khỏi mã người dùng hoặc kiểm soát chặt chẽ vòng đời của họ (ví dụ: https://softwareengineering.stackexchange.com/a/345100/172 ) thì mã người dùng không cần phải gọi release(). Tuy nhiên, API trở nên phức tạp hơn, bị hạn chế ... và IMO "xấu xí". Ngoài ra, điều này không buộc các lập trình viên phải làm điều đúng đắn . Nó đang loại bỏ khả năng của lập trình viên để làm điều sai!


1
Người hoàn thiện dạy một bài học rất tệ - nếu bạn làm hỏng nó, tôi sẽ sửa nó cho bạn. Tôi thích cách tiếp cận của netty -> một tham số cấu hình hướng dẫn nhà máy (ByteBuffer) tạo ngẫu nhiên với xác suất X% bytebuffers với bộ hoàn thiện in vị trí nơi bộ đệm này được mua (nếu chưa được phát hành). Vì vậy, trong sản xuất, giá trị này có thể được đặt thành 0% - tức là không có chi phí hoạt động và trong quá trình thử nghiệm & dev thành giá trị cao hơn như 50% hoặc thậm chí 100% để phát hiện rò rỉ trước khi phát hành sản phẩm
Svetlin Zarev

11
và hãy nhớ rằng các công cụ hoàn thiện không được đảm bảo để chạy, ngay cả khi đối tượng đang được thu gom rác!
jwenting

3
Điều đáng chú ý là Eclipse phát ra cảnh báo trình biên dịch nếu AutoClosizable không bị đóng. Tôi cũng mong Java IDE khác cũng làm như vậy.
meriton - đình công

1
@jwenting Trong trường hợp nào họ không chạy khi đối tượng là GC'd? Tôi không thể tìm thấy bất kỳ tài nguyên nêu hoặc giải thích điều đó.
Cedric Reichenbach

3
@Voo Bạn hiểu nhầm ý kiến ​​của tôi. Bạn có hai triển khai -> DefaultResourceFinalizableResourcemở rộng cái đầu tiên và thêm phần hoàn thiện. Trong sản xuất, bạn sử dụng DefaultResourcemà không có bộ hoàn thiện-> không có chi phí. Trong quá trình phát triển và thử nghiệm, bạn định cấu hình ứng dụng để sử dụng FinalizableResourcecó bộ hoàn thiện và do đó thêm chi phí. Trong quá trình phát triển và kiểm tra đó không phải là vấn đề, nhưng nó có thể giúp bạn xác định rò rỉ và khắc phục chúng trước khi đưa vào sản xuất. Tuy nhiên, nếu rò rỉ đến PRD, bạn có thể định cấu hình nhà máy để tạo X% FinalizableResourceđể xử lý rò rỉ
Svetlin Zarev

55

Điều này có vẻ giống như trường hợp sử dụng cho AutoCloseablegiao diệntry-with-resourcescâu lệnh được giới thiệu trong Java 7. Nếu bạn có một cái gì đó như

public class MyService implements AutoCloseable {
    public void doWork() {
        // ...
    }

    @Override
    public void close() {
        // release resources
    }
}

sau đó người tiêu dùng của bạn có thể sử dụng nó như

public class MyConsumer {
    public void foo() {
        try (MyService myService = new MyService()) {
            //...
            myService.doWork()
            //...
            myService.doWork()
            //...
        }
    }
}

và không phải lo lắng về việc gọi một cách rõ ràng releasebất kể mã của họ có ném ngoại lệ hay không.


Câu trả lời bổ sung

Nếu những gì bạn thực sự tìm kiếm là để đảm bảo rằng không ai có thể quên sử dụng chức năng của bạn, bạn phải làm một vài điều

  1. Đảm bảo tất cả các nhà xây dựng MyServicelà riêng tư.
  2. Xác định một điểm duy nhất để sử dụng MyServiceđể đảm bảo nó được dọn sạch sau.

Bạn sẽ làm một cái gì đó như

public interface MyServiceConsumer {
    void accept(MyService value);
}

public class MyService implements AutoCloseable {
    private MyService() {
    }


    public static void useMyService(MyServiceConsumer consumer) {
        try (MyService myService = new MyService()) {
            consumer.accept(myService)
        }
    }
}

Sau đó, bạn sẽ sử dụng nó như là

public class MyConsumer {
    public void foo() {
        MyService.useMyService(new MyServiceConsumer() {
            public void accept(MyService myService) {
                myService.doWork()
            }
        });
    }
}

Tôi đã không đề xuất đường dẫn này ban đầu vì nó gớm ghiếc nếu không có lambdas Java-8 và các giao diện chức năng (nơi MyServiceConsumertrở thành Consumer<MyService>) và nó khá áp đặt đối với người tiêu dùng của bạn. Nhưng nếu những gì bạn chỉ muốn là release()phải được gọi, nó sẽ hoạt động.


1
Câu trả lời này có lẽ tốt hơn của tôi. Cách của tôi có một sự chậm trễ trước khi Bộ sưu tập rác bắt đầu.
Dar Brett

1
Làm thế nào để bạn đảm bảo rằng khách hàng sẽ sử dụng try-with-resourcesvà không bỏ trysót closecuộc gọi , do đó bỏ lỡ cuộc gọi?
Frank

@walpen Cách này có cùng một vấn đề. Làm thế nào tôi có thể buộc người dùng sử dụng try-with-resources?
hasanghaforian

1
@Frank / @hasanghaforian, Vâng, cách này không bắt buộc phải gọi. Nó có lợi ích của sự quen thuộc: Các nhà phát triển Java biết bạn thực sự nên đảm bảo .close()được gọi khi lớp thực hiện AutoCloseable.
walpen

1
Bạn không thể ghép một bộ hoàn thiện với một usingkhối để hoàn thiện xác định? Ít nhất là trong C #, điều này trở thành một mô hình khá rõ ràng để các nhà phát triển có thói quen sử dụng khi tận dụng các tài nguyên cần quyết toán xác định. Một cái gì đó dễ dàng và hình thành thói quen là điều tốt nhất tiếp theo khi bạn không thể ép buộc ai đó làm điều gì đó. stackoverflow.com/a/2016311/84206
AaronLS

52

Vâng bạn có thể

Bạn hoàn toàn có thể buộc họ phát hành, và làm cho nó không thể quên 100%, bằng cách khiến họ không thể tạo ra các Servicethể hiện mới .

Nếu họ đã làm một cái gì đó như thế này trước đây:

Service s = s.new();
try {
  s.doWork("something");
  if (...) s.doWork("something else");
  ...
}
finally {
  s.release(); // oops: easy to forget
}

Sau đó thay đổi nó để họ phải truy cập nó như thế này.

// Their code

Service.runWithRelease(new ServiceConsumer() {
  void run(Service s) {
    s.doWork("something");
    if (...) s.doWork("something else");
    ...
  }  // yay! no s.release() required.
}

// Your code

interface ServiceConsumer {
  void run(Service s);
}

class Service {

   private Service() { ... }      // now: private
   private void release() { ... } // now: private
   public void doWork() { ... }   // unchanged

   public static void runWithRelease(ServiceConsumer consumer) {
      Service s = new Service();
      try {
        consumer.run(s);
      }
      finally {
        s.release();
      } 
    } 
  }

Hãy cẩn thận:

  • Hãy xem xét mã giả này, nó đã có từ rất lâu kể từ khi tôi viết Java.
  • Có thể có nhiều biến thể thanh lịch hơn trong những ngày này, có thể bao gồm AutoCloseablegiao diện mà ai đó đã đề cập; nhưng ví dụ đã cho sẽ hoạt động tốt, và ngoại trừ sự thanh lịch (bản thân nó là một mục tiêu tốt đẹp) không nên có lý do chính để thay đổi nó. Lưu ý rằng điều này dự định nói rằng bạn có thể sử dụng AutoCloseablebên trong triển khai riêng tư của mình; lợi ích của giải pháp của tôi đối với việc người dùng của bạn sử dụng AutoCloseablelà nó có thể, một lần nữa, không bị lãng quên.
  • Bạn sẽ phải bổ sung nó khi cần thiết, ví dụ để đưa các đối số vào new Servicecuộc gọi.
  • Như đã đề cập trong các ý kiến, câu hỏi liệu một cấu trúc như thế này (nghĩa là thực hiện quá trình tạo và hủy a Service) có thuộc về người gọi hay không nằm ngoài phạm vi của câu trả lời này. Nhưng nếu bạn quyết định rằng bạn hoàn toàn cần điều này, đây là cách bạn có thể làm nó.
  • Một người bình luận đã cung cấp thông tin này về việc xử lý ngoại lệ: Google Sách

2
Tôi hài lòng về các bình luận cho downvote, vì vậy tôi có thể cải thiện câu trả lời.
AnoE

2
Tôi đã không downvote, nhưng thiết kế này có thể sẽ gặp nhiều rắc rối hơn giá trị của nó. Nó thêm rất nhiều phức tạp và đặt một gánh nặng lớn cho người gọi. Các nhà phát triển nên đủ quen thuộc với các lớp kiểu thử tài nguyên, sử dụng chúng là bản chất thứ hai bất cứ khi nào một lớp thực hiện giao diện thích hợp, do đó, điều đó thường đủ tốt.
jpmc26

30
@ jpmc26, vui thay, tôi cảm thấy rằng cách tiếp cận đó làm giảm sự phức tạp và gánh nặng cho người gọi bằng cách cứu họ khỏi suy nghĩ về vòng đời tài nguyên. Bên cạnh đó; OP không hỏi "tôi có nên ép buộc người dùng ..." mà là "làm thế nào để tôi ép buộc người dùng". Và nếu nó đủ quan trọng, thì đây là một giải pháp hoạt động rất tốt, có cấu trúc đơn giản (chỉ cần gói nó trong một bao đóng / có thể chạy được), thậm chí có thể chuyển sang các ngôn ngữ khác có lambdas / procs / đóng. Chà, tôi sẽ để nó cho nền dân chủ SE để phán xét; OP đã quyết định dù sao đi nữa. ;)
AnoE

14
Một lần nữa, @ jpmc26, tôi không quan tâm lắm đến việc thảo luận liệu phương pháp này có đúng với bất kỳ trường hợp sử dụng nào không. OP hỏi làm thế nào để thực thi một điều như vậy và câu trả lời này cho biết làm thế nào để thực thi một điều như vậy . Nó không phải là để chúng tôi quyết định liệu nó có phù hợp để làm như vậy ngay từ đầu không. Kỹ thuật được hiển thị là một công cụ, không hơn, không kém, và hữu ích để có trong các túi công cụ, tôi tin.
AnoE

11
Đây là câu trả lời đúng imho, về cơ bản nó là mô hình lỗ giữa
jk.

11

Bạn có thể thử sử dụng mẫu Command .

class MyServiceManager {

    public void execute(MyServiceTask tasks...) {
        // set up everyting
        MyService service = this.acquireService();
        // execute the submitted tasks
        foreach (Task task : tasks)
            task.executeWith(service);
        // cleanup yourself
        service.releaseResources();
    }

}

Điều này cho phép bạn kiểm soát hoàn toàn việc thu thập và phát hành tài nguyên. Người gọi chỉ gửi các tác vụ đến Dịch vụ của bạn và chính bạn chịu trách nhiệm thu nhận và dọn dẹp tài nguyên.

Có một nhược điểm, tuy nhiên. Người gọi vẫn có thể làm điều này:

MyServiceTask t1 = // some task
manager.execute(t1);
MyServiceTask t2 = // some task
manager.execute(t2);

Nhưng bạn có thể giải quyết vấn đề này khi nó xảy ra. Khi có vấn đề về hiệu suất và bạn phát hiện ra rằng một số người gọi thực hiện việc này, chỉ cần chỉ cho họ cách thức phù hợp và giải quyết vấn đề:

MyServiceTask t1 = // some task
MyServiceTask t2 = // some task
manager.execute(t1, t2);

Bạn có thể làm cho điều này trở nên phức tạp một cách tùy tiện bằng cách thực hiện các lời hứa cho các nhiệm vụ phụ thuộc vào các nhiệm vụ khác, nhưng sau đó việc phát hành nội dung cũng trở nên phức tạp hơn. Đây chỉ là một điểm khởi đầu.

Không đồng bộ

Như đã được chỉ ra trong các ý kiến, ở trên không thực sự hoạt động với các yêu cầu không đồng bộ. Đúng rồi. Nhưng điều này có thể dễ dàng được giải quyết trong Java 8 bằng cách sử dụng CompleteableFuture , đặc biệt là CompleteableFuture#supplyAsyncđể tạo ra các tương lai riêng lẻ và CompleteableFuture#allOfđể hoàn thiện việc giải phóng các tài nguyên khi tất cả các nhiệm vụ đã hoàn thành. Ngoài ra, người ta luôn có thể sử dụng Chủ đề hoặc ExecutorService để triển khai triển khai Tương lai / Lời hứa của riêng họ.


1
Tôi sẽ đánh giá cao nếu những người downvot để lại bình luận tại sao họ nghĩ rằng điều này là xấu, để tôi có thể trực tiếp giải quyết những mối quan tâm đó hoặc ít nhất là có được quan điểm khác cho tương lai. Cảm ơn!
Polygnome

+1 upvote. Đây có thể là một cách tiếp cận, nhưng dịch vụ không đồng bộ và người tiêu dùng cần có kết quả đầu tiên trước khi yêu cầu dịch vụ tiếp theo.
hasanghaforian

1
Đây chỉ là một mẫu barebones. JS giải quyết điều này bằng các lời hứa, bạn có thể thực hiện những điều tương tự trong Java với tương lai và một trình thu thập được gọi khi tất cả các nhiệm vụ kết thúc và sau đó dọn sạch. Nó chắc chắn có thể có được async và thậm chí phụ thuộc trong đó, nhưng nó cũng làm phức tạp mọi thứ rất nhiều.
Polygnome 28/03/2017

3

Nếu bạn có thể, không cho phép người dùng tạo tài nguyên. Để người dùng chuyển đối tượng khách truy cập vào phương thức của bạn, phương thức này sẽ tạo tài nguyên, chuyển nó sang phương thức của đối tượng khách truy cập và phát hành sau đó.


Cảm ơn bạn đã trả lời, nhưng xin lỗi, bạn có nghĩa là tốt hơn là chuyển tài nguyên cho người tiêu dùng và sau đó nhận lại khi anh ta yêu cầu dịch vụ? Sau đó, vấn đề sẽ vẫn còn: Người tiêu dùng có thể quên phát hành các tài nguyên đó.
hasanghaforian

Nếu bạn muốn kiểm soát tài nguyên, đừng buông bỏ nó, đừng chuyển hoàn toàn nó cho luồng thực thi của người khác. Một cách để làm điều đó là chạy một phương thức được xác định trước của đối tượng khách truy cập của người dùng. AutoCloseablelàm một điều rất giống đằng sau hậu trường. Dưới một góc độ khác, nó tương tự như tiêm phụ thuộc .
9000

1

Ý tưởng của tôi tương tự như của Polygnome.

Bạn có thể có các phương thức "do_s Something ()" trong lớp của bạn chỉ cần thêm các lệnh vào danh sách lệnh riêng. Sau đó, có một phương thức "commit ()" thực sự hoạt động và gọi "release ()".

Vì vậy, nếu người dùng không bao giờ gọi commit (), công việc không bao giờ được thực hiện. Nếu họ làm, các tài nguyên được giải phóng.

public class Foo
{
  private ArrayList<ICommand> _commands;

  public Foo doSomething(int arg) { _commands.Add(new DoSomethingCommand(arg)); return this; }
  public Foo doSomethingElse() { _commands.Add(new DoSomethingElseCommand()); return this; }

  public void commit() { 
     for(ICommand c : _commands) c.doWork();
     release();
     _commands.clear();
  }

}

Sau đó, bạn có thể sử dụng nó như foo.doSthing.doSomethineElse (). Commit ();


Đây là giải pháp tương tự, nhưng thay vì yêu cầu người gọi duy trì danh sách nhiệm vụ bạn duy trì nó trong nội bộ. Khái niệm cốt lõi là theo nghĩa đen. Nhưng điều này không tuân theo bất kỳ quy ước mã hóa nào cho Java mà tôi từng thấy và thực sự khá khó đọc (thân vòng lặp trên cùng dòng với tiêu đề vòng lặp, _ ở đầu).
Polygnome

Vâng, đó là mẫu lệnh. Tôi chỉ cần đặt một số mã để đưa ra ý chính về những gì tôi đã suy nghĩ một cách ngắn gọn nhất có thể. Tôi chủ yếu viết C # những ngày này, trong đó các biến riêng tư thường có tiền tố là _.
Sava B.

-1

Một cách khác để thay thế những thứ được đề cập sẽ là việc sử dụng "tài liệu tham khảo thông minh" để quản lý tài nguyên được đóng gói của bạn theo cách cho phép trình thu gom rác tự động dọn dẹp mà không cần giải phóng tài nguyên theo cách thủ công. Tất nhiên tính hữu dụng của chúng phụ thuộc vào việc thực hiện của bạn. Đọc thêm về chủ đề này trong bài đăng blog tuyệt vời này: https://community.oracle.com/bloss/enicholas/2006/05/04/under Hiểu-weak-report


Đây là một ý tưởng thú vị, nhưng tôi không thể thấy việc sử dụng các tài liệu tham khảo yếu giải quyết vấn đề của OP như thế nào. Lớp dịch vụ không biết liệu nó có nhận được yêu cầu doWork () khác hay không. Nếu nó giải phóng tham chiếu đến các đối tượng nặng của nó, và sau đó nó nhận được một lệnh gọi doWork () khác, nó sẽ thất bại.
Jay Elston

-4

Đối với bất kỳ ngôn ngữ Định hướng Lớp nào khác tôi muốn nói hãy đặt nó vào hàm hủy, nhưng vì đó không phải là một tùy chọn nên tôi nghĩ bạn có thể ghi đè phương thức hoàn thiện. Theo cách đó, khi cá thể đi ra khỏi phạm vi và là rác được thu thập, nó sẽ được phát hành.

@Override
public void finalize() {
    this.release();
}

Tôi không quá quen thuộc với Java, nhưng tôi nghĩ rằng đây là mục đích để hoàn thiện ().

http://docs.oracle.com/javase/7/docs/api/java/lang/Object.html#finalize ()


7
Đây là một giải pháp tồi cho vấn đề vì lý do Stephen nói trong câu trả lời của mình -If you rely on finalizers to tidy up, you run into the problem that it can take a very long time for the tidy-up to happen. The finalizer will only be run after the GC decides that the object is no longer reachable. That may not happen until the JVM does a full collection.
Daenyth 28/03/2017

4
Và thậm chí sau đó, trình hoàn thiện trên thực tế có thể không chạy ...
jwenting

3
Người hoàn thiện nổi tiếng là không đáng tin cậy: họ có thể chạy, họ có thể không bao giờ chạy, nếu họ chạy thì không có gì đảm bảo khi nào họ sẽ chạy. Ngay cả các kiến ​​trúc sư Java như Josh Bloch nói rằng các công cụ hoàn thiện là một ý tưởng tồi tệ (tham khảo: Java hiệu quả (Phiên bản 2) ).

Những loại vấn đề này khiến tôi nhớ C ++ với sự phá hủy mang tính quyết định và RAII ...
Mark K Cowan
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.