O modo Observador, também conhecido como Modo de Publicação/Subscrição, foi proposto pelo grupo de quatro pessoas (Gof, como Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides) no "Padrão de design de 1994: o básico do Software Orientado a Objetos Reutilizáveis" (consulte as páginas 293-313 no livro para detalhes). Embora esse padrão tenha um histórico considerável, ele ainda é amplamente aplicável a uma variedade de cenários e até se tornou parte integrante da biblioteca Java padrão. Embora já existam muitos artigos sobre padrões de observador, todos se concentram na implementação em Java, mas ignoram os vários problemas encontrados pelos desenvolvedores ao usar padrões de observador em Java.
A intenção original de escrever este artigo é preencher essa lacuna: este artigo apresenta principalmente a implementação do padrão de observador usando a arquitetura Java8 e explora ainda mais questões complexas sobre padrões clássicos nessa base, incluindo classes internas anônimas, expressões de lambda, segurança de threads e implementação de observadores que consumidores no tempo não assumido. Embora o conteúdo deste artigo não seja abrangente, muitas das questões complexas envolvidas neste modelo não podem ser explicadas em apenas um artigo. Mas depois de ler este artigo, os leitores podem entender qual é o padrão do observador, sua universalidade em Java e como lidar com alguns problemas comuns ao implementar o padrão de observador em Java.
Modo de observador
De acordo com a definição clássica proposta pelo GOF, o tema do padrão do observador é:
Define uma dependência um para muitos entre objetos. Quando o estado de um objeto muda, todos os objetos que dependem dele são notificados e atualizados automaticamente.
O que isso significa? Em muitos aplicativos de software, os estados entre objetos são interdependentes. Por exemplo, se um aplicativo se concentrar no processamento numérico de dados, esses dados poderão ser exibidos através de tabelas ou gráficos da interface gráfica do usuário (GUI) ou usados ao mesmo tempo, ou seja, quando os dados subjacentes são atualizados, os componentes da GUI correspondentes também devem ser atualizados. A chave para o problema é como atualizar os dados subjacentes quando os componentes da GUI são atualizados e, ao mesmo tempo, minimize o acoplamento entre os componentes da GUI e os dados subjacentes.
Uma solução simples e não escalável é referir-se à tabela e aos componentes da GUI da imagem dos objetos que gerenciam esses dados subjacentes, para que os objetos possam notificar os componentes da GUI quando os dados subjacentes mudarem. Obviamente, essa solução simples mostrou rapidamente suas deficiências para aplicações complexas que lidam com mais componentes da GUI. Por exemplo, existem 20 componentes da GUI que dependem de dados subjacentes; portanto, os objetos que gerenciam os dados subjacentes precisam manter as referências a esses 20 componentes. À medida que o número de objetos dependentes dos dados relacionados aumenta, o grau de acoplamento entre gerenciamento de dados e objetos se torna difícil de controlar.
Outra solução melhor é permitir que os objetos se registrem para obter permissões para atualizar os dados de interesse, que o gerenciador de dados notificará esses objetos quando os dados mudarem. Nos termos do leigo, deixe o objeto de interesse de dados informar ao gerente: "Notifique -me quando os dados mudarem". Além disso, esses objetos podem não apenas se registrar para obter notificações de atualização, mas também cancelar o registro para garantir que o gerenciador de dados não notifique mais o objeto quando os dados mudarem. Na definição original de GOF, o objeto registrado para obter atualizações é chamado de "Observador", o gerenciador de dados correspondente é chamado de "sujeito", os dados em que o observador está interessado é chamado de "Estado -alvo", o processo de registro é chamado de "add" e o processo de desfazer a observação é chamado de "destacar". Como mencionado acima, o modo Observer também é chamado de Modo Publish-Subscribe. Pode -se entender que um cliente assina o observador sobre o destino. Quando o status do destino é atualizado, o destino publica essas atualizações no assinante (esse padrão de design é estendido a uma arquitetura geral, chamada de arquitetura de publicação de inscrição). Esses conceitos podem ser representados pelo diagrama de classe a seguir:
O ConcereteObServer o usa para receber alterações de atualização do estado e passar uma referência ao concreeteSubject ao seu construtor. Isso fornece uma referência a um assunto específico para um observador específico, a partir do qual as atualizações podem ser obtidas quando o estado mudar. Simplificando, o observador específico será instruído a atualizar o tópico e, ao mesmo tempo, usar as referências em seu construtor para obter o estado do tópico específico e, finalmente, armazenar esses objetos de estado de pesquisa sob a propriedade Observerstate do observador específico. Este processo é mostrado no seguinte diagrama de sequência:
Professional de modelos clássicos
Embora o modelo de observador seja universal, existem muitos modelos especializados, os mais comuns dos quais são os dois a seguir:
Fornece um parâmetro para o objeto de estado, passado para o método de atualização chamado pelo Observer. No modo clássico, quando o observador é notificado de que o estado do assunto mudou, seu estado atualizado será obtido diretamente do assunto. Isso exige que o observador salve uma referência de objeto ao estado recuperado. Isso forma uma referência circular, a referência do concreto -subjacente aponta para sua lista de observadores e a referência do concreto -server aponta para o concreto subjecto que pode obter o estado de sujeito. Além de obter o estado atualizado, não há conexão entre o observador e o assunto que ele se registra para ouvir. O observador se preocupa com o objeto de estado, não com o próprio sujeito. Ou seja, em muitos casos, concreteobserver e concreto -subjacto estão ligados à força. Pelo contrário, quando o concreto -subjacente chama a função de atualização, o objeto de estado é passado para o concreto -servidor e os dois não precisam ser associados. A associação entre concreto -servidor e objeto de estado reduz o grau de dependência entre o observador e o estado (consulte o artigo de Martin Fowler para obter mais diferenças em associação e dependência).
Mesclar a classe abstrata do sujeito e o concreta subjacente em uma classe singlelesbject. Na maioria dos casos, o uso de classes abstratas no sujeito não melhora a flexibilidade e a escalabilidade do programa, portanto, a combinação dessa classe abstrata e da classe concreta simplifica o design.
Depois que esses dois modelos especializados são combinados, o diagrama de classes simplificado é o seguinte:
Nesses modelos especializados, a estrutura de classe estática é bastante simplificada e as interações entre classes também são simplificadas. O diagrama de sequência neste momento é o seguinte:
Outra característica do modo de especialização é a remoção da variável de membro observada do concreto -server. Às vezes, o observador específico não precisa salvar o estado mais recente do assunto, mas só precisa monitorar o status do sujeito quando o status é atualizado. Por exemplo, se o observador atualizar o valor da variável de membro para a saída padrão, ele poderá excluir o Observerstate, que remove a associação entre o concretoerver e a classe estadual.
Regras de nomeação mais comuns
O modelo clássico e até o modelo profissional mencionado acima usam termos como anexar, destacar e observador, enquanto muitas implementações de Java usam dicionários diferentes, incluindo registro, não registro, ouvinte etc. Vale mencionar que o estado é um termo geral para todos os objetos que o ouvinte precisa monitorar as alterações. O nome específico do objeto de estado depende do cenário usado no modo Observer. Por exemplo, no modo Observador na cena em que o ouvinte ouve a ocorrência de eventos, o ouvinte registrado receberá uma notificação quando o evento ocorrer. O objeto de status neste momento é o evento, ou seja, se o evento ocorreu.
Em aplicações reais, a nomeação de metas raramente inclui um assunto. Por exemplo, crie um aplicativo sobre um zoológico, registre vários ouvintes para observar a aula do zoológico e receber notificações quando novos animais entrarem no zoológico. O objetivo neste caso é a aula do zoológico. Para manter a terminologia consistente com o domínio do problema, o termo "sujeito" não será usado, o que significa que a classe do zoológico não será nomeada zoosubject.
A nomeação do ouvinte é geralmente seguida pelo sufixo do ouvinte. Por exemplo, o ouvinte mencionado acima para monitorar novos animais será nomeado AnimalAddedListener. Da mesma forma, a nomeação de funções como Registro, Registro e Notificação é frequentemente sufixo por seus nomes de ouvintes correspondentes. Por exemplo, o Registro, o Registro e a Notificação das Funções do AnimalDedListener será nomeado RegisteranimaladdedListener, UN -RecisterAnimaladdedListener e NotifyAnimaladdlisterners. Deve -se notar que o nome da função Notify é usado, porque a função Notify lida com vários ouvintes em vez de um único ouvinte.
Esse método de nomeação parecerá demorado e, geralmente, um sujeito registrará vários tipos de ouvintes. Por exemplo, no exemplo do zoológico mencionado acima, no zoológico, além de registrar novos ouvintes para monitorar animais, ele também precisa registrar um ouvinte de animais para reduzir os ouvintes. No momento, haverá duas funções de registro: (RegisteranImalAddedListener e RegisteranimalRemovedListener. Dessa forma, o tipo do ouvinte é usado como um qualificador para indicar o tipo de observador. Outra solução é criar uma função de registro e depois sobrecarregar, mas a solução 1 pode saber que é mais conveniente que é o que o ouvinte é.
Outra sintaxe idiomática é usar no prefixo em vez de atualizar, por exemplo, a função de atualização é nomeada onanimaladded, em vez de atualizar Essa situação é mais comum quando o ouvinte recebe notificações para uma sequência, como adicionar um animal à lista, mas raramente é usado para atualizar dados separados, como o nome do animal.
Em seguida, este artigo usará as regras simbólicas de Java. Embora as regras simbólicas não alterem o design e a implementação reais do sistema, é um importante princípio de desenvolvimento usar termos com os quais outros desenvolvedores estão familiarizados; portanto, você deve estar familiarizado com as regras simbólicas do padrão do observador em Java descritas acima. O conceito acima será explicado abaixo usando um exemplo simples no ambiente Java 8.
Um exemplo simples
É também o exemplo do zoológico mencionado acima. Usando a interface da API do Java8 para implementar um sistema simples, explicando os princípios básicos do padrão do observador. O problema é descrito como:
Crie um zoológico do sistema, permitindo que os usuários escutem e desfigurem o estado de adicionar novos animais de objeto e crie um ouvinte específico, responsável por gerar o nome do novo animal.
De acordo com o aprendizado anterior do padrão de observador, sabemos que, para implementar esse aplicativo, precisamos criar 4 classes, especificamente:
Classe do zoológico: ou seja, o tema do padrão, responsável por armazenar todos os animais no zoológico e notificar todos os ouvintes registrados quando novos animais se juntarem.
Classe de animais: representa um objeto animal.
Classe AnimalAddedListener: isto é, interface do observador.
PrintNameanImalAddedListener: A classe Observer específica é responsável por produzir o nome do animal recém -adicionado.
Primeiro, criamos uma classe de animais, que é um objeto Java simples que contém variáveis de membros, construtores, getters e métodos de setter. O código é o seguinte:
public class Animal {private String Name; público animal (nome da string) {this.name = name;} public string getName () {return this.name;} public void setName (string name) {this.name = name;}}Use esta classe para representar objetos animais e, em seguida, você pode criar a interface AnimalAddedListener:
interface pública AnimalAddedListener {public void onanimaladded (animal animal);}As duas primeiras classes são muito simples, então não as apresentarei em detalhes. Em seguida, crie a aula do zoológico:
classe pública zoológico {Lista privada <iminal> animais = novo ArrayList <> (); lista privada <iminaladdedListener> ouvintes = new ArrayList <> (); public void addanimal (animal animal) {// Adicione a lista dos escutadores registrados. RegisteranImalAddedListener (ouvinte AnimalAddedListener) {// Adicione o ouvinte à lista de ouvintes registrados ,.listeners.add (ouvinte);} public void unregisterAnimaladdedListener (ListersMis.Listers. notifyanimaladdedlisteners (animal animal) {// notifique cada um dos ouvintes na lista de ouvintes registrados, ouvintes.Essa analogia é complexa que os dois anteriores. Ele contém duas listas, uma é usada para armazenar todos os animais no zoológico e o outro é usado para armazenar todos os ouvintes. Dado que os objetos armazenados em coleções de animais e ouvintes são simples, este artigo escolheu a Arraylist para armazenamento. A estrutura de dados específica do ouvinte armazenado depende do problema. Por exemplo, para o problema do zoológico aqui, se o ouvinte tiver prioridade, você deve escolher outra estrutura de dados ou reescrever o algoritmo de registro do ouvinte.
A implementação de registro e remoção é um método de delegado simples: cada ouvinte é adicionado ou removido da lista de audição do ouvinte como um parâmetro. A implementação da função Notify está ligeiramente desativada do formato padrão do padrão do observador. Inclui o parâmetro de entrada: o animal recém -adicionado, para que a função Notify possa passar na referência de animais recém -adicionada ao ouvinte. Use a função foreach da API dos fluxos para atravessar os ouvintes e executar a função Theonanimaladded em cada ouvinte.
Na função Addanimal, o recém -adicionado objeto e ouvinte são adicionados à lista correspondente. Se a complexidade do processo de notificação não for levada em consideração, essa lógica deve ser incluída em um método de chamada conveniente. Você só precisa passar em uma referência ao objeto animal recém -adicionado. É por isso que a implementação lógica do ouvinte de notificação é encapsulada na função NotifyAnimalAddedListeners, que também é mencionada na implementação do Addanimal.
Além das questões lógicas das funções notificadas, é necessário enfatizar a questão controversa na visibilidade de notificar as funções. No modelo de observador clássico, como o Gof disse na página 301 dos padrões de design de livros, a função Notify é pública, mas, embora seja usada no padrão clássico, isso não significa que deve ser público. A seleção da visibilidade deve ser baseada no aplicativo. Por exemplo, no exemplo do zoológico deste artigo, a função Notify é do tipo protegido e não exige que cada objeto inicie uma notificação de um observador registrado. Ele só precisa garantir que o objeto possa herdar a função da classe pai. Claro, esse não é exatamente o caso. É necessário descobrir quais classes podem ativar a função Notificar e, em seguida, determinar a visibilidade da função.
Em seguida, você precisa implementar a classe PrintNeanImalAddedListener. Esta classe usa o método System.out.println para produzir o nome do novo animal. O código específico é o seguinte:
classe pública PrintnameanImaladdedListener implementa AnimalAddedListener {@OverridePublic void updateanimaladded (animal animal) {// imprima o nome do recém -adicionado Animalsystem.out.println ("adicionou um novo animal com o nome '" + animal.getname () + "'");}}}}Finalmente, precisamos implementar a principal função que impulsiona o aplicativo:
public class Main {public static void main (string [] args) {// Crie o zoológico para armazenar AnimalsZoo Zoo = new Zoo (); // Registre um ouvinte a ser notificado quando um animal é adicionado Zoo.registerAnimalAddedListener (Novo PrintnameanImaladListener ()); Animal ("tigre"));}}A função principal simplesmente cria um objeto de zoológico, registra um ouvinte que gera o nome do animal e cria um novo objeto animal para acionar o ouvinte registrado. A saída final é:
Adicionado um novo animal com nome 'tigre'
Adicionado ouvinte
As vantagens do modo Observador são totalmente exibidas quando o ouvinte é restabelecido e adicionado ao assunto. Por exemplo, se você deseja adicionar um ouvinte que calcula o número total de animais em um zoológico, basta criar uma classe de ouvinte específica e registrá -lo na classe do zoológico sem nenhuma modificação na classe do zoológico. Adicionar o ou ouvinte contando o código de listener é o seguinte:
public classe continginganimaladdedlistener implementa AnimalDedListener {private estático intAddedCount = 0; @OverridePublic void updateanimaladded (animal animal) {// incrementam o número de animais de animais.A função principal modificada é a seguinte:
classe pública principal {public static void main (string [] args) {// Crie o zoológico para armazenar AnimalsZoo Zoo = new Zoo (); // Registre os ouvintes a serem notificados quando um animal é adicionado (), zooAnimalAdlistner (New PrintniAnimaladlistener ()); notifique os ouvintes registrados.addanimal (novo animal ("tigre")); zoo.addanimal (novo animal ("leão")); zoo.addanimal (novo animal ("urso");}}}O resultado da saída é:
Adicionado um novo animal com nome 'Tiger' Total Animals Adicionado: 1 Adicionado um novo animal com nome 'leão' Total Animals Adicionado: 2 Adicionado um novo animal com nome 'urso' animais totais adicionados: 3
O usuário pode criar qualquer ouvinte se apenas modificar o código de registro do ouvinte. Essa escalabilidade ocorre principalmente porque o sujeito está associado à interface do observador, em vez de diretamente associada ao concreto -servidor. Enquanto a interface não for modificada, não há necessidade de modificar o assunto da interface.
Classes internas anônimas, funções lambda e registro de ouvinte
Uma grande melhoria no Java 8 é a adição de recursos funcionais, como a adição de funções lambda. Antes de apresentar a função Lambda, o Java forneceu funções semelhantes por meio de classes internas anônimas, que ainda são usadas em muitos aplicativos existentes. No modo Observer, um novo ouvinte pode ser criado a qualquer momento sem criar uma classe observadora específica. Por exemplo, a classe PrintNameanImalAddedListener pode ser implementada na função principal com classe interna anônima. O código de implementação específico é o seguinte:
public class main {public static void main (string [] args) {// Crie o zoológico para armazenar AnimalsZoo Zoo = new Zoo (); // Registre os ouvintes a serem notificados quando um animal é adicionado Zoo.registerAnimaladdedListner (New AnimalAddedListener () @OverrideOpublic theadTeanAdenAdded (AnimalAdListener () @OverrideOpublic the Atualizador Animalsystem.out.println ("adicionou um novo animal com o nome '" + animal.getName () + "'");}}); // Adicione um animal notifique os ouvintes registrados.addanimal (novo animal ("tigre");}}}Da mesma forma, as funções lambda também podem ser usadas para concluir essas tarefas:
classe pública main {public static void main (string [] args) {// Crie o zoológico para armazenar AnimalsZoo Zoo = new Zoo (); // Registre os ouvintes a serem notificados quando um animal é adicionado zoo.registerAnimalAddedListner ((animal) -> System.out.println ("Adicionado ANIMEN ENNIMENTO '; ouvinteszoo.addanimal (novo animal ("tigre"));}}Deve -se notar que a função Lambda é adequada apenas para situações em que existe apenas uma função na interface do ouvinte. Embora esse requisito pareça rigoroso, muitos ouvintes são realmente funções únicas, como o Listener Animaladdedded no exemplo. Se a interface tiver várias funções, você poderá optar por usar classes internas anônimas.
Existe um problema com o registro implícito do ouvinte criado: como o objeto é criado dentro do escopo da chamada de registro, é impossível armazenar uma referência a um ouvinte específico. Isso significa que os ouvintes registrados através de funções Lambda ou classes internas anônimas não podem ser revogadas porque as funções de revogação exigem uma referência ao ouvinte registrado. Uma maneira fácil de resolver esse problema é retornar uma referência ao ouvinte registrado na função RegisteranimaladdedListener. Dessa forma, você pode registrar o ouvinte criado com funções Lambda ou classes internas anônimas. O código de método aprimorado é o seguinte:
Public AnimalAddedListener RegisteranImalAddedListener (Listener AnimalAddedListener) {// Adicione o ouvinte à lista de ouvintes registrados. devolver ouvinte;}O código do cliente para a interação da função reprojetada é a seguinte:
public class Main {public static void main (string [] args) {// Crie o zoológico para armazenar AnimalsZoo Zoo = new Zoo (); // Registre os ouvintes a serem notificados quando um animal é adicionado, com um listener (norma) -> system.out.println ("adicionado um novo (animal) -> system.out.println (" adicionado um novo (animal) -> system.out.println (" "'")); // Adicione um animal notifique os ouvintes registrados.addanimal (novo animal ("tigre")); // Registre o ouvinte.A saída de resultado neste momento é adicionada apenas um novo animal com o nome 'Tiger', porque o ouvinte foi cancelado antes que o segundo animal seja adicionado:
Adicionado um novo animal com nome 'tigre'
Se uma solução mais complexa for adotada, a função de registro também poderá retornar a classe receptor para que o ouvinte não registrador seja chamado, por exemplo:
public class AnimalAddedListenErreCeipt {Private Final AnimalAddedListener Listaner; Public AnimalAddedListenErreCeipt (Listener AnimalAddedListener) {this.Listener = ouvinte;} public final AnimalAddedListener getListener () {return this.listener;}}}}}O recibo será usado como o valor de retorno da função de registro e os parâmetros de entrada da função de registro são cancelados. Neste momento, a implementação do zoológico é a seguinte:
public class ZoousingReceipt {// ... Atributos e construtor existentes ... Public AnimalAddedListenErreCeipt RegisteranImalAddedListener (Listener AnimalAddedListener) {// Adicione o ouvinte a lista de ouvintes registrados. UN -REGISTERANIMALADDORDListener (Recepção AnimalAddedListenErreCeipt) {// Remova o ouvinte da lista dos ouvintes registrados.O mecanismo de implementação de recebimento descrito acima permite o armazenamento de informações para chamada para o ouvinte ao revogar, ou seja, se o algoritmo de registro de revogação depende do status do ouvinte quando o sujeito registrar o ouvinte, esse status será salvo. Se o registro de revogação exigir apenas uma referência ao ouvinte registrado anterior, a tecnologia de recepção parecerá problemática e não será recomendada.
Além de ouvintes específicos particularmente complexos, a maneira mais comum de registrar os ouvintes é através de funções Lambda ou através de classes internas anônimas. Obviamente, existem exceções, ou seja, a classe que contém o assunto implementa a interface do observador e registra um ouvinte que chama o destino de referência. O caso como mostrado no código a seguir:
classe pública zoocontainer implementa AnimalAddedListener {zoológico privado zoo = new Zoo (); public zoocontainer () {// registre este objeto como um ouvinte. {System.out.println ("Adicionado animal com nome '" + animal.getName () + "'");} public static void main (string [] args) {// crie o recipiente do zoológico (add a adicam um animal no interior. Animal ("tigre"));}}Essa abordagem é adequada apenas para casos simples e o código não parece profissional o suficiente, e ainda é muito popular entre os modernos desenvolvedores de Java, por isso é necessário entender como esse exemplo funciona. Como o Zoocontainer implementa a interface AnimalAddedListener, uma instância (ou objeto) do zoocontainer pode ser registrada como um listener de animais. Na classe Zoocontainer, essa referência representa uma instância do objeto atual, a saber, Zoocontainer, e pode ser usada como um listener de animais.
Geralmente, nem todas as classes de contêiner são necessárias para implementar essas funções, e a classe de contêiner que implementa a interface do ouvinte pode chamar apenas a função de registro de assunto, mas simplesmente passar a referência à função de registro como objeto do ouvinte. Nos capítulos seguintes, as perguntas frequentes e soluções para ambientes multithread serão introduzidos.
O OneApm fornece soluções de desempenho de aplicativos Java de ponta a ponta. Suportamos todas as estruturas Java e servidores de aplicativos comuns para ajudá -lo a descobrir rapidamente gargalos do sistema e localizar as causas raiz das anormalidades. A implantação em níveis de minuto e a experiência instantaneamente, o monitoramento Java nunca foi tão fácil. Para ler mais artigos técnicos, visite o blog oficial de tecnologia da OneApm.
O conteúdo acima apresenta o conteúdo relevante do uso do Java 8 para implementar o modo Observer (parte 1). O próximo artigo apresenta o método de usar o Java 8 para implementar o modo Observer (parte 2). Amigos interessados continuarão aprendendo, esperando que seja útil para todos!