强迫其他开发人员在完成工作后调用方法


34

在Java 7的库中,我有一个为其他类提供服务的类。创建此服务类的实例后,可以多次调用该服务类的一个方法(将其称为doWork()方法)。因此,我不知道服务类的工作何时完成。

问题在于服务类使用重对象,并且应该释放它们。我在方法中设置了这一部分(我们称它为release()),但不能保证其他开发人员将使用此方法。

有没有一种方法可以迫使其他开发人员在完成服务类任务后调用此方法?我当然可以记录下来,但是我想强迫他们。

注意:我无法在release()方法中调用该方法doWork(),因为doWork() 在下一次调用该方法时需要这些对象。


46
您正在执行的操作是一种时间耦合形式,通常被认为是代码异味。您可能需要强迫自己设计出更好的设计。
弗兰克

1
@Frank我同意,如果更改底层的设计是一种选择,那么那将是最好的选择。但是我怀疑这是围绕他人服务的包装。
达尔·布雷特

您的服务需要哪种重物?如果Service类本身不能自动管理那些资源(不需要时间耦合),那么可能应该在其他地方管理那些资源并将其注入到Service中。您是否为Service类编写了单元测试?

18
要求是非常“非Java”的。Java是一种带有垃圾回收的语言,现在您遇到了一些需要开发人员“清理”的问题。
Pieter B

22
@PieterB为什么?AutoCloseable是为了这个确切的目的而制作的。
BgrWorker

Answers:


48

务实的解决方案是制作类AutoCloseable,并提供一个finalize()作为支持的方法(如果合适,请参见下文!)。然后,您依靠班级的用户使用try-with-resourceclose()显式调用。

我当然可以记录下来,但是我想强迫他们。

不幸的是,Java中没有方法1可以迫使程序员做正确的事。您可能希望做的最好的事情就是在静态代码分析器中发现不正确的用法。


关于终结器的话题。此Java功能很少有很好的用例。如果您依靠终结器进行整理,则会遇到以下问题:整理可能需要很长时间。只有在GC决定该对象不再可访问之后,终结器才会运行。在JVM完成完整收集之前,可能不会发生这种情况。

因此,如果您要解决的问题是回收需要尽早释放的资源,那么您将无法使用finalization 来解决问题。

并且以防万一您没有我在上面说的话... 在生产代码中几乎不适合使用终结器,并且您永远不应依赖它们!


