Forçar outros desenvolvedores a chamar o método depois de concluir seu trabalho

34

Em uma biblioteca em Java 7, tenho uma classe que fornece serviços para outras classes. Após criar uma instância dessa classe de serviço, um método pode ser chamado várias vezes (vamos chamá-lo de doWork()método). Portanto, não sei quando o trabalho da classe de serviço está concluído.

O problema é que a classe de serviço usa objetos pesados ​​e deve liberá-los. Defino essa parte em um método (vamos chamá-lo release()), mas não é garantido que outros desenvolvedores usem esse método.

Existe uma maneira de forçar outros desenvolvedores a chamar esse método depois de concluir a tarefa da classe de serviço? Claro que posso documentar isso, mas quero forçá-los.

Nota: Não posso chamar o release()método no doWork()método, porque doWork() precisa desses objetos quando for chamado a seguir.

hasanghaforian
fonte
46
O que você está fazendo lá é uma forma de acoplamento temporal e geralmente é considerado um cheiro de código. Você pode se forçar a criar um design melhor.
28417 Frank
11
@ Frank, eu concordo, se alterar o design da camada subjacente for uma opção, essa seria a melhor aposta. Mas suspeito que seja um invólucro do serviço de outra pessoa.
Dar Brett
Que tipo de objetos pesados ​​seu Serviço precisa? Se a classe Service não puder gerenciar esses recursos automaticamente por si mesma (sem exigir acoplamento temporal), talvez esses recursos devam ser gerenciados em outro local e injetados no Serviço. Você escreveu testes de unidade para a classe Serviço?
Vem de
18
É muito "un-Java" exigir isso. Java é uma linguagem com coleta de lixo e agora você cria algum tipo de engenhoca na qual exige que os desenvolvedores "limpem".
Pieter B
22
@PieterB why? AutoCloseablefoi feito para esse propósito exato.
BgrWorker

Respostas:

48

A solução pragmática é fazer a classe AutoCloseablee fornecer um finalize()método como um pano de fundo (se apropriado ... veja abaixo!). Então você conta com os usuários da sua classe para usar o try-with-resource ou ligar close()explicitamente.

Claro que posso documentar isso, mas quero forçá-los.

Infelizmente, não existe uma maneira 1 em Java para forçar o programador a fazer a coisa certa. O melhor que você pode esperar é escolher um uso incorreto em um analisador de código estático.


Sobre o tema dos finalizadores. Esse recurso Java possui muito poucos casos de uso bons . Se você contar com os finalizadores para arrumar, terá o problema de que pode levar muito tempo para que a arrumação aconteça. O finalizador será executado somente depois que o GC decidir que o objeto não está mais acessível. Isso pode não acontecer até que a JVM faça uma coleção completa .

Portanto, se o problema que você está tentando resolver é recuperar recursos que precisam ser liberados mais cedo , você perdeu o barco usando a finalização.

E caso você não tenha entendido o que estou dizendo acima ... quase nunca é apropriado usar finalizadores no código de produção, e você nunca deve confiar neles!


