Заставить других разработчиков вызывать метод после завершения их работы

34

В библиотеке Java 7 у меня есть класс, который предоставляет сервисы другим классам. После создания экземпляра этого класса обслуживания один его метод может вызываться несколько раз (назовем его doWork()методом). Поэтому я не знаю, когда работа класса обслуживания будет завершена.

Проблема в том, что класс обслуживания использует тяжелые объекты и должен их освобождать. Я установил эту часть в методе (давайте назовем его release()), но не гарантируется, что другие разработчики будут использовать этот метод.

Есть ли способ заставить других разработчиков вызывать этот метод после выполнения задачи класса обслуживания? Конечно, я могу это задокументировать, но я хочу заставить их.

Примечание: я не могу вызвать release()метод в doWork()методе, потому что doWork() нуждается в тех объектах, когда он вызывается в следующем.

hasanghaforian
источник
46
То, что вы делаете, - это форма временной связи, которая обычно считается запахом кода. Вы можете заставить себя придумать лучший дизайн вместо этого.
Франк
1
@ Фрэнк, я бы согласился, если изменение дизайна нижележащего слоя является вариантом, то это было бы лучшим выбором. Но я подозреваю, что это обертка вокруг чужой службы.
Дар Бретт
Какой вид тяжелых предметов нужен вашему Сервису? Если класс Сервиса не может самостоятельно управлять этими ресурсами (не требуя временной связи), возможно, этими ресурсами нужно управлять в другом месте и вводить в Сервис. Вы написали модульные тесты для класса обслуживания?
ПРИХОДИТ ОТ
18
Это очень "не Java", чтобы требовать этого. Ява - это язык со сборкой мусора, и теперь вы придумаете какое-то хитроумное средство, в котором вам нужно, чтобы разработчики «очистились».
Питер Б
22
@PieterB почему? AutoCloseableбыл сделан именно для этой цели.
BgrWorker

Ответы:

48

Прагматичное решение состоит в том, чтобы создать класс AutoCloseableи предоставить finalize()метод в качестве поддержки (если это уместно ... см. Ниже!). Затем вы полагаетесь на то, что пользователи вашего класса будут использовать try-with-resource или close()явно вызывать .

Конечно, я могу это задокументировать, но я хочу заставить их.

К сожалению, нет никакого способа 1 в Java , чтобы заставить программиста сделать правильную вещь. Лучшее, на что вы можете надеяться, это обнаружить неправильное использование в статическом анализаторе кода.


По теме финализаторов. Эта функция Java имеет очень мало хороших вариантов использования. Если вы упорядочиваете финализаторы, вы сталкиваетесь с проблемой того, что приведение в порядок может занять очень много времени. Финализатор будет запущен только после того, как ГХ решит, что объект больше недоступен. Этого может не произойти, пока JVM не выполнит полную коллекцию.

Таким образом, если проблема, которую вы пытаетесь решить, состоит в том, чтобы вернуть ресурсы, которые должны быть выпущены раньше , то вы пропустили лодку, используя финализацию.

И на тот случай, если вы не поняли того, что я говорю выше ... почти никогда не целесообразно использовать финализаторы в производственном коде, и вы никогда не должны полагаться на них!


