web-dev-qa-db-fra.com

Forcer les autres développeurs à appeler la méthode après avoir terminé leur travail

Dans une bibliothèque en Java 7, j'ai une classe qui fournit des services à d'autres classes. Après avoir créé une instance de cette classe de service, une méthode peut être appelée plusieurs fois (appelons-la doWork() méthode). Je ne sais donc pas quand le travail de la classe de service est terminé.

Le problème est que la classe de service utilise des objets lourds et doit les libérer. J'ai défini cette partie dans une méthode (appelons-la release()), mais il n'est pas garanti que d'autres développeurs utiliseront cette méthode.

Existe-t-il un moyen de forcer d'autres développeurs à appeler cette méthode après avoir terminé la tâche de classe de service? Bien sûr, je peux documenter cela, mais je veux les forcer.

Remarque: Je ne peux pas appeler la méthode release() dans la méthode doWork(), car doWork() a besoin de ces objets lors de son prochain appel.

34
hasanghaforian

La solution pragmatique est de créer la classe AutoCloseable, et de fournir une méthode finalize() comme filet de sécurité (le cas échéant ... voir ci-dessous!). Ensuite, vous comptez sur les utilisateurs de votre classe pour utiliser essayez-avec-ressource ou appelez close() explicitement.

Bien sûr, je peux documenter cela, mais je veux les forcer.

Malheureusement, il n'y a aucun moyen1 in Java pour forcer le programmeur à faire la bonne chose. Le mieux que vous puissiez espérer faire est de détecter une utilisation incorrecte dans un analyseur de code statique.


Sur le thème des finaliseurs. Cette fonctionnalité Java a très peu de bons ) cas d'utilisation. Si vous comptez sur les finaliseurs pour ranger, vous rencontrez problème que le rangement peut prendre très longtemps. Le finaliseur ne sera exécuté que après le GC décide que l'objet n'est plus accessible. Cela peut ne pas se produire tant que la JVM ne fait pas une collection complète.

Ainsi, si le problème que vous tentez de résoudre est de récupérer les ressources qui doivent être libérées tôt, alors vous avez raté le bateau en utilisant la finalisation.

Et juste au cas où vous n'avez pas ce que je dis ci-dessus ... il est presque jamais approprié d'utiliser des finaliseurs dans le code de production, et vous devriez ne jamais compter sur eux!