1 - Existem maneiras. Se você estiver preparado para "ocultar" os objetos de serviço do código do usuário ou controlar rigidamente seu ciclo de vida (por exemplo, https://softwareengineering.stackexchange.com/a/345100/172 ), o código do usuário não precisará ser chamado release(). No entanto, a API se torna mais complicada, restrita ... e IMO "feia". Além disso, isso não está forçando o programador a fazer a coisa certa . Está removendo a capacidade do programador de fazer a coisa errada!

Stephen C
fonte
11
Os finalizadores ensinam uma lição muito ruim - se você estragar tudo, eu vou consertar isso para você. Eu prefiro a abordagem netty -> um parâmetro de configuração que instrui a fábrica (ByteBuffer) a criar aleatoriamente com probabilidade de X% bytebuffers com um finalizador que imprime o local onde esse buffer foi adquirido (se ainda não tiver sido liberado). Assim, na produção de este valor pode ser definido como 0% - ou seja, sem sobrecarga, e durante os testes & dev para um valor mais elevado como 50% ou mesmo 100%, a fim de detectar as fugas antes de libertar o produto
Svetlin Zarev
11
e lembre-se de que não é garantido que os finalizadores sejam executados, mesmo que a instância do objeto esteja sendo coletada no lixo!
jwenting
3
Vale ressaltar que o Eclipse emite um aviso do compilador se um AutoCloseable não estiver fechado. Eu esperaria que outro Java IDE o fizesse também.
meriton - em greve 28/03
11
@jwenting Em que caso eles não são executados quando o objeto é submetido ao GC? Não consegui encontrar nenhum recurso afirmando ou explicando isso.
Cedric Reichenbach
3
@Voo Você entendeu mal o meu comentário. Você tem duas implementações -> DefaultResourcee FinalizableResourceque estende a primeira e adiciona um finalizador. Na produção, você usa o DefaultResourceque não possui finalizador-> sem custos indiretos. Durante o desenvolvimento e o teste, você configura o aplicativo para uso FinalizableResourceque possui um finalizador e, portanto, adiciona sobrecarga. Durante o desenvolvimento e o teste, isso não é um problema, mas pode ajudar a identificar vazamentos e corrigi-los antes de atingir a produção. No entanto, se ocorrer um vazamento alcances PRD você pode configurar a fábrica para criar X% FinalizableResourcepara id o vazamento
Svetlin Zarev
55

Esse parece ser o caso de uso da AutoCloseableinterface e a try-with-resourcesinstrução introduzida no Java 7. Se você tiver algo como

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

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

seus consumidores podem usá-lo como

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

e não precisa se preocupar em chamar um explícito, releaseindependentemente de seu código gerar uma exceção.


Resposta adicional

Se o que você está realmente procurando é garantir que ninguém se esqueça de usar sua função, você deve fazer algumas coisas

  1. Assegure-se de que todos os construtores de MyServicesejam privados.
  2. Defina um único ponto de entrada a ser usado para MyServicegarantir que ele seja limpo depois.

Você faria algo como

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

Você o usaria como

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

Eu não recomendo esse caminho originalmente porque é hediondo sem lambdas Java-8 e interfaces funcionais (onde MyServiceConsumerfica Consumer<MyService>) e é bastante imponente para seus consumidores. Mas se o que você deseja apenas é que release()deve ser chamado, ele funcionará.

Walpen
fonte
11
Esta resposta é provavelmente melhor que a minha. Meu caminho tem um atraso antes que o coletor de lixo entre em ação.
Dar Brett
11
Como você garante que os clientes usem try-with-resourcese não apenas omitam try, perdendo assim a closechamada?
28417 Frank
@walpen Desta forma, tem o mesmo problema. Como posso forçar o usuário a usar try-with-resources?
hasanghaforian
11
@Frank / @hasanghaforian, Sim, esse caminho não força a possibilidade de ser chamado. Ele tem o benefício da familiaridade: os desenvolvedores de Java sabem que você deve ter certeza de que .close()será chamado quando a classe for implementada AutoCloseable.
walpen
11
Não foi possível emparelhar um finalizador com um usingbloco para finalização determinística? Pelo menos em C #, isso se torna um padrão bastante claro para os desenvolvedores adquirirem o hábito de usar ao aproveitar recursos que precisam de finalização determinística. Algo fácil e a formação de hábitos é a próxima melhor coisa quando você não pode forçar alguém a fazer alguma coisa. stackoverflow.com/a/2016311/84206
AaronLS
52

sim você pode

Você pode absolutamente forçá-los a liberar e torná-lo 100% impossível de esquecer, impossibilitando a criação de novas Serviceinstâncias.

Se eles fizeram algo assim antes:

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

Em seguida, altere-o para que eles possam acessá-lo assim.

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

Ressalvas:

  • Considere esse pseudocódigo, já faz muito tempo desde que escrevi Java.
  • Pode haver variantes mais elegantes hoje em dia, talvez incluindo a AutoCloseableinterface que alguém mencionou; mas o exemplo dado deve funcionar imediatamente e, exceto pela elegância (que é um objetivo legal por si só), não deve haver motivo principal para alterá-lo. Observe que isso pretende dizer que você poderia usar AutoCloseabledentro de sua implementação privada; o benefício da minha solução em relação ao uso de seus usuários AutoCloseableé que ela pode, novamente, não ser esquecida.
  • Você precisará realizá-lo conforme necessário, por exemplo, para injetar argumentos na new Servicechamada.
  • Como mencionado nos comentários, a questão de saber se um construto como este (isto é, assumir o processo de criação e destruição de a Service) pertence à mão do chamador ou não está fora do escopo desta resposta. Mas se você decidir que precisa absolutamente disso, é assim que pode fazê-lo.
  • Um comentarista forneceu essas informações sobre o tratamento de exceções: Google Livros
AnoE
fonte
2
Estou feliz com os comentários do voto negativo, para que eu possa melhorar a resposta.
AnoE
2
Não diminuí o voto, mas esse design provavelmente será mais problemático do que vale a pena. Ele adiciona uma grande complexidade e impõe uma grande carga aos chamadores. Os desenvolvedores devem estar familiarizados o suficiente com as classes de estilo try-with-resources, pois seu uso é uma segunda natureza sempre que uma classe implementa a interface apropriada, de modo que normalmente é bom o suficiente.
Jpmc26 28/03
30
@ jpmc26, engraçado, acho que essa abordagem reduz a complexidade e a carga dos chamadores , impedindo-os de pensar no ciclo de vida dos recursos. Além de que; o OP não perguntou "devo forçar os usuários ...", mas "como forço os usuários". E se for importante o suficiente, esta é uma solução que funciona muito bem, é estruturalmente simples (apenas envolvida em um fechamento / executável), até transferível para outros idiomas que também possuem lambdas / procs / closures. Bem, vou deixar para a democracia do SE julgar; o OP já decidiu de qualquer maneira. ;)
Ano
14
Novamente, @ jpmc26, não estou muito interessado em discutir se essa abordagem está correta para um caso de uso único por aí. O OP pergunta como aplicar uma coisa dessas e essa resposta diz como aplicar uma coisa dessas . Não cabe a nós decidir se é apropriado fazê-lo em primeiro lugar. A técnica mostrada é uma ferramenta, nada mais, nada menos e útil para ter em um saco de ferramentas, acredito.
AnoE
11
Esta é a resposta correta, é essencialmente o padrão do buraco no meio
jk.
11