1-有办法。如果您准备从用户代码中“隐藏”服务对象或严格控制其生命周期(例如https://softwareengineering.stackexchange.com/a/345100/172),则用户代码不需要调用release()。但是,API变得更加复杂,受限制……而且“丑陋”的IMO。同样,这并不是在强迫程序员做正确的事。它正在消除程序员做错事的能力!


1
终结者会教一个非常糟糕的课程-如果您拧错了,我会为您修复。我更喜欢netty的方法->一个配置参数,该参数指示(ByteBuffer)工厂以X%字节缓冲区的概率随机创建,并带有终结符,该终结符将打印获取该缓冲区的位置(如果尚未释放)。因此,在生产中,此值可以设置为0%(即无开销),并且在测试和开发过程中可以设置为较高的值(如50%甚至100%),以便在发布产品之前检测泄漏
Svetlin Zarev

11
请记住,即使对象实例正在被垃圾回收,终结器也不能保证永远运行!
jwenting

3
值得注意的是,如果未关闭AutoCloseable,则Eclipse会发出编译器警告。我希望其他Java IDE也会这样做。
meriton-罢工

1
@jwenting在什么情况下当对象被GC处理时它们不运行?我找不到任何说明或解释的资源。
Cedric Reichenbach

3
@Voo您误解了我的评论。您有两个实现-> DefaultResourceFinalizableResource并且扩展了第一个实现并添加了终结器。在生产中,您使用的DefaultResource没有终结器->没有开销。在开发和测试期间,您需要配置要使用的应用程序,该应用程序FinalizableResource具有终结器,因此会增加开销。在开发和测试过程中,这不是问题,但可以帮助您识别泄漏并在投入生产之前进行修复。但是,如果泄漏达到PRD,您可以配置工厂以创建X%FinalizableResource来识别泄漏
Svetlin Zarev,2017年

55

这似乎是AutoCloseable接口的用例和Java 7中引入的try-with-resources语句

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

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

然后您的消费者可以将其用作

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

不必担心调用显式方法release,无论其代码是否引发异常。


附加答案

如果您真正要寻找的是确保没有人会忘记使用您的功能,那么您必须做几件事

  1. 确保的所有构造函数MyService都是私有的。
  2. 定义要使用的单个入口点,以MyService确保在此之后对其进行清理。

你会做类似的事情

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

然后,您将其用作

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

我最初不建议使用此路径,因为它没有Java-8 lambda和功能性接口(MyServiceConsumer变为Consumer<MyService>),因此很丑陋,并且确实强加于您的使用者。但是,如果您只想要被release()调用,它将起作用。


1
这个答案可能比我的要好。我的方式在垃圾之前的延迟收藏家踢。
达累斯萨拉姆布雷特

1
您如何确保客户将使用try-with-resources而不只是忽略try,从而错过了close电话?
弗兰克(Frank)

@walpen这种方式有同样的问题。我该如何强迫用户使用try-with-resources
hasanghaforian

1
@Frank / @hasanghaforian,是的,这种方式不会强制接近被调用。它确实带来了熟悉的好处:Java开发人员知道您确实应该确保.close()在类实现时被调用AutoCloseable
Walpen

1
您不能将终结器与using用于确定性终结的块配对吗?至少在C#中,这成为开发人员养成在利用需要确定性终结处理的资源时养成使用习惯的清晰模式。当您不能强迫某人做某事时,容易且容易养成习惯的事情是次之。stackoverflow.com/a/2016311/84206
AaronLS

52

是的你可以

您可以绝对迫使他们释放它们,并通过使它们无法创建新Service实例来使它成为100%不可能忘记的事情。

如果他们之前做过这样的事情:

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

然后更改它,以便他们必须像这样访问它。

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

注意事项:

  • 考虑一下这个伪代码,自从我编写Java以来​​已经存在了很长时间。
  • 这些天可能会有更优雅的变体,也许包括AutoCloseable有人提到的那个界面。但是给定的示例应该是开箱即用的,并且除了优雅(这本身是一个不错的目标)之外,没有任何理由更改它。注意,这是要说明您可以AutoCloseable在私有实现中使用;与让您的用户使用相比,我的解决方案的好处AutoCloseable是,它不会再次被遗忘。
  • 您必须根据需要对其进行充实,例如,将参数注入到new Service调用中。
  • 如评论中所述,类似这样的构造(即,进行创建和销毁a的过程Service)是否属于调用方的问题不在此答案的范围之内。但是,如果您确定绝对需要此功能,这就是您可以做到的方式。
  • 一位评论者提供了有关异常处理的以下信息:Google图书

2
我很高兴收到有关弃权票的评论,因此我可以改善答案。
AnoE

2
我没有投票,但是这种设计可能会带来更多麻烦,而不是值得的。这增加了很多复杂性,并给呼叫者带来了很大的负担。开发人员应该对try-with-resources样式类足够熟悉,每当一个类实现适当的接口时,使用它们就是第二自然,因此通常就足够了。
jpmc26

30
@ jpmc26,有趣的是,我认为这种方法通过使调用者完全不必考虑资源生命周期,从而降低调用者的复杂性和负担。除此之外;OP并未询问“我应该强迫用户...”,而是询问“我如何强迫用户”。而且,如果它足够重要,那么它是一种效果很好的解决方案,其结构简单(只需将其包装在闭包/可运行中),甚至可以转移到也具有lambda / proc / closures的其他语言中。好吧,我将由东南民主来判断;反正OP已经决定了。;)
AnoE

14
同样,@ jpmc26,我对讨论这种方法是否适用于其中的任何单个用例并不感兴趣。OP 询问如何执行此类操作,此答案说明如何执行此类操作。首先,我们不应该决定这样做是否合适。我认为,所示的技术只是一种工具,仅此而已,并且对于将其放在一个工具包中很有用。
AnoE

11
这是正确的答案恕我直言,这是本质上的孔,在最中间的图案
JK。

11

您可以尝试使用命令模式。

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

}

这使您可以完全控制资源的获取和释放。呼叫者仅将任务提交给您的服务,您自己负责获取和清理资源。

