Por exemplo:
Somente solicitações de trabalho que ainda não estão em revisão ou aprovadas podem ser atualizadas. Em outras palavras, uma pessoa pode atualizar seu formulário de dispositivo de trabalho até que o RH comece a analisá-lo, ou ele já será aceito.
Portanto, um pedido de emprego pode estar em 4 estados:
APLICADO (estado inicial), IN_REVIEW, APROVADO, DECLINADO
Como faço para alcançar esse comportamento?
Certamente, posso escrever um método update () na classe Application, verificar o estado do aplicativo e não fazer nada ou lançar uma exceção se o aplicativo não estiver no estado necessário
Mas esse tipo de código não torna óbvio que essa regra existe, permite que qualquer pessoa chame o método update () e somente depois de falhar o cliente sabe que essa operação não era permitida. Portanto, o cliente precisa estar ciente de que tal tentativa pode falhar, portanto, tenha cuidado. O cliente estar ciente de tais coisas também significa que a lógica está vazando para fora.
Tentei criar classes diferentes para cada estado (ApprovedApplication etc.) e coloquei as operações permitidas apenas nas classes permitidas, mas esse tipo de abordagem também parece errado.
Existe um padrão oficial de design, ou um simples trecho de código, para implementar esse comportamento?
this kind of code does not make it obvious such a rule exists
- É por isso que o código possui documentação. Os escritores de bom código seguirão o conselho da Euphoric e fornecerão um método para permitir que pessoas de fora testem a regra antes de experimentar o hardware.Respostas:
Esse tipo de situação aparece com bastante frequência. Por exemplo, os arquivos só podem ser manipulados enquanto estão abertos e, se você tentar fazer algo com um arquivo depois de fechado, você receberá uma exceção de tempo de execução.
Seu desejo ( expresso em sua pergunta anterior ) de usar o sistema de tipos da linguagem para garantir que algo errado não possa acontecer é nobre, pois erros em tempo de compilação são sempre preferíveis a erros em tempo de execução. No entanto, não há um padrão de design que conheço para esse tipo de situação, provavelmente porque acabaria causando mais problemas do que resolveria. (Seria impraticável.)
A coisa mais próxima da sua situação que eu conheço é modelar estados diferentes de um objeto que correspondam a recursos diferentes por meio de interfaces extras, mas dessa maneira você está reduzindo o número de locais no código onde pode ocorrer um erro de tempo de execução. não erradicar a possibilidade de um erro de tempo de execução.
Portanto, na sua situação, você declararia várias interfaces descrevendo o que pode ser feito com seu objeto em seus vários estados, e seu objeto retornaria uma referência à interface correta em uma transição de estado.
Então, por exemplo, o
approve()
método da sua classe retornaria umaApprovedApplication
interface. A interface seria implementada em particular (por meio de uma classe aninhada), de modo que o código que apenas faz referência aApplication
não pode invocar nenhum dosApprovedApplication
métodos. Em seguida, o código que manipula um aplicativo aprovado declara explicitamente sua intenção de fazê-lo no momento da compilação, exigindoApprovedApplication
que ele trabalhe. Mas, é claro, se você armazenar essa interface em algum lugar e continuar a usá-la após adecline()
invocação do método, você ainda receberá um erro de tempo de execução. Eu não acho que exista uma solução perfeita para o seu problema.fonte
if( someone.hasApprovalPermission( application ) ) { application.approve(); }
o princípio da separação de preocupações indica que nem o aplicativo nem alguém deve se preocupar em tomar decisões sobre permissões e segurança.Estou acenando com a cabeça em diferentes partes das várias respostas, mas o OP parece ainda ter a preocupação de controlar o fluxo. Há muito para tentar se unir em palavras. Eu apenas vou corrigir algum código - O Padrão de Estado.
Nomes de estados como pretérito
"In_Review" não é um estado, talvez, mas uma transição ou processo. Caso contrário, os nomes dos seus estados devem ser consistentes: "Aplicando", "Aprovando", "Recusando" etc. etc. OU também "Revisado". Ou não.
O estado Aplicado faz uma transição de revisão e define o estado para Revisado. O estado revisado faz uma transição de aprovação e define o estado como Aprovado (ou Recusado).
Editar - Comentários sobre o tratamento de erros
Um comentário recente:
E a partir da pergunta original:
Há mais trabalho de design a ser feito. Não existe
Unified Field Theory Pattern
. A confusão vem do pressuposto de que a estrutura de transição de estado executará funções gerais de aplicativos e manipulação de erros. Isso parece errado, porque é. A resposta mostrada foi projetada para controlar a mudança de estado.Isso sugere que existem três funcionalidades trabalhando aqui: O Estado, Atualização e interação dos dois. Nesse caso,
Application
não é o código que escrevi. Pode ser usado para determinar o estado atual.Application
também não éapplicationPaperwork
.Application
não é a interação dos dois, mas poderia ser umaStateContextEvaluator
classe geral . AgoraApplication
orquestrará essas interações de componentes e, em seguida, agirá de acordo, como emitir uma mensagem de erro.Finalizar edição
fonte
Application
construtor em que a exceção é lançada. Talvez a chamadaAppliedState.Approve()
possa resultar em uma mensagem do usuário "O aplicativo deve ser revisado antes de poder ser aprovado".AppliedState.apply()
lembrar gentilmente ao usuário que o aplicativo já foi enviado e está aguardando revisão. E o programa continua.Em geral, o que você está descrevendo é um fluxo de trabalho. Mais especificamente, funções comerciais incorporadas por estados como REVISADO APROVADO ou RECUSADO se enquadram no cabeçalho de "regras de negócios" ou "lógica de negócios".
Mas, para ficar claro, as regras de negócios não devem ser codificadas em exceções. Fazer isso seria usar exceções para o controle de fluxo do programa, e há muitas boas razões para você não fazer isso. Exceções devem ser usadas para condições excepcionais, e o estado INVÁLIDO de um aplicativo é totalmente excepcional do ponto de vista comercial.
Use exceções nos casos em que o programa não pode se recuperar de uma condição de erro sem a intervenção do usuário ("arquivo não encontrado", por exemplo).
Não há um padrão específico para escrever lógica de negócios, além das técnicas usuais para organizar sistemas de processamento de dados de negócios e escrever código para implementar seus processos. Se as regras de negócios e o fluxo de trabalho forem elaborados, considere usar algum tipo de servidor de fluxo de trabalho ou mecanismo de regras de negócios.
De qualquer forma, os estados REVIEW, APPROVED, DECLINED etc. podem ser representados por uma variável privada do tipo enum em sua classe. Se você usar métodos getter / setter, poderá controlar se os setters permitirão ou não alterações, examinando primeiro o valor da variável enum. Se alguém tenta escrever para um setter quando o valor de enumeração está no estado errado, então você pode lançar uma exceção.
fonte
Application
poderia ser uma interface e você poderia ter uma implementação para cada um dos estados. A interface poderia ter ummoveToNextState()
método, e isso ocultaria toda a lógica do fluxo de trabalho.Para as necessidades do cliente, também pode haver um método retornando diretamente o que você pode fazer e não (ou seja, um conjunto de booleanos), em vez de apenas o estado, para que você não precise de uma "lista de verificação" no cliente (presumo mesmo assim, o cliente deve ser um controlador ou interface do usuário MVC).
No entanto, em vez de lançar uma exceção, você não pode fazer nada e registrar a tentativa. Isso é seguro no tempo de execução, as regras foram aplicadas e o cliente tinha maneiras de ocultar os controles de "atualização".
fonte
Uma abordagem para esse problema que tem sido extremamente bem-sucedida na natureza é a hipermídia - a representação do estado da entidade é acompanhada por controles de hipermídia que descrevem os tipos de transições atualmente permitidas. O consumidor consulta os controles para descobrir o que pode ser feito.
É uma máquina de estado, com uma consulta em sua interface que permite descobrir quais eventos você pode disparar.
Em outras palavras: estamos descrevendo a web (REST).
Outra abordagem é levar sua idéia de interfaces diferentes para diferentes estados e fornecer uma consulta que permita detectar quais interfaces estão disponíveis no momento. Pense em IUnknown :: QueryInterface ou em baixa transmissão. O código do cliente toca Mother May I com o estado para descobrir o que é permitido.
É essencialmente o mesmo padrão - basta usar uma interface para representar os controles da hipermídia.
fonte
Aqui está um exemplo de como você pode abordar isso de uma perspectiva funcional e como isso ajuda a evitar as possíveis armadilhas. Estou trabalhando em Haskell, que presumo que você não saiba, então explicarei em detalhes à medida que for avançando.
Isso define um tipo de dados que pode estar em um dos quatro estados que correspondem aos estados do seu aplicativo.
ApplicationDetails
é considerado um tipo existente que contém as informações detalhadas.Um alias de tipo que precisa de conversão explícita de e para
Application
. Isso significa que, se definirmos a seguinte função que aceita e desembrulha umUpdatableApplication
e faz algo útil com ele,precisamos converter explicitamente o aplicativo em um UpdatableApplication antes de podermos usá-lo. Isso é feito usando esta função:
Aqui fazemos três coisas interessantes:
UpdatableApplication
(que envolve apenas uma nota de tipo de compilação da alteração do tipo que está sendo adicionada, pois Haskell possui um recurso específico para fazer esse tipo de truque no nível de tipo, não custa nada em tempo de execução) eOption
C # ouOptional
em Java - é um objeto que envolve um resultado que pode estar ausente).Agora, para realmente montar isso, precisamos chamar essa função e, se o resultado for bem-sucedido, passá-lo para a função de atualização ...
Como a
updateApplication
função precisa do objeto empacotado, não podemos esquecer de verificar as pré-condições. E como a função de verificação de pré-condição retorna o objeto embrulhado dentro de umMaybe
objeto, não podemos esquecer de verificar o resultado e responder de acordo se houver falha.Agora ... você pode fazer isso em uma linguagem orientada a objetos. Mas é menos conveniente:
Maybe
elas geralmente não têm uma maneira conveniente de extrair os dados e escolher o caminho a seguir ao mesmo tempo. A correspondência de padrões também é realmente útil aqui.fonte
Você pode usar o padrão «command» e pedir ao Invoker para fornecer uma lista de funções válidas de acordo com o estado da classe do receptor.
Eu usei o mesmo para fornecer funcionalidade a diferentes interfaces que deveriam chamar meu código; algumas das opções não estavam disponíveis dependendo do estado atual do registro; portanto, meu invocador atualizou a lista e, dessa forma, toda GUI solicitou ao Invocador quais opções estavam disponíveis e eles se pintaram de acordo.
fonte