Você pode tentar usar o padrão de comando .

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

}

Isso fornece controle total sobre a aquisição e liberação de recursos. O chamador envia apenas tarefas ao seu Serviço, e você é responsável por adquirir e limpar recursos.

Há um problema, no entanto. O chamador ainda pode fazer isso:

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

Mas você pode resolver esse problema quando ele surgir. Quando houver problemas de desempenho e você descobrir que alguns chamadores fazem isso, simplesmente mostre a eles a maneira correta e resolva o problema:

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

Você pode tornar isso arbitrariamente complexo implementando promessas para tarefas que dependem de outras tarefas, mas liberar coisas também fica mais complicado. Este é apenas um ponto de partida.

Assíncrono

Como foi apontado nos comentários, o acima não funciona realmente com solicitações assíncronas. Está correto. Mas isso pode ser facilmente resolvido no Java 8 com o uso do CompleteableFuture , especialmente CompleteableFuture#supplyAsyncpara criar futuros individuais e CompleteableFuture#allOfaperfeiçoar a liberação dos recursos quando todas as tarefas forem concluídas. Como alternativa, sempre é possível usar Threads ou ExecutorServices para lançar sua própria implementação de Futuros / Promessas.

Polygnome
fonte
11
Eu apreciaria se os defensores da opinião pública deixassem um comentário por que acham que isso é ruim, para que eu possa resolver essas preocupações diretamente ou, pelo menos, obter outro ponto de vista para o futuro. Obrigado!
Polygnome
Marcado com +1. Essa pode ser uma abordagem, mas o serviço é assíncrono e o consumidor precisa obter o primeiro resultado antes de solicitar o próximo serviço.
hasanghaforian
11
Este é apenas um modelo de barebones. O JS resolve isso com promessas; você pode fazer coisas semelhantes em Java com futuros e um coletor chamado quando todas as tarefas são concluídas e depois limpas. É definitivamente possível obter assíncrono e até mesmo dependências, mas também complica bastante as coisas.
Polygnome
3