但是有一个陷阱。呼叫者仍然可以这样做:

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

但是您可以解决这个问题。如果存在性能问题,而您发现有一些调用者这样做,则只需向他们显示正确的方法并解决问题:

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

您可以通过为依赖于其他任务的任务实现承诺来使此过程变得任意复杂,但是发布内容也变得更加复杂。这仅仅是一个起点。

异步

正如评论中指出的那样,以上内容实际上不适用于异步请求。没错。但这可以通过使用CompleteableFuture在Java 8中轻松解决,尤其是CompleteableFuture#supplyAsync在创建完所有任务之后创建单个的期货并CompleteableFuture#allOf执行资源的释放。或者,可以始终使用Threads或ExecutorServices滚动其自己的Future / Promises实现。


1
如果下降投票者发表评论,为什么他们认为这不好,我将不胜感激,以便我可以直接解决这些问题,或者至少对未来有另一种观点。谢谢!
Polygnome

+1投票。这可能是一种方法,但是服务是异步的,消费者需要在获得下一个服务之前获得第一个结果。
hasanghaforian

1
这只是一个准系统模板。JS通过promise解决了这一问题,您可以在Java中使用future和一个收集器(在完成所有任务然后进行清理时调用该收集器)来执行类似的操作。绝对有可能在其中获取异步甚至依赖关系,但这也使事情变得非常复杂。
Polygnome

3

如果可以,则不允许用户创建资源。让用户将访问者对象传递给您的方法,该方法将创建资源,将其传递给访问者对象的方法,然后释放。


谢谢您的答复,但是对不起,您是说最好将资源传递给消费者,然后在他要求服务时再次获得它吗?然后问题仍然存在:消费者可能会忘记释放这些资源。
hasanghaforian

如果要控制资源,请不要放开它,也不要将其完全传递给其他人的执行流程。一种方法是运行用户的访客对象的预定义方法。AutoCloseable在幕后做一个非常相似的事情。从另一个角度讲,它类似于依赖注入
9000

1

我的想法类似于Polygnome的想法。

您可以在类中使用“ do_something()”方法,只需将命令添加到专用命令列表中即可。然后,使用一个“ commit()”方法实际完成工作,并调用“ release()”。

因此,如果用户从不调用commit(),则该工作永远不会完成。如果这样做,则释放资源。

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

}

然后,您可以像foo.doSomething.doSomethineElse()。commit();一样使用它。


这是相同的解决方案,但是您无需内部维护它,而是要求呼叫者维护任务列表。核心概念实际上是相同的。但这并没有遵循我见过的Java编码惯例,实际上很难阅读(循环体与循环头在同一行,开头是_)。
Polygnome

是的,这是命令模式。我只是放下一些代码,以尽可能简洁的方式给出我的想法。这些天我主要写C#,私有变量通常以_开头。
Sava B.

-1

提到的方法的另一种选择是使用“智能引用”来管理封装的资源,使垃圾收集器能够自动清理而无需手动释放资源。它们的有用性当然取决于您的实现。在此精彩的博客文章中了解有关此主题的更多信息:https : //community.oracle.com/blogs/enicholas/2006/05/04/understanding-weak-references


这是一个有趣的想法,但是我看不到使用弱引用如何解决OP的问题。服务类不知道是否要获取另一个doWork()请求。如果它释放对其重对象的引用,然后又得到另一个doWork()调用,它将失败。
杰伊·埃尔斯顿

-4

对于任何其他面向类的语言,我都会说将其放在析构函数中,但是由于这不是一种选择,因此我认为您可以覆盖finalize方法。这样,当实例超出范围并被垃圾回收时,它将被释放。

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

我不太熟悉Java,但是我认为这是finalize()的目的。

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


7
这是对问题的不好解决方案,原因是斯蒂芬在回答中说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.
丹妮斯17-3-28

4
即使这样,终结器实际上也可能无法运行...
jwenting

3
终结器众所周知是不可靠的:它们可能运行,它们可能永远不会运行,如果运行,则无法保证何时运行。甚至Josh Bloch之类的Java架构师都说,终结器是一个可怕的想法(参考:Effective Java(第二版))。

这些问题使我怀念C ++的确定性破坏和RAII ...
Mark K Cowan
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.