1 - Il existe des moyens. Si vous êtes prêt à "masquer" les objets de service du code utilisateur ou à contrôler étroitement leur cycle de vie (par exemple https://softwareengineering.stackexchange.com/a/345100/172 ), le code utilisateur ne fonctionne pas " t besoin pour appeler release(). Cependant, l'API devient plus compliquée, restreinte ... et IMO "laide". De plus, ce n'est pas forçant le programmeur à faire ce qu'il faut. Cela supprime la capacité du programmeur à faire la mauvaise chose!

48
Stephen C

Cela semble être le cas d'utilisation de AutoCloseable interface et try-with-resources Instruction introduit dans Java 7. Si vous avez quelque chose comme

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

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

alors vos consommateurs peuvent l'utiliser comme

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

et ne pas avoir à vous soucier d'appeler un release explicite, que leur code lève ou non une exception.


Réponse supplémentaire

Si ce que vous cherchez vraiment est de faire en sorte que personne n'oublie d'utiliser votre fonction, vous devez faire quelques choses

  1. Assurez-vous que tous les constructeurs de MyService sont privés.
  2. Définissez un seul point d'entrée pour utiliser MyService qui garantit qu'il est nettoyé après.

Tu ferais quelque chose comme

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

Vous l'utiliseriez alors comme

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

Je n'ai pas recommandé ce chemin à l'origine car il est hideux sans lambda Java-8 et interfaces fonctionnelles (où MyServiceConsumer devient Consumer<MyService>) Et il est assez imposant pour vos consommateurs. Mais si ce que vous voulez seulement, c'est que release() doive être appelée, cela fonctionnera.

55
walpen

Oui, vous pouvez

Vous pouvez absolument les forcer à sortir, et faire en sorte qu'il soit 100% impossible d'oublier, en les empêchant de créer de nouvelles instances Service.

S'ils ont fait quelque chose comme ça avant:

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

Ensuite, changez-le pour qu'ils y accèdent comme ceci.

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

Mises en garde:

  • Considérez ce pseudocode, cela fait longtemps que je n'ai pas écrit Java.
  • Il pourrait y avoir des variantes plus élégantes ces jours-ci, y compris peut-être l'interface AutoCloseable que quelqu'un a mentionnée; mais l'exemple donné devrait fonctionner hors de la boîte, et à l'exception de l'élégance (qui est un objectif de Nice en soi), il ne devrait y avoir aucune raison majeure de le changer. Notez que cela veut dire que vous pouvez utiliser AutoCloseable dans votre implémentation privée; l'avantage de ma solution par rapport à l'utilisation par vos utilisateurs de AutoCloseable est qu'elle ne peut, encore une fois, pas être oubliée.
  • Vous devrez l'étoffer au besoin, par exemple pour injecter des arguments dans le new Service appel.
  • Comme mentionné dans les commentaires, la question de savoir si une construction comme celle-ci (c'est-à-dire prendre le processus de création et de destruction d'un Service) appartient ou non à la main de l'appelant est en dehors du champ d'application de cette réponse. Mais si vous décidez que vous en avez absolument besoin, voici comment vous pouvez le faire.
  • Un intervenant a fourni ces informations concernant la gestion des exceptions: Google Books
52
AnoE

Vous pouvez essayer d'utiliser le modèle 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();
    }

}

Cela vous donne un contrôle total sur l'acquisition et la libération des ressources. L'appelant ne soumet que des tâches à votre service et vous êtes vous-même responsable de l'acquisition et du nettoyage des ressources.

Il y a cependant un hic. L'appelant peut toujours faire ceci:

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

Mais vous pouvez résoudre ce problème lorsqu'il survient. Lorsqu'il y a des problèmes de performances et que vous découvrez que certains appelants le font, montrez-leur simplement la bonne façon et résolvez le problème:

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

Vous pouvez rendre cela arbitrairement complexe en implémentant des promesses pour des tâches qui dépendent d'autres tâches, mais la publication de choses devient également plus compliquée. Ce n'est qu'un point de départ.

Async

Comme cela a été souligné dans les commentaires, ce qui précède ne fonctionne pas vraiment avec les requêtes asynchrones. C'est correct. Mais cela peut facilement être résolu dans Java 8 avec l'utilisation de CompleteableFuture , en particulier CompleteableFuture#supplyAsync pour créer les futurs individuels et CompleteableFuture#allOf pour effectuer la libération des ressources une fois tous les tâches terminées. Alternativement, on peut toujours utiliser Threads ou ExecutorServices pour lancer leur propre implémentation de Futures/Promises.

11
Polygnome

Si vous le pouvez, n'autorisez pas l'utilisateur à créer la ressource. Laissez l'utilisateur passer un objet visiteur à votre méthode, ce qui créera la ressource, le passera à la méthode de l'objet visiteur et le relâchera ensuite.

3
9000

Mon idée était similaire à celle de Polygnome.

Vous pouvez avoir les méthodes "do_something ()" dans votre classe juste ajouter des commandes à une liste de commandes privée. Ensuite, ayez une méthode "commit ()" qui fait le travail et appelle "release ()".

Ainsi, si l'utilisateur n'appelle jamais commit (), le travail n'est jamais terminé. S'ils le font, les ressources sont libérées.

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

}

Vous pouvez ensuite l'utiliser comme foo.doSomething.doSomethineElse (). Commit ();

1
Sava B.