Forcer d'autres développeurs à appeler la méthode une fois leur travail terminé

34

Dans une bibliothèque 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 de ses méthodes peut être appelée plusieurs fois (appelons-la la doWork()méthode). Donc, je ne sais 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 qu'elle devrait les libérer. J'ai mis cette partie dans une méthode (appelons-la release()), mais il n'est pas garanti que d'autres développeurs l'utilisent.

Existe-t-il un moyen de forcer les 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 release()méthode dans la doWork()méthode, car elle a doWork() besoin de ces objets lors de son appel suivant.

hasanghaforian
la source
46
Ce que vous faites est une forme de couplage temporel et est généralement considérée comme une odeur de code. Vous voudrez peut-être vous forcer à créer un meilleur design à la place.
Frank
1
@Frank Je serais d'accord, si changer la conception du calque sous-jacent est une option, alors ce serait le meilleur choix. Mais je soupçonne que ceci est une enveloppe autour du service de quelqu'un d'autre.
Dar Brett
Quel type d'objets lourds votre service a besoin? Si la classe de service n'est pas en mesure de gérer ces ressources automatiquement par elle-même (sans nécessiter de couplage temporel), ces ressources devraient peut-être être gérées ailleurs et injectées dans le service. Avez-vous écrit des tests unitaires pour la classe de service?
VENEZ DU
18
C'est très "non-Java" d'exiger cela. Java est un langage avec garbage collection et maintenant vous créez une sorte d’engin dans lequel vous demandez aux développeurs de "nettoyer".
Pieter B
22
@PieterB pourquoi? AutoCloseablea été fait dans ce but précis.
BgrWorker

Réponses:

48

La solution pragmatique consiste à faire la classe AutoCloseableet à fournir une finalize()méthode de protection (si nécessaire ... voir ci-dessous!). Ensuite, vous comptez sur les utilisateurs de votre classe pour utiliser try-with-resource ou appeler close()explicitement.

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

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


Au sujet des finaliseurs. Cette fonctionnalité Java a très peu de bons cas d'utilisation. Si vous comptez sur les finaliseurs pour ranger, le problème est que cela peut prendre beaucoup de temps pour le ranger. Le finaliseur ne sera exécuté qu'une fois que le GC aura décidé que l'objet n'est plus accessible. Cela ne se produira peut-être pas tant que la machine virtuelle Java ne réalisera pas une collection complète .

Ainsi, si le problème que vous tentez de résoudre consiste à récupérer les ressources qui doivent être libérées rapidement , vous avez manqué le coche en utilisant la finalisation.

Et juste au cas où vous n’auriez pas ce que je dis plus haut ... il n’est presque jamais approprié d’utiliser des finaliseurs dans le code de production, et vous ne devriez jamais vous fier à eux!


1 - Il y a des façons. 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 ), il n'est pas nécessaire d'appeler le code utilisateur release(). Cependant, l'API devient plus compliquée, plus restreinte ... et "moche" à l'OMI. En outre, cela ne force pas le programmeur à faire ce qui est bien . Cela supprime la capacité du programmeur à faire la mauvaise chose!