1 - Есть способы. Если вы готовы «спрятать» объекты службы от кода пользователя или жестко контролировать их жизненный цикл (например, https://softwareengineering.stackexchange.com/a/345100/172 ), тогда код пользователя не должен вызывать release(). Тем не менее, API становится более сложным, ограниченным ... и "некрасивым" IMO. Кроме того, это не заставляет программиста поступать правильно . Это устраняет способность программиста делать неправильные вещи!

Стивен С
источник
1
Финализаторы преподносят очень плохой урок - если ты это испортишь, я исправлю это для тебя. Я предпочитаю подход netty -> параметр конфигурации, который инструктирует фабрику (ByteBuffer) случайным образом создавать с вероятностью X% байтовых буферов с помощью финализатора, который печатает местоположение, где этот буфер был получен (если еще не освобожден). Таким образом, в производстве это значение может быть установлено на 0% - т.е. без накладных расходов, а во время тестирования & dev - на более высокое значение, такое как 50% или даже 100%, чтобы обнаружить утечки перед выпуском продукта
Светлин Зарев,
11
и помните, что финализаторы не гарантированно когда-либо будут работать, даже если экземпляр объекта собирается сборщиком мусора!
17
3
Стоит отметить, что Eclipse выдает предупреждение компилятора, если AutoCloseable не закрыт. Я ожидаю, что другие Java IDE также сделают это.
меритон
1
@jwenting В каком случае они не запускаются, когда объект GC'd? Я не мог найти какой-либо ресурс, в котором говорится или объясняется.
Седрик Райхенбах
3
@ Voo Вы неправильно поняли мой комментарий. У вас есть две реализации -> DefaultResourceи FinalizableResourceкоторая проходит первый и добавляет финализации. В производстве вы используете, DefaultResourceкоторый не имеет финализатор-> нет накладных расходов. Во время разработки и тестирования вы настраиваете приложение для использования, FinalizableResourceкоторое имеет финализатор и, следовательно, добавляет накладные расходы. Во время разработки и тестирования это не проблема, но она может помочь вам выявить утечки и устранить их, прежде чем приступить к работе. Тем не менее, если утечка достигает PRD, вы можете настроить фабрику для создания X%, FinalizableResourceчтобы идентифицировать утечку
Светлин Зарев
55

Это похоже на сценарий использования AutoCloseableинтерфейса и try-with-resourcesоператор, введенный в Java 7. Если у вас есть что-то вроде

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 и функциональных интерфейсов (где это MyServiceConsumerделается Consumer<MyService>), и это довольно навязывает потребителям. Но если то, что вы хотите, это то, что вам release()нужно позвонить, это сработает.

walpen
источник
1
Этот ответ, вероятно, лучше, чем мой. Мой путь задерживается до того, как включится сборщик мусора.
Дар Бретт,
1
Как вы гарантируете, что клиенты будут использовать, try-with-resourcesа не просто пропускать try, тем самым пропуская closeвызов?
Франк
@walpen У этого способа та же проблема. Как я могу заставить пользователя использовать try-with-resources?
hasanghaforian
1
@Frank / @hasanghaforian, да, этот способ не заставляет близко называться. У него есть преимущество знакомства: Java-разработчики знают, что вы действительно должны убедиться, что .close()вызывается при реализации класса AutoCloseable.
Walpen
1
Не могли бы вы соединить финализатор с usingблоком для детерминированной финализации? По крайней мере, в C # это становится довольно четким шаблоном для разработчиков, использующих ресурсы, требующие детерминированной доработки. Нечто легкое и формирование привычки - следующая лучшая вещь, когда вы не можете заставить кого-то что-то делать. stackoverflow.com/a/2016311/84206
AaronLS
52

Да, ты можешь

Вы можете абсолютно заставить их выпустить и сделать так, чтобы их было на 100% невозможно забыть, лишив их возможности создавать новые Serviceэкземпляры.

Если они сделали что-то подобное раньше:

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 Книги
Anoe
источник
2
Я рад комментариям для downvote, поэтому я могу улучшить ответ.
AnoE
2
Я не отрицал, но этот дизайн, вероятно, будет больше проблем, чем оно того стоит. Это добавляет большую сложность и накладывает большое бремя на абонентов. Разработчики должны быть достаточно знакомы с классами стиля try-with-resources, чтобы их использование было второй натурой всякий раз, когда класс реализует соответствующий интерфейс, так что это обычно достаточно хорошо.
jpmc26
30
@ jpmc26, как ни странно, я чувствую, что такой подход уменьшает сложность и нагрузку на абонентов , избавляя их от размышлений о жизненном цикле ресурса вообще. Помимо этого; ОП не спрашивал «должен ли я принуждать пользователей ...», а «как заставить пользователей». И если это достаточно важно, это одно решение, которое работает очень хорошо, структурно просто (просто заключает его в замыкание / работает), даже может быть перенесено на другие языки, которые также имеют лямбды / procs / замыкания. Что ж, я оставлю это на усмотрение демократии ЮВ; ОП уже решил в любом случае. ;)
AnoE
14
Опять же, @ jpmc26, мне не очень интересно обсуждать, является ли этот подход правильным для любого отдельного варианта использования. ОП спрашивает, как применить такую ​​вещь, и этот ответ говорит, как применить такую ​​вещь . Не нам решать, уместно ли это делать в первую очередь. Я полагаю, что показанная техника - инструмент, не более, не менее и полезный, чтобы иметь в своем распоряжении целый набор инструментов.
AnoE
11
Это правильный ответ imho, это, по сути, паттерн «дыра в середине»
jk.
11

