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.
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!
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
MyService
sont privés.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.
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:
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. new Service
appel.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.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.
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.
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.
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 ();