Stephen C
la source
1
Les finalistes enseignent une très mauvaise leçon - si vous la vissez, je la réparerai pour vous. Je préfère l'approche de netty -> un paramètre de configuration qui indique à la fabrique (ByteBuffer) de créer de manière aléatoire avec une probabilité de X% bytebuffers avec un finaliseur qui imprime l'emplacement où cette mémoire tampon a été acquise (si elle n'a pas déjà été publiée). Donc, en production, cette valeur peut être définie sur 0% - c’est-à-dire pas de frais généraux, et pendant les tests & dev, une valeur supérieure de 50% ou même 100% afin de détecter les fuites avant la sortie du produit
Svetlin Zarev
11
Et rappelez-vous qu'il n'est pas garanti que les finaliseurs soient exécutés, même si l'instance de l'objet est en cours de récupération!
Jwenting
3
Il convient de noter qu'Eclipse émet un avertissement du compilateur si un AutoCloseable n'est pas fermé. Je m'attendrais à ce que d'autres IDE Java le fassent également.
Meriton - en grève
1
@jwenting Dans quel cas ne sont-ils pas exécutés lorsque l'objet est GC'd? Je ne pouvais trouver aucune ressource indiquant ou expliquant cela.
Cedric Reichenbach
3
@Voo Vous avez mal compris mon commentaire. Vous avez deux implémentations -> DefaultResourceet FinalizableResourcequi étend la première et ajoute un finaliseur. En production, vous utilisez DefaultResourceun finaliseur sans> frais généraux. Pendant le développement et les tests, vous configurez l'application à utiliser FinalizableResourceavec un finaliseur et donc une surcharge. Pendant le développement et les tests, ce n’est pas un problème, mais cela peut vous aider à identifier les fuites et à les réparer avant d’atteindre la production. Pourtant, si une fuite atteint PRD, vous pouvez configurer l’usine pour créer X% FinalizableResourceafin d’identifier la fuite
Svetlin Zarev
55

Cela ressemble au cas d' AutoCloseableutilisation de l' interface et à la try-with-resourcesdéclaration introduite 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 à s'inquiéter d'appeler un explicite, releaseque leur code lève une exception ou non.


Réponse supplémentaire

Si vous voulez réellement vous assurer que personne ne peut oublier d'utiliser votre fonction, vous devez faire quelques choses

  1. Assurez-vous que tous les constructeurs de MyServicesont privés.
  2. Définissez un point d’entrée unique à utiliser MyServicepour s’assurer qu’il est nettoyé après.

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

À l'origine, je ne recommandais pas ce chemin car il est affreux sans les lambdas et interfaces fonctionnelles de Java-8 (où MyServiceConsumerdevient Consumer<MyService>) et il est assez imposant pour vos consommateurs. Mais si ce que vous voulez seulement, c'est qu'il release()faille l'appeler, cela fonctionnera.

walpen
la source
1
Cette réponse est probablement meilleure que la mienne. Mon chemin a du retard avant que le récupérateur de déchets n'entre en jeu.
Dar Brett Le
1
Comment vous assurez-vous que les clients utiliseront try-with-resourceset non simplement omettre le try, manquant ainsi l' closeappel?
Frank
@walpen Cette façon de faire a le même problème. Comment puis-je forcer l'utilisateur à utiliser try-with-resources?
hasanghaforian
1
@Frank / @hasanghaforian, Oui, cette façon ne vous oblige pas à être appelé. Cela présente l'avantage de la familiarité: les développeurs Java savent que vous devez vous assurer que le .close()nom est appelé lorsque la classe implémente AutoCloseable.
walpen
1
Ne pourriez-vous pas associer un finaliseur à un usingbloc pour la finalisation déterministe? Au moins en C #, cela devient un modèle assez clair pour que les développeurs prennent l'habitude de l'utiliser lorsqu'ils exploitent des ressources qui nécessitent une finalisation déterministe. Quelque chose de facile et de créer une habitude est la meilleure chose à faire lorsque vous ne pouvez pas forcer quelqu'un à faire quelque chose. stackoverflow.com/a/2016311/84206
AaronLS
52

Oui, vous pouvez

Vous pouvez absolument les forcer à se libérer, et faire en sorte qu’il soit à 100% impossible à oublier, en leur rendant impossible la création de nouvelles Service instances.

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
}

Puis changez-le pour qu'ils puissent y accéder 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 de nos jours, y compris peut-être l' AutoCloseableinterface mentionnée par quelqu'un; mais l'exemple donné devrait fonctionner immédiatement, et à l'exception de l'élégance (qui est un objectif agréable en soi), il ne devrait y avoir aucune raison majeure pour le changer. Notez que cela veut dire que vous pourriez utiliser AutoCloseabledans votre implémentation privée; L'avantage de ma solution par rapport à l'utilisation de vos utilisateurs AutoCloseableest qu'elle peut, encore une fois, ne pas être oubliée.
  • Vous devrez préciser si nécessaire, par exemple pour injecter des arguments dans le new Service appel.
  • Comme mentionné dans les commentaires, la question de savoir si un tel construit (c'est-à-dire prendre le processus de création et de destruction d'un Service ) appartient à la main de l’appelant ou non sort du cadre 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.
AnoE
la source
2
Je suis content des commentaires pour le vote négatif, afin que je puisse améliorer la réponse.
AnoE
2
Je n'ai pas voté vers le bas, mais cette conception risque de causer plus de problèmes qu'elle n'en vaut la peine. Cela ajoute beaucoup de complexité et impose un lourd fardeau aux appelants. Les développeurs doivent être suffisamment familiarisés avec les classes de style try-with-resources. Leur utilisation est une seconde nature lorsqu'une classe implémente l'interface appropriée, ce qui est généralement suffisant.
jpmc26
30
@ jpmc26, curieusement, j’estime que cette approche réduit la complexité et le fardeau des appelants en leur évitant de penser au cycle de vie des ressources. À part ça; le PO n'a pas demandé "dois-je forcer les utilisateurs ..." mais "comment forcer les utilisateurs". Et si elle est suffisamment importante, c’est une solution qui fonctionne très bien, qui est structurellement simple (il suffit de l’envelopper dans une fermeture / exécutable), même de la transférer dans d’autres langues disposant également de lambdas / procs / closures. Eh bien, je laisserai à la démocratie SE le soin de juger; le PO a déjà décidé de toute façon. ;)
AnoE
14
Encore une fois, @ jpmc26, je ne suis pas vraiment intéressé à discuter de la pertinence de cette approche pour un cas d'utilisation unique. Le PO demande comment appliquer une telle chose et cette réponse indique comment l’appliquer . Ce n'est pas à nous de décider s'il est approprié de le faire en premier lieu. La technique présentée est un outil, rien de plus, rien de moins, et utile d’avoir, je crois, un sac d’outils.
AnoE
11
C’est la bonne réponse à mon humble avis, c’est essentiellement le motif «trou dans le milieu»
jk.
11

Vous pouvez essayer d'utiliser le modèle de commande .

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 responsable de l'acquisition et du nettoyage des ressources.