Вы можете попытаться использовать шаблон 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();
    }

}

Это дает вам полный контроль над приобретением и выпуском ресурсов. Вызывающий абонент только отправляет задачи в вашу Службу, а вы сами несете ответственность за приобретение и очистку ресурсов.

Однако есть одна загвоздка. Абонент все еще может сделать это:

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

Вы можете сделать это сколь угодно сложным, выполняя обещания для задач, которые зависят от других задач, но затем выпуск вещей также становится более сложным. Это только отправная точка.

асинхронный

Как было отмечено в комментариях, вышеприведенное не работает с асинхронными запросами. Правильно. Но это легко может быть решено в Java 8 с использованием CompleteableFuture , особенно CompleteableFuture#supplyAsyncдля создания отдельных вариантов будущего и CompleteableFuture#allOfдля освобождения ресурсов после завершения всех задач. В качестве альтернативы, всегда можно использовать Threads или ExecutorServices для развертывания собственной реализации Futures / Promises.

Polygnome
источник
1
Я был бы признателен, если бы подписчики оставили комментарий, почему они считают это плохим, так что я могу либо обратиться к этим проблемам напрямую, либо хотя бы получить другую точку зрения на будущее. Благодарность!
Полигном
+1 upvote. Это может быть подход, но услуга асинхронная, и потребитель должен получить первый результат, прежде чем запрашивать следующую услугу.
hasanghaforian
1
Это просто шаблон barebones. JS решает эту проблему с помощью обещаний, вы можете делать подобные вещи в Java с фьючерсами и сборщиком, который вызывается, когда все задачи завершаются, а затем очищается. Определенно возможно получить асинхронность и даже зависимости, но это также сильно усложняет ситуацию.
Полигном
3

Если вы можете, не позволяйте пользователю создавать ресурс. Позвольте пользователю передать объект посетителя вашему методу, который создаст ресурс, передаст его методу объекта посетителя и впоследствии освободит его.

9000
источник
Спасибо за ваш ответ, но извините, вы имеете в виду, что лучше передать ресурсы потребителю, а затем получить его снова, когда он попросит об услуге? Тогда проблема останется: потребитель может забыть освободить эти ресурсы.
hasanghaforian
Если вы хотите контролировать ресурс, не отпускайте его, не передавайте его другому потоку выполнения полностью. Один из способов сделать это - запустить предопределенный метод объекта посетителя пользователя. AutoCloseableделает очень похожую вещь за кадром. Под другим углом это похоже на внедрение зависимости .
9000
1

Моя идея была похожа на идею Полигнома.

Вы можете использовать методы 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, которые я когда-либо видел, и на самом деле довольно трудно читать (тело цикла в той же строке, что и заголовок цикла, _ в начале).
Полигном
Да, это шаблон команды. Я просто записал некоторый код, чтобы изложить то, о чем я думал, как можно более кратко. Сейчас я в основном пишу на C #, где частные переменные часто начинаются с префикса _.
Сава Б.
-1

Другой альтернативой упомянутым может быть использование «умных ссылок» для управления инкапсулированными ресурсами таким образом, чтобы сборщик мусора автоматически очищался без освобождения ресурсов вручную. Их полезность зависит, конечно, от вашей реализации. Подробнее об этой теме читайте в этом замечательном сообщении в блоге: https://community.oracle.com/blogs/enicholas/2006/05/04/understanding-weak-references

Dracam
источник
Это интересная идея, но я не вижу, как использование слабых ссылок решает проблему ОП. Класс обслуживания не знает, получит ли он еще один запрос 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.
Дейнит,
4
И даже тогда финализации не может фактически бежать ...
jwenting
3
Финализаторы, как известно, ненадежны: они могут работать, они могут никогда не работать, если они работают, нет гарантии, что они будут работать. Даже архитекторы Java, такие как Джош Блох, говорят, что финализаторы - ужасная идея (ссылка: Effective Java (2nd Edition) ).
Подобные проблемы заставляют меня скучать по C ++ с его детерминированным разрушением и RAII ...
Марк К Коуэн