Примушуйте інших розробників викликати метод після завершення роботи


34

У бібліотеці Java 7 у мене є клас, який надає послуги іншим класам. Після створення екземпляра цього класу обслуговування один метод його може бути викликаний кілька разів (назвемо його doWork()методом). Тож я не знаю, коли робота класу обслуговування закінчена.

Проблема полягає в тому, що клас обслуговування використовує важкі об'єкти, і він повинен звільнити їх. Я встановив цю частину методом (назвемо так release()), але не гарантується, що інші розробники будуть використовувати цей метод.

Чи є спосіб змусити інших розробників викликати цей метод після виконання завдання сервісного класу? Звичайно, я можу це документувати, але хочу їх змусити.

Примітка: я не можу викликати release()метод у doWork()методі, тому що doWork() потрібні ці об'єкти, коли він викликається у наступному.


46
Те, що ви робите там, є формою тимчасової зв’язки і зазвичай вважається кодовим запахом. Можливо, ви захочете змусити себе придумати кращий дизайн.
Френк

1
@Frank Я погодився б, якщо зміна дизайну базового шару є варіантом, то це було б найкраще. Але я підозрюю, що це обгортка навколо чужої служби.
Дар Бретт

Які важкі предмети потрібні вашій службі? Якщо клас Сервісу не в змозі самостійно керувати цими ресурсами (не вимагаючи тимчасової зв'язку), можливо, цими ресурсами слід керувати в іншому місці та вводити їх у Сервіс. Ви написали одиничні тести для класу обслуговування?
ПРИЙДАЄТЬСЯ

18
Цього вимагати дуже "un-Java". Java - це мова зі збиранням сміття, і тепер ви придумали якусь контрацепцію, в якій вимагаєте від розробників «очищення».
Пітер Б

22
@PieterB чому? AutoCloseableбуло зроблено саме з цією метою.
BgrWorker

Відповіді:


48

Прагматичне рішення - скласти клас AutoCloseableта надати finalize()метод як зворотний стоп (якщо це доречно ... див. Нижче!). Тоді ви покладаєтесь на користувачів вашого класу, щоб користуватися пробним ресурсом або close()явно телефонувати .

Звичайно, я можу це документувати, але хочу їх змусити.

На жаль, немає ніякого способу 1 в Java , щоб змусити програміста зробити правильну річ. Найкраще, на що ви можете сподіватися, - це підібрати неправильне використання в аналізаторі статичного коду.


По темі фіналізаторів. Ця функція Java має дуже мало корисних випадків використання. Якщо ви покладаєтесь на фіналізатори, щоб привести в порядок, ви зіткнетесь з проблемою, що це може зайняти дуже багато часу, щоб привести в порядок. Фіналізатор буде запущений лише після того, як GC вирішить, що об'єкт більше недоступний. Це може не статися до тих пір, поки JVM не зробить повну колекцію.

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

І на всякий випадок, якщо у вас немає того, про що я говорив вище ... майже ніколи не доцільно використовувати фіналізатори у виробничому коді, і ви ніколи не повинні покладатися на них!


1 - Є способи. Якщо ви готові "заховати" об’єкти сервісу від коду користувача або жорстко контролювати їх життєвий цикл (наприклад, https://softwareengineering.stackexchange.com/a/345100/172 ), тоді користувачеві код не потрібно дзвонити release(). Однак API стає складнішим, обмеженим ... та "потворним" ІМО. Також це не змушує програміста робити правильно . Це усунення здатності програміста робити неправильну справу!


1
Фіналізатори вчать дуже поганий урок - якщо ви його накрутите, я це виправлю. Я віддаю перевагу підходу netty -> параметру конфігурації, який доручає фабриці (ByteBuffer) довільно створювати з вірогідністю X% байтбуферів з фіналізатором, який друкує місце, де цей буфер був придбаний (якщо він ще не вийшов). Тож у виробництві це значення можна встановити на 0% - тобто немає накладних витрат, а під час випробувань & dev до більш високого значення, як 50% або навіть 100%, щоб виявити витоки перед випуском продукту
Світлін Зарев

11
і пам’ятайте, що фіналізатори не гарантуються ніколи, навіть якщо об’єкт об'єкта збирається сміттям!
jwenting

3
Варто відзначити, що Eclipse надсилає попередження компілятора, якщо автозахисний файл не закритий. Я також очікував, що це зробить і інший 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()потрібно викликати, це спрацює.


1
Ця відповідь, мабуть, краща за мою. Мій шлях має затримку до того, як починає збирати сміття.
Дар Бретт

1
Як ви гарантуєте, що клієнти будуть використовувати, try-with-resourcesа не просто опускати try, тим самим пропускаючи closeвиклик?
Френк

@walpen У цього шляху є та ж проблема. Як змусити користувача використовувати try-with-resources?
hasanghaforian

1
@Frank / @hasanghaforian, Так, цей спосіб не змушує бути близьким викликати. Це має перевагу знайомства: розробники Java знають, що вам справді слід переконатися, що .close()викликається, коли клас реалізується AutoCloseable.
проходити

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виклик.
  • Як зазначалося в коментарях, питання про те, чи належить така конструкція (тобто, взявши процес створення та знищення а Service), належить до руки абонента чи ні, не виходить за рамки цієї відповіді. Але якщо ви вирішили, що вам це абсолютно потрібно, саме так ви можете це зробити.
  • Один із коментаторів надав цю інформацію щодо обробки винятків: Google Books

2
Я щасливий щодо коментарів щодо голосування, тому можу вдосконалити відповідь.
AnoE

2
Я не заявив, але ця конструкція, швидше за все, складе більше проблем, ніж це варто. Це додає великої складності та накладає велике навантаження на абонентів. Розробники повинні бути достатньо знайомі з класами стилів, які використовуються для використання ресурсів, що їх використання є другою природою, коли клас реалізує відповідний інтерфейс, так що це, як правило, досить добре.
jpmc26

30
@ jpmc26, дивно, я вважаю, що такий підхід зменшує складність та навантаження на абонентів, врятуючи їх взагалі від роздумів про життєвий цикл ресурсів. Окрім цього; ОП не запитувала "чи варто змушувати користувачів ...", але "як змусити користувачів". І якщо це досить важливо, це одне рішення, яке працює дуже добре, є структурно простим (просто загортаючи його в замикання / запуску), навіть перекладається на інші мови, які також мають лямбдаси / прок / закриття. Що ж, я залишаю це демократії СВ, щоб судити; ОП вже вирішила все одно. ;)
AnoE

14
Знову ж таки, @ jpmc26, я не так сильно зацікавлений обговорювати, чи правильний такий підхід для будь-яких випадків використання в одній з них. ОП запитує, як застосувати таку річ, і ця відповідь розповідає, як таке застосувати . Не нам вирішувати, чи доцільно це робити в першу чергу. Показана техніка - це інструмент, нічого більш, нічого менш, і я вважаю корисним мати в цих пакетах з інструментами.
AnoE

11
Це правильна відповідь імхо, це, по суті, шаблон "отвір у середині"
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для досконалого випуску ресурсів, коли всі завдання завершені. Крім того, завжди можна використовувати теми або сервіси ExecutorServices, щоб виконати власну реалізацію Futures / Promises.


1
Я би вдячний, якщо бродячі залишають коментар, чому вони вважають це поганим, так що я можу або вирішити ці проблеми безпосередньо, або принаймні отримати іншу точку зору на майбутнє. Спасибі!
Полігном

+1 виклик. Це може бути підхід, але сервіс є асинхронним, і споживач повинен отримати перший результат, перш ніж просити наступну послугу.
hasanghaforian

1
Це просто шаблони босоніжок. JS вирішує це за допомогою обіцянок, ви можете робити подібні речі на Java з ф'ючерсами та колектором, який викликається, коли всі завдання закінчені, а потім очиститься. Тут безумовно можливо отримати асинхронність і навіть залежності, але це також дуже сильно ускладнює справи.
Полігном

3

Якщо можете, не дозволяйте користувачеві створювати ресурс. Дозвольте користувачеві передати об’єкт відвідувача вашому методу, який створить ресурс, передати його методу об’єкта відвідувача і після цього звільнити.


Дякую за вашу відповідь, але вибачте, ви маєте на увазі, що краще передавати ресурси споживачеві, а потім отримувати його знову, коли він попросить послуги? Тоді проблема залишатиметься: споживач може забути випустити ці ресурси.
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


Це цікава ідея, але я не бачу, як використання слабких посилань вирішує проблему ОП. Клас обслуговування не знає, чи збирається отримати ще один запит doWork (). Якщо він випустить посилання на свої важкі об'єкти, а потім отримає ще один дзвінок doWork (), він вийде з ладу.
Джей Елстон

-4

Для будь-якої іншої мови, орієнтованої на клас, я б сказав, що я поставив його в деструктор, але оскільки це не варіант, я думаю, що ви можете перекрити метод завершення. Таким чином, коли екземпляр вийде за межі і буде зібрано сміття, він буде випущений.

@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.
Daenyth

4
І навіть тоді фіналізатор може насправді не працювати ...
jwenting

3
Фіналізатори, як відомо, ненадійні: вони можуть бігати, вони ніколи не бігати, якщо вони бігають, немає гарантії, коли вони будуть працювати. Навіть архітектори Java, такі як Джош Блок, кажуть, що фіналізатори - це жахлива ідея (довідка: Ефективна Java (2-е видання) ).

Такі проблеми змушують мене пропустити C ++ з його детермінованим знищенням та RAII ...
Mark K Cowan
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.