Il y a un piège, cependant. 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 quand il y a une crise. Lorsqu'il y a des problèmes de performances et que vous découvrez que certains appelants le font, montrez-leur simplement la manière appropriée et résolvez le problème:

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

Vous pouvez rendre cette complexité arbitraire en mettant en œuvre des promesses pour des tâches qui dépendent d'autres tâches, mais la publication d'éléments devient également plus compliquée. Ceci n'est qu'un point de départ.

Async

Comme indiqué dans les commentaires, ce qui précède ne fonctionne pas vraiment avec les demandes asynchrones. C'est correct. Mais cela peut être facilement résolu dans Java 8 avec l’utilisation de CompleteableFuture , en particulier CompleteableFuture#supplyAsyncpour créer des futurs individuels et CompleteableFuture#allOfpour permettre la libération des ressources une fois que toutes les tâches sont terminées. Sinon, on peut toujours utiliser Threads ou ExecutorServices pour lancer leur propre implémentation de Futures / Promises.

Polygone
la source
1
J'apprécierais que les électeurs qui arrivent au bas laissent un commentaire expliquant pourquoi ils pensent que c'est mauvais, afin que je puisse répondre directement à ces préoccupations ou au moins obtenir un autre point de vue pour l'avenir. Merci!
Polygnome
+1 upvote. Cela peut être une approche, mais le service est asynchrone et le consommateur doit obtenir le premier résultat avant de demander le prochain service.
hasanghaforian
1
Ceci est juste un modèle barebones. JS résout le problème avec des promesses, vous pourriez faire la même chose en Java avec des futures et un collecteur appelé lorsque toutes les tâches sont terminées, puis nettoyé. Il est tout à fait possible d’obtenir de l’async et même des dépendances là-dedans, mais cela complique également beaucoup les choses.
Polygnome
3

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 transmettra à la méthode de l'objet visiteur et le libérera ensuite.

9000
la source
Merci pour votre réponse, mais excusez-moi, voulez-vous dire qu'il est préférable de transmettre des ressources au consommateur, puis de le récupérer lorsqu'il demande un service? Ensuite, le problème persiste: le consommateur peut oublier de libérer ces ressources.
hasanghaforian
Si vous souhaitez contrôler une ressource, ne la lâchez pas, ne la transmettez pas complètement au flux d'exécution de quelqu'un d'autre. Une façon de le faire est d'exécuter une méthode prédéfinie de l'objet visiteur d'un utilisateur. AutoCloseablefait une chose très similaire dans les coulisses. Sous un autre angle, c'est similaire à l' injection de dépendance .
9000
1

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

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

Donc, 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 ();

Sava B.
la source
C'est la même solution, mais au lieu de demander à l'appelant de gérer la liste des tâches, vous la maintenez en interne. Le concept de base est littéralement le même. Mais cela ne suit pas les conventions de codage pour Java que j'ai jamais vues et est en fait assez difficile à lire (corps de la boucle sur la même ligne que l'en-tête de la boucle, _ au début).
Polygnome
Oui, c'est le modèle de commande. Je viens de mettre un peu de code pour donner un résumé de ce que je pensais de la manière la plus concise possible. J'écris surtout en C # ces jours-ci, où les variables privées sont souvent précédées de _.
Sava B.
-1

Une autre alternative à celles mentionnées serait l'utilisation de "références intelligentes" pour gérer vos ressources encapsulées de manière à ce que le ramasse-miettes puisse nettoyer automatiquement sans libérer manuellement de ressources. Leur utilité dépend bien sûr de votre mise en œuvre. En savoir plus sur ce sujet dans ce merveilleux article de blog: https://community.oracle.com/blogs/enicholas/2006/05/04/understanding-weak-references

Dracam
la source
C'est une idée intéressante, mais je ne vois pas en quoi l'utilisation de références faibles résout le problème du PO. La classe de service ne sait pas si elle va obtenir une autre demande doWork (). S'il libère sa référence à ses objets lourds et qu'il obtient un autre appel à doWork (), il échouera.
Jay Elston
-4

Pour tout autre langage de classe, je dirais de le mettre dans le destructeur, mais comme ce n’est pas une option, je pense que vous pouvez remplacer la méthode finalize. Ainsi, lorsque l'instance sort du cadre et que les ordures sont collectées, elle sera publiée.

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

Je ne connais pas trop Java, mais je pense que c’est l’objectif auquel finalize () est destiné.

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

Dar Brett
la source
7
C'est une mauvaise solution au problème pour la raison pour laquelle Stephen dit dans sa réponse -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
Et même dans ce cas, le finaliste pourrait ne pas être exécuté ...
jwenting
3
Les finaliseurs sont notoirement peu fiables: ils peuvent courir, ils ne peuvent jamais courir, s’ils courent, il n’ya aucune garantie quant au moment où ils courront. Même les architectes Java tels que Josh Bloch disent que les finaliseurs sont une idée terrible (référence: Effective Java (2nd Edition) ).
Ce genre de problèmes me fait rater C ++ avec sa destruction déterministe et RAII ...
Mark K Cowan