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:
Especialização de modelos clássicos <Br />, embora o modelo de observador seja universal, também existem muitos modelos especializados, os mais comuns dos dois seguintes:
1. Forneça um parâmetro para o objeto de estado e passe -o 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).
2. 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 <Br /> Modos clássicos e até o modo 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 a pena mencionar que o estado é um termo geral para todos os objetos que o ouvinte precisa monitorar mudanças. 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:
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:
classe pública Animal {private String Name; public animal (nome da string) {this.name = name; } public string getName () {return this.name; } public void setName (nome da string) {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 Zoo {Private List <Armen> Animals = new ArrayList <> (); Lista privada <iminalAddedListener> ouvintes = new ArrayList <> (); public void addanimal (animal animal) {// Adicione o animal à lista de animais this.animals.add (animal); // notifique a lista de ouvintes registrados this.notifyanimaladdedlisterners (animal); } public void RegisterAnimalAddedListener (Listener AnimalAddedListener) {// Adicione o ouvinte à lista de ouvintes registrados this.listeners.add (ouvinte); } public void un -registerAnimalAddedListener (Listener AnimalAddedListener) {// Remova o ouvinte da lista dos ouvintes registrados this.listeners.remove (ouvinte); } Void protegido NotifyAnimalAddedListeners (animal animal) {// notifique cada um dos ouvintes na lista de ouvintes registrados this.listeners.foreach (ouvinte -> ouvinte.UpDateanimaladded (Animal)); }}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 {@Override public void updateanimaladded (animal animal) {// imprima o nome do recém -adicionado sistema animal.out.println ("adicionou um novo animal com nome '" + animal.getname () + "'"); }}Finalmente, precisamos implementar a principal função que impulsiona o aplicativo:
classe pública principal {public static void main (string [] args) {// Crie o zoológico para armazenar animais zoológicos = new Zoo (); // Registre um ouvinte a ser notificado quando um animal é adicionado zoológico. // Adicione um animal notifique os ouvintes registrados Zoo.addanimal (New Animal ("Tiger")); }}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:
classe pública CountingAnimaladdedListener implementa AnimalAddedListener {private static int AnimalDedCount = 0; @Override Public void updateanimaladded (animal animal) {// incremento o número de animais de animais quedcount ++; // Imprima o número de animais de animais.out.println ("Total Animals acrescentou:" + AnimalDedCount); }}A função principal modificada é a seguinte:
classe pública principal {public static void main (string [] args) {// Crie o zoológico para armazenar animais zoológicos = new Zoo (); // Registre os ouvintes a serem notificados quando um animal é adicionado zoológico.RegisterAnimalAddedListener (novo PrintNameanImalAddedListener ()); zoo.ReGisterAnimalAddedListener (novo continganimalAddedListener ()); // Adicione um animal notifique os ouvintes registrados Zoo.addanimal (New Animal ("Tiger")); 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 Add um novo animal com o nome' Lion'Total Animals Adicionado: 2Ad um novo animal com nome 'Bear'Total Animals Adicionado: 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:
classe pública principal {public static void main (string [] args) {// Crie o zoológico para armazenar animais zoológicos = new Zoo (); // Registre os ouvintes a serem notificados quando um animal é adicionado zoológico.ReGisterAnimalAddedListener (New AnimalAddedListener () {@Override public void updateanimaladded (animal animal) {// imprima o nome do sistema recém -adicionado () ("); // Adicione um animal notifique os ouvintes registrados Zoo.addanimal (New Animal ("Tiger")); }}Da mesma forma, as funções lambda também podem ser usadas para concluir essas tarefas:
classe pública principal {public static void main (string [] args) {// Crie o zoológico para armazenar animais zoológicos = new Zoo (); // Registre os ouvintes a serem notificados quando um animal é adicionado zoológico.registerAnimaladdedListener (((animal) -> System.out.println ("Adicionado um novo animal com nome '" + animal.getName () + "'")); // Adicione um animal notifique os ouvintes registrados Zoo.addanimal (New Animal ("Tiger")); }}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 this.listeners.add (ouvinte); devolver ouvinte;}O código do cliente para a interação da função reprojetada é a seguinte:
classe pública principal {public static void main (string [] args) {// Crie o zoológico para armazenar animais zoológicos = new Zoo (); // Registre os ouvintes a serem notificados quando um animal é adicionado AnimalAddedListener ouvinte = zoo.registerAnimaladdedListener ((animal) -> System.out.println ("Adicionado um novo animal com nome '" + animal.getName () + "'")); // Adicione um animal notifique os ouvintes registrados Zoo.addanimal (New Animal ("Tiger")); // não registrar o ouvinte Zoo.unregisterAnimalAddedListener (ouvinte); // Adicione outro animal, que não imprimirá o nome, pois o ouvinte // foi anteriormente não registrado zoológico.addanimal (New Animal ("Lion")); }}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:
classe pública AnimalAddedListenErreCeipt {Private Final AnimalAddedListener ouvinte; Public AnimalAddedListenErreCeipt (ouvinte 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:
classe pública zoousingreceipt {// ... atributos e construtor existentes ... Public AnimalAddedListenErreCeipt RegisteranImalAddedListener (Listener AnimalAddedListener) {// Adicione o ouvinte à lista de ouvintes registrados this.Listeners.add (ouvinte); devolver o novo animalAddedListenErreCeipt (ouvinte); } public void un -registerAnimalAddedListener (Recepção AnimalAddedListenErreCeipt) {// Remova o ouvinte da lista dos ouvintes registrados this.Listeners.Remove (Receipt.getListener ()); } // ... Método de notificação existente ...}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 {Private Zoo Zoo = new Zoo (); public zoocontainer () {// Registre este objeto como um ouvinte this.zoo.registerAnimalAddedListener (this); } public zoo getZoo () {return this.zoo; } @Override public void updateanimaladded (animal animal) {System.out.println ("Adicionado animal com nome '" + animal.getName () + "'"); } public static void main (string [] args) {// crie o zoo contêiner zoocontainer zoocontainer = new zoocontainer (); // Adicione um animal notificar o ouvinte interior notificado zoocontainer.getzoo (). Addanimal (novo 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.
Implementação da segurança do thread <br /> O capítulo anterior apresenta a implementação do padrão de observador no ambiente java moderno. Embora seja simples, mas completo, essa implementação ignora uma questão -chave: segurança do thread. A maioria dos aplicativos Java abertos é multithread e o modo Observer é usado principalmente em sistemas multithread ou assíncronos. Por exemplo, se um serviço externo atualizar seu banco de dados, o aplicativo também receberá uma mensagem de forma assíncrona e notificará o componente interno a ser atualizado no modo Observador, em vez de registrar e ouvir diretamente o serviço externo.
A segurança do encadeamento no modo de observador está focada principalmente no corpo do modo, porque é provável que ocorram conflitos de encadeamento ao modificar a coleção de ouvintes registrados. Por exemplo, um thread tenta adicionar um novo ouvinte, enquanto o outro thread tenta adicionar um novo objeto animal, que desencadeia notificações a todos os ouvintes registrados. Dada a ordem da sequência, o primeiro thread pode ou não ter concluído o registro do novo ouvinte antes que o ouvinte registrado receba notificação do animal adicionado. Este é um caso clássico de concorrência de recursos de threads, e é esse fenômeno que diz aos desenvolvedores que eles precisam de um mecanismo para garantir a segurança do thread.
A solução mais fácil para esse problema é: todas as operações que acessam ou modificam a lista de ouvintes de registro devem seguir o mecanismo de sincronização Java, como:
public sincronizado AnimalAddedListener RegisteranimalAddedListener (ouvinte AnimalAddedListener) {/*...*/} public sincronizado void un -registerAnimaladdedListener (lister animalDlistener {/*...*/} aninhanizado ivid / NotifedListener.Dessa maneira, ao mesmo tempo, apenas um thread pode modificar ou acessar a lista de ouvintes registrados, que pode evitar com sucesso problemas de concorrência de recursos, mas surgem novos problemas, e essas restrições são muito rigorosas (para obter mais informações sobre palavras -chave sincronizadas e modelos de concorrência de Java, consulte a página oficial). Através da sincronização do método, o acesso simultâneo à lista de ouvintes pode ser observado o tempo todo. Registrar e revogar o ouvinte é uma operação de gravação para a lista de ouvintes, enquanto notifica o ouvinte para acessar a lista de ouvintes é uma operação somente leitura. Como o acesso através da notificação é uma operação de leitura, várias operações de notificação podem ser executadas simultaneamente.
Portanto, desde que não haja registro ou revogação do ouvinte, desde que o registro não seja registrado, desde que qualquer número de notificações simultâneas possa ser executado simultaneamente sem acionar a competição de recursos para a lista de ouvintes registrados. Obviamente, a concorrência de recursos em outras situações existe há muito tempo. Para resolver esse problema, o bloqueio de recursos do ReadWritelock foi projetado para gerenciar operações de leitura e gravação separadamente. O código de implementação do ThreadSafeZoo, seguro para threadsafezoo, é o seguinte:
classe pública threadSafeZoo {private final readWritElock readWritelock = new ReentrantreadWritelock (); Readlock de bloqueio final protegido = readWritelock.readlock (); WriteLock de bloqueio final protegido = readWritelock.Writelock (); Lista privada <Armal> Animals = New ArrayList <> (); Lista privada <iminalAddedListener> ouvintes = new ArrayList <> (); public void addanimal (animal animal) {// Adicione o animal à lista de animais this.animals.add (animal); // notifique a lista de ouvintes registrados this.notifyanimaladdedlisterners (animal); } public AnimalAddedListener RegisteranImalAddedListener (Listener AnimalAddedListener) {// bloqueia a lista de ouvintes para escrever this.writelock.lock (); tente {// Adicione o ouvinte à lista de ouvintes registrados this.listeners.add (ouvinte); } finalmente {// desbloqueia o escritor bloqueia this.writelock.unlock (); } retornar ouvinte; } public void un -registerAnimalAddedListener (Listener AnimalAddedListener) {// bloqueia a lista de ouvintes para escrever this.writelock.lock (); tente {// remova o ouvinte da lista dos ouvintes registrados this.listeners.remove (ouvinte); } finalmente {// desbloqueia o escritor bloqueia this.writelock.unlock (); }} public void notifyanimaladdedListenners (animal animal) {// bloqueia a lista de ouvintes para ler this.readlock.lock (); tente {// notifique cada um dos ouvintes na lista de ouvintes registrados this.listeners.foreach (ouvinte -> ouvinte.updateanimaladded (animal)); } finalmente {// desbloqueia o leitor bloqueia this.readlock.unlock (); }}}Por meio dessa implantação, a implementação do assunto pode garantir que a segurança dos threads e vários threads possam emitir notificações ao mesmo tempo. Mas, apesar disso, ainda existem dois problemas de concorrência de recursos que não podem ser ignorados:
Acesso simultâneo a cada ouvinte. Vários tópicos podem notificar o ouvinte de que novos animais são necessários, o que significa que um ouvinte pode ser chamado por vários threads ao mesmo tempo.
Acesso simultâneo à lista de animais. Vários tópicos podem adicionar objetos à lista de animais ao mesmo tempo. Se a ordem das notificações tiver um impacto, pode levar à concorrência de recursos, que requer um mecanismo de processamento de operação simultâneo para evitar esse problema. Se a lista de ouvintes registrados receber uma notificação para adicionar Animal2 e depois receber uma notificação para adicionar Animal1, a concorrência de recursos ocorrerá. No entanto, se a adição de Animal1 e Animal2 for realizada por diferentes fios, também é possível concluir a adição de Animal1 antes do Animal2. Especificamente, o Thread 1 adiciona Animal1 antes de notificar o ouvinte e trava o módulo, o Thread 2 adiciona Animal2 e notifica o ouvinte e, em seguida, Thread 1 notifica o ouvinte que o Animal1 foi adicionado. Although resource competition can be ignored when the order of sequence is not considered, the problem is real.
对监听器的并发访问
并发访问监听器可以通过保证监听器的线程安全来实现。秉承着类的“责任自负”精神,监听器有“义务”确保自身的线程安全。例如,对于前面计数的监听器,多线程的递增或递减动物数量可能导致线程安全问题,要避免这一问题,动物数的计算必须是原子操作(原子变量或方法同步),具体解决代码如下:
public class ThreadSafeCountingAnimalAddedListener implements AnimalAddedListener { private static AtomicLong animalsAddedCount = new AtomicLong(0); @Override public void updateAnimalAdded (Animal animal) { // Increment the number of animals animalsAddedCount.incrementAndGet(); // Print the number of animals System.out.println("Total animals added: " + animalsAddedCount); }}方法同步解决方案代码如下:
public class CountingAnimalAddedListener implements AnimalAddedListener { private static int animalsAddedCount = 0; @Override public synchronized void updateAnimalAdded (Animal animal) { // Increment the number of animals animalsAddedCount++; // Print the number of animals System.out.println("Total animals added: " + animalsAddedCount); }}要强调的是监听器应该保证自身的线程安全,subject需要理解监听器的内部逻辑,而不是简单确保对监听器的访问和修改的线程安全。否则,如果多个subject共用同一个监听器,那每个subject类都要重写一遍线程安全的代码,显然这样的代码不够简洁,因此需要在监听器类内实现线程安全。
监听器的有序通知当要求监听器有序执行时,读写锁就不能满足需求了,而需要引入一个新的机制,可以保证notify函数的调用顺序和animal添加到zoo的顺序一致。有人尝试过用方法同步来实现,然而根据Oracle文档中的方法同步介绍,可知方法同步并不提供操作执行的顺序管理。它只是保证原子操作,也就是说操作不会被打断,并不能保证先来先执行(FIFO)的线程顺序。ReentrantReadWriteLock可以实现这样的执行顺序,代码如下:
public class OrderedThreadSafeZoo { private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true); protected final Lock readLock = readWriteLock.readLock(); protected final Lock writeLock = readWriteLock.writeLock(); private List<Animal> animals = new ArrayList<>(); private List<AnimalAddedListener> listeners = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals this.animals.add(animal); // Notify the list of registered listeners this.notifyAnimalAddedListeners(animal); } public AnimalAddedListener registerAnimalAddedListener (AnimalAddedListener listener) { // Lock the list of listeners for writing this.writeLock.lock(); try { // Add the listener to the list of registered listeners this.listeners.add(listener); } finally { // Unlock the writer lock this.writeLock.unlock(); } return listener; } public void unregisterAnimalAddedListener (AnimalAddedListener listener) { // Lock the list of listeners for writing this.writeLock.lock(); try { // Remove the listener from the list of the registered listeners this.listeners.remove(listener); } finally { // Unlock the writer lock this.writeLock.unlock(); } } public void notifyAnimalAddedListeners (Animal animal) { // Lock the list of listeners for reading this.readLock.lock(); try { // Notify each of the listeners in the list of registered listeners this.listeners.forEach(listener -> listener.updateAnimalAdded(animal)); } finally { // Unlock the reader lock this.readLock.unlock(); }}}这样的实现方式,register, unregister和notify函数将按照先进先出(FIFO)的顺序获得读写锁权限。例如,线程1注册一个监听器,线程2在开始执行注册操作后试图通知已注册的监听器,线程3在线程2等待只读锁的时候也试图通知已注册的监听器,采用fair-ordering方式,线程1先完成注册操作,然后线程2可以通知监听器,最后线程3通知监听器。这样保证了action的执行顺序和开始顺序一致。
如果采用方法同步,虽然线程2先排队等待占用资源,线程3仍可能比线程2先获得资源锁,而且不能保证线程2比线程3先通知监听器。问题的关键所在:fair-ordering方式可以保证线程按照申请资源的顺序执行。读写锁的顺序机制很复杂,应参照ReentrantReadWriteLock的官方文档以确保锁的逻辑足够解决问题。
截止目前实现了线程安全,在接下来的章节中将介绍提取主题的逻辑并将其mixin类封装为可重复代码单元的方式优缺点。
主题逻辑封装到Mixin类<br />把上述的观察者模式设计实现封装到目标的mixin类中很具吸引力。通常来说,观察者模式中的观察者包含已注册的监听器的集合;负责注册新的监听器的register函数;负责撤销注册的unregister函数和负责通知监听器的notify函数。对于上述的动物园的例子,zoo类除动物列表是问题所需外,其他所有操作都是为了实现主题的逻辑。
Mixin类的案例如下所示,需要说明的是为使代码更为简洁,此处去掉关于线程安全的代码:
public abstract class ObservableSubjectMixin<ListenerType> { private List<ListenerType> listeners = new ArrayList<>(); public ListenerType registerListener (ListenerType listener) { // Add the listener to the list of registered listeners this.listeners.add(listener); return listener; } public void unregisterAnimalAddedListener (ListenerType listener) { // Remove the listener from the list of the registered listeners this.listeners.remove(listener); } public void notifyListeners (Consumer<? super ListenerType> algorithm) { // Execute some function on each of the listeners this.listeners.forEach(algorithm); }}正因为没有提供正在注册的监听器类型的接口信息,不能直接通知某个特定的监听器,所以正需要保证通知功能的通用性,允许客户端添加一些功能,如接受泛型参数类型的参数匹配,以适用于每个监听器,具体实现代码如下:
public class ZooUsingMixin extends ObservableSubjectMixin<AnimalAddedListener> { private List<Animal> animals = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals this.animals.add(animal); // Notify the list of registered listeners this.notifyListeners((listener) -> listener.updateAnimalAdded(animal)); }}Mixin类技术的最大优势是把观察者模式的Subject封装到一个可重复调用的类中,而不是在每个subject类中都重复写这些逻辑。此外,这一方法使得zoo类的实现更为简洁,只需要存储动物信息,而不用再考虑如何存储和通知监听器。
然而,使用mixin类并非只有优点。比如,如果要存储多个类型的监听器怎么办?例如,还需要存储监听器类型AnimalRemovedListener。mixin类是抽象类,Java中不能同时继承多个抽象类,而且mixin类不能改用接口实现,这是因为接口不包含state,而观察者模式中state需要用来保存已经注册的监听器列表。
其中的一个解决方案是创建一个动物增加和减少时都会通知的监听器类型ZooListener,代码如下所示:
public interface ZooListener { public void onAnimalAdded (Animal animal); public void onAnimalRemoved (Animal animal);}这样就可以使用该接口实现利用一个监听器类型对zoo状态各种变化的监听了:
public class ZooUsingMixin extends ObservableSubjectMixin<ZooListener> { private List<Animal> animals = new ArrayList<>(); public void addAnimal (Animal animal) { // Add the animal to the list of animals this.animals.add(animal); // Notify the list of registered listeners this.notifyListeners((listener) -> listener.onAnimalAdded(animal)); } public void removeAnimal (Animal) animal) { // Remove the animal from the list of animals this.animals.remove(animal); // Notify the list of registered listeners this.notifyListeners((listener) -> listener.onAnimalRemoved(animal)); }}将多个监听器类型合并到一个监听器接口中确实解决了上面提到的问题,但仍旧存在不足之处,接下来的章节会详细讨论。
Multi-Method监听器和适配器
在上述方法,监听器的接口中实现的包含太多函数,接口就过于冗长,例如,Swing MouseListener就包含5个必要的函数。尽管可能只会用到其中一个,但是只要用到鼠标点击事件就必须要添加这5个函数,更多可能是用空函数体来实现剩下的函数,这无疑会给代码带来不必要的混乱。
其中一种解决方案是创建适配器(概念来自GoF提出的适配器模式),适配器中以抽象函数的形式实现监听器接口的操作,供具体监听器类继承。这样一来,具体监听器类就可以选择其需要的函数,对adapter不需要的函数采用默认操作即可。例如上面例子中的ZooListener类,创建ZooAdapter(Adapter的命名规则与监听器一致,只需要把类名中的Listener改为Adapter即可),代码如下:
public class ZooAdapter implements ZooListener { @Override public void onAnimalAdded (Animal animal) {} @Override public void onAnimalRemoved (Animal animal) {}}乍一看,这个适配器类微不足道,然而它所带来的便利却是不可小觑的。比如对于下面的具体类,只需选择对其实现有用的函数即可:
public class NamePrinterZooAdapter extends ZooAdapter { @Override public void onAnimalAdded (Animal animal) { // Print the name of the animal that was added System.out.println("Added animal named " + animal.getName()); }}有两种替代方案同样可以实现适配器类的功能:一是使用默认函数;二是把监听器接口和适配器类合并到一个具体类中。默认函数是Java8新提出的,在接口中允许开发者提供默认(防御)的实现方法。
Java库的这一更新主要是方便开发者在不改变老版本代码的情况下,实现程序扩展,因此应该慎用这个方法。部分开发者多次使用后,会感觉这样写的代码不够专业,而又有开发者认为这是Java8的特色,不管怎样,需要明白这个技术提出的初衷是什么,再结合具体问题决定是否要用。使用默认函数实现的ZooListener接口代码如下示:
public interface ZooListener { default public void onAnimalAdded (Animal animal) {} default public void onAnimalRemoved (Animal animal) {}}通过使用默认函数,实现该接口的具体类,无需在接口中实现全部函数,而是选择性实现所需函数。虽然这是接口膨胀问题一个较为简洁的解决方案,开发者在使用时还应多加注意。
第二种方案是简化观察者模式,省略了监听器接口,而是用具体类实现监听器的功能。比如ZooListener接口就变成了下面这样:
public class ZooListener { public void onAnimalAdded (Animal animal) {} public void onAnimalRemoved (Animal animal) {}}这一方案简化了观察者模式的层次结构,但它并非适用于所有情况,因为如果把监听器接口合并到具体类中,具体监听器就不可以实现多个监听接口了。例如,如果AnimalAddedListener和AnimalRemovedListener接口写在同一个具体类中,那么单独一个具体监听器就不可以同时实现这两个接口了。此外,监听器接口的意图比具体类更显而易见,很显然前者就是为其他类提供接口,但后者就并非那么明显了。
如果没有合适的文档说明,开发者并不会知道已经有一个类扮演着接口的角色,实现了其对应的所有函数。此外,类名不包含adapter,因为类并不适配于某一个接口,因此类名并没有特别暗示此意图。综上所述,特定问题需要选择特定的方法,并没有哪个方法是万能的。
在开始下一章前,需要特别提一下,适配器在观察模式中很常见,尤其是在老版本的Java代码中。Swing API正是以适配器为基础实现的,正如很多老应用在Java5和Java6中的观察者模式中所使用的那样。zoo案例中的监听器或许并不需要适配器,但需要了解适配器提出的目的以及其应用,因为我们可以在现有的代码中对其进行使用。下面的章节,将会介绍时间复杂的监听器,该类监听器可能会执行耗时的运算或进行异步调用,不能立即给出返回值。
Complex & Blocking监听器关于观察者模式的一个假设是:执行一个函数时,一系列监听器会被调用,但假定这一过程对调用者而言是完全透明的。例如,客户端代码在Zoo中添加animal时,在返回添加成功之前,并不知道会调用一系列监听器。如果监听器的执行需要时间较长(其时间受监听器的数量、每个监听器执行时间影响),那么客户端代码将会感知这一简单增加动物操作的时间副作用。
本文不能面面俱到的讨论这个话题,下面几条是开发者调用复杂的监听器时应该注意的事项:
监听器启动新线程。新线程启动后,在新线程中执行监听器逻辑的同时,返回监听器函数的处理结果,并运行其他监听器执行。
Subject启动新线程。与传统的线性迭代已注册的监听器列表不同,Subject的notify函数重启一个新的线程,然后在新线程中迭代监听器列表。这样使得notify函数在执行其他监听器操作的同时可以输出其返回值。需要注意的是需要一个线程安全机制来确保监听器列表不会进行并发修改。
队列化监听器调用并采用一组线程执行监听功能。将监听器操作封装在一些函数中并队列化这些函数,而非简单的迭代调用监听器列表。这些监听器存储到队列中后,线程就可以从队列中弹出单个元素并执行其监听逻辑。这类似于生产者-消费者问题,notify过程产生可执行函数队列,然后线程依次从队列中取出并执行这些函数,函数需要存储被创建的时间而非执行的时间供监听器函数调用。例如,监听器被调用时创建的函数,那么该函数就需要存储该时间点,这一功能类似于Java中的如下操作:
public class
如何使用Java8 实现观察者模式?相信通过这篇文章大家都有了大概的了解了吧!