Se você puder, não permita que o usuário crie o recurso. Permita que o usuário passe um objeto de visitante para o seu método, o qual criará o recurso, passe para o método do objeto de visitante e libere depois.

9000
fonte
Obrigado pela sua resposta, mas com licença, você quer dizer que é melhor repassar recursos ao consumidor e recuperá-lo novamente quando ele solicitar serviço? O problema continuará: o consumidor pode esquecer de liberar esses recursos.
hasanghaforian
Se você deseja controlar um recurso, não o solte, não o repasse completamente para o fluxo de execução de outra pessoa. Uma maneira de fazer isso é executar um método predefinido do objeto de visitante de um usuário. AutoCloseablefaz uma coisa muito semelhante nos bastidores. Sob outro ângulo, é semelhante à injeção de dependência .
9000
1

Minha ideia foi semelhante à da Polygnome.

Você pode ter os métodos "do_something ()" em sua classe, apenas adicionando comandos a uma lista de comandos privada. Então, tenha um método "commit ()" que realmente faça o trabalho e chame "release ()".

Portanto, se o usuário nunca chamar commit (), o trabalho nunca será concluído. Se o fizerem, os recursos são liberados.

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

}

Você pode usá-lo como foo.doSomething.doSomethineElse (). Commit ();

Sava B.
fonte
Essa é a mesma solução, mas, em vez de pedir ao chamador para manter a lista de tarefas, você a mantém internamente. O conceito central é literalmente o mesmo. Mas isso não segue nenhuma das convenções de codificação para Java que eu já vi e é realmente muito difícil de ler (corpo do loop na mesma linha do cabeçalho do loop, _ no início).
Polygnome
Sim, é o padrão de comando. Acabei de escrever um código para explicar o que eu estava pensando da maneira mais concisa possível. Atualmente, escrevo C # atualmente, onde variáveis ​​privadas geralmente são prefixadas com _.
Sava B.
-1

Outra alternativa às mencionadas seria o uso de "referências inteligentes" para gerenciar seus recursos encapsulados de uma maneira que permita ao coletor de lixo limpar automaticamente sem liberar recursos manualmente. Sua utilidade depende, é claro, da sua implementação. Leia mais sobre este tópico nesta maravilhosa postagem no blog: https://community.oracle.com/blogs/enicholas/2006/05/04/understanding-weak-references

Dracam
fonte
Essa é uma ideia interessante, mas não vejo como o uso de referências fracas resolve o problema do OP. A classe de serviço não sabe se receberá outra solicitação doWork (). Se liberar sua referência a seus objetos pesados ​​e receber outra chamada doWork (), falhará.
Jay Elston
-4

Para qualquer outra linguagem orientada a classe, eu diria que o colocaria no destruidor, mas como essa não é uma opção, acho que você pode substituir o método finalize. Dessa forma, quando a instância sair do escopo e for coletada como lixo, ela será liberada.

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

Não estou muito familiarizado com Java, mas acho que esse é o objetivo para o qual finalize () se destina.

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

Dar Brett
fonte
7
Esta é uma má solução para o problema para a razão Stephen diz em sua resposta -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
E mesmo assim o finalizador não pode, de facto, correr ...
jwenting
3
Os finalizadores são notoriamente não confiáveis: eles podem executar, eles podem nunca executar, se eles executam, não há garantia de quando eles serão executados. Até os arquitetos de Java, como Josh Bloch, dizem que os finalizadores são uma péssima idéia (referência: Java eficaz (2ª edição) ).
Este tipo de questões me faça sentir falta C ++ com sua destruição determinista e RAII ...
Mark K Cowan