Cenário
Um aplicativo da web define uma interface de back-end do usuário IUserBackend
com os métodos
- getUser (uid)
- createUser (uid)
- deleteUser (uid)
- setPassword (uid, senha)
- ...
Diferentes back-ends de usuários (por exemplo, LDAP, SQL, ...) implementam essa interface, mas nem todos os back-end podem fazer tudo. Por exemplo, um servidor LDAP concreto não permite que este aplicativo da Web exclua usuários. Portanto, a LdapUserBackend
classe que implementa IUserBackend
não implementará deleteUser(uid)
.
A classe concreta precisa comunicar ao aplicativo da Web o que ele pode fazer com os usuários do back-end.
Solução conhecida
Eu vi uma solução em que IUserInterface
tem um implementedActions
método que retorna um número inteiro que é o resultado de ORs bit a bit das ações AND bit a bit com as ações solicitadas:
function implementedActions(requestedActions) {
return (bool)(
ACTION_GET_USER
| ACTION_CREATE_USER
| ACTION_DELTE_USER
| ACTION_SET_PASSWORD
) & requestedActions)
}
Onde
- ACTION_GET_USER = 1
- ACTION_CREATE_USER = 2
- ACTION_DELETE_USER = 4
- ACTION_SET_PASSWORD = 8
- .... = 16
- .... = 32
etc.
Portanto, o aplicativo da Web define uma máscara de bits com o que precisa e implementedActions()
responde com um booleano, se é compatível com eles.
Opinião
Essas operações de bits para mim parecem relíquias da era C, não necessariamente fáceis de entender em termos de código limpo.
Questão
O que é um padrão moderno (melhor?) Para a classe comunicar o subconjunto dos métodos de interface que implementa? Ou o "método de operação de bits" de cima ainda é a melhor prática?
( No caso de ser importante: PHP, embora eu esteja procurando uma solução geral para linguagens OO )
fonte
IUserBackend
não deve conter odeleteUser
método. Isso deve fazer parteIUserDeleteBackend
(ou como você quiser chamar). O código que precisa excluir os usuários terá argumentosIUserDeleteBackend
, o código que não precisa dessa funcionalidade será usadoIUserBackend
e não terá problemas com métodos não implementados.Respostas:
De um modo geral, existem duas abordagens que você pode adotar aqui: teste e lançamento ou composição via polimorfismo.
Testar e lançar
Essa é a abordagem que você já descreve. Por alguns meios, você indica ao usuário da classe se certos outros métodos estão implementados ou não. Isso pode ser feito com um único método e uma enumeração bit a bit (como você descreve), ou através de uma série de
supportsDelete()
métodos etc.Então, se
supportsDelete()
retornarfalse
, a chamadadeleteUser()
poderá resultar em umNotImplementedExeption
lançamento, ou o método simplesmente não fará nada.Esta é uma solução popular entre alguns, pois é simples. No entanto, muitos - inclusive eu - argumentam que é uma violação do princípio de substituição de Liskov (o L no SOLID) e, portanto, não é uma solução agradável.
Composição via Polimorfismo
A abordagem aqui é ver
IUserBackend
um instrumento muito franco. Se as classes nem sempre podem implementar todos os métodos nessa interface, divida a interface em partes mais focadas. Portanto, você pode ter:IGeneralUser IDeletableUser IRenamableUser ...
Em outras palavras, todos os métodos que todos os seus back-end podem implementar entramIGeneralUser
e você cria uma interface separada para cada uma das ações que somente alguns podem executar.Dessa forma, você
LdapUserBackend
não implementaIDeletableUser
e você testa isso usando um teste como (usando a sintaxe C #):(Não tenho certeza do mecanismo no PHP para determinar se uma instância implementa uma interface e como você faz a conversão para essa interface, mas tenho certeza de que há um equivalente nessa linguagem)
A vantagem desse método é que ele faz bom uso do polimorfismo para permitir que seu código esteja em conformidade com os princípios do SOLID e, na minha opinião, é muito mais elegante.
A desvantagem é que ela pode se tornar difícil de manejar com muita facilidade. Se, por exemplo, você tiver que implementar dezenas de interfaces porque cada back-end concreto possui recursos ligeiramente diferentes, essa não é uma boa solução. Por isso, aconselho que você use seu julgamento sobre se essa abordagem é prática para você nesta ocasião e a use, se for.
fonte
if (backend instanceof IDelatableUser) {...}
Divide(float,float)
método. O valor de entrada é variável e a exceção cobre um pequeno subconjunto de possíveis execuções. Mas se você jogar com base no seu tipo de implementação, sua incapacidade de executar é um fato. A exceção abrange todas as entradas possíveis , não apenas um subconjunto delas. É como colocar uma placa de "piso molhado" em todos os pisos molhados de um mundo onde todos os pisos estão sempre molhados.NotImplementedException
. Essa exceção é destinada a interrupções temporárias , ou seja, código que ainda não foi desenvolvido, mas será desenvolvido. Isso não é o mesmo que decidir definitivamente que uma determinada classe nunca fará nada com um determinado método, mesmo após a conclusão do desenvolvimento.A situação atual
A configuração atual viola o princípio de segregação de interface (o I no SOLID).
Referência
Em outras palavras, se esta é sua interface:
Então, toda classe que implementa essa interface deve utilizar todos os métodos listados da interface. Sem exceção.
Imagine se houver um método generalizado:
Se você realmente deseja fazer com que apenas algumas das classes de implementação consigam excluir um usuário, esse método ocasionalmente aparece na sua cara (ou não faz nada). Isso não é um bom design.
Sua solução proposta
O que você essencialmente quer fazer é:
Estou ignorando como exatamente determinamos se uma determinada classe é capaz de excluir um usuário. Se é um sinal booleano, um pouco sinalizado, ... não importa. Tudo se resume a uma resposta binária: ele pode excluir um usuário, sim ou não?
Isso resolveria o problema, certo? Bem, tecnicamente, ele faz. Mas agora, você está violando o Princípio de Substituição de Liskov (o L no SOLID).
Abandonando a explicação bastante complexa da Wikipedia, encontrei um exemplo decente no StackOverflow . Tome nota do exemplo "ruim":
Presumo que você veja a semelhança aqui. É um método que deve lidar com um objeto abstraído (
IDuck
,IUserBackend
), mas devido a um design de classe comprometido, ele é forçado a lidar primeiro com implementações específicas (ElectricDuck
verifique se não é umaIUserBackend
classe que não pode excluir usuários).Isso anula o objetivo de desenvolver uma abordagem abstrata.
Nota: O exemplo aqui é mais fácil de corrigir do que o seu caso. Por exemplo, basta ter a
ElectricDuck
própria ativação dentro doSwim()
método. Ambos os patos ainda são capazes de nadar, então o resultado funcional é o mesmo.Você pode fazer algo semelhante. Não . Você não pode simplesmente fingir excluir um usuário, mas, na realidade, possui um corpo de método vazio. Embora isso funcione de uma perspectiva técnica, torna-se impossível saber se sua classe de implementação fará ou não algo quando solicitado. Esse é um terreno fértil para código não sustentável.
Minha solução proposta
Mas você disse que é possível (e correto) para uma classe de implementação lidar apenas com alguns desses métodos.
Por uma questão de exemplo, digamos que para toda combinação possível desses métodos, há uma classe que o implementará. Abrange todas as nossas bases.
A solução aqui é dividir a interface .
Note que você poderia ter visto isso chegando no começo da minha resposta. O nome do Princípio de Segregação de Interface já revela que esse princípio foi projetado para fazer com que você separe as interfaces em um grau suficiente.
Isso permite que você misture e combine interfaces como desejar:
Toda classe pode decidir o que quer fazer, sem quebrar o contrato de sua interface.
Isso também significa que não precisamos verificar se uma determinada classe é capaz de excluir um usuário. Toda classe que implementa a
IDeleteUserService
interface poderá excluir um usuário = Nenhuma violação do Princípio de Substituição de Liskov .Se alguém tentar passar um objeto que não é implementado
IDeleteUserService
, o programa se recusará a compilar. É por isso que gostamos de ter segurança de tipo.Nota de rodapé
Levei o exemplo ao extremo, segregando a interface nos menores pedaços possíveis. No entanto, se sua situação for diferente, você poderá se livrar de pedaços maiores.
Por exemplo, se todo serviço que pode criar um usuário for sempre capaz de excluir um usuário (e vice-versa), você poderá manter esses métodos como parte de uma única interface:
Não há nenhum benefício técnico em fazer isso, em vez de separar os pedaços menores; mas tornará o desenvolvimento um pouco mais fácil, pois requer menos caldeiras.
fonte
TryDeleteUser
para refletir isso); ou você tem o método intencionalmente lançar uma exceção se for uma situação possível, mas problemática. Usar uma abordagemCanDoThing()
eDoThing()
método funciona, mas exigiria que os chamadores externos usassem duas chamadas (e sejam punidas por não fazer isso), o que é menos intuitivo e não tão elegante.Se você deseja usar tipos de nível superior, pode optar pelo tipo definido no idioma de sua escolha. Espero que ele forneça um pouco de sintaxe para fazer interseções e determinações de subconjuntos.
Isso é basicamente o que o Java faz com o EnumSet (menos o açúcar da sintaxe, mas ei, é o Java)
fonte
No mundo .NET, você pode decorar métodos e classes com atributos personalizados. Isso pode não ser relevante para o seu caso.
Parece-me, no entanto, que o problema que você tem pode ter mais a ver com um nível mais alto do design.
Se esse é um recurso da interface do usuário, como uma página ou componente de edição de usuários, como estão sendo ocultados os diferentes recursos? Nesse caso, 'test and throw' será uma abordagem bastante ineficiente para esse propósito. Ele pressupõe que, antes de carregar todas as páginas, você execute uma chamada simulada para cada função para determinar se o widget ou elemento deve ser oculto ou apresentado de forma diferente. Como alternativa, você tem uma página da Web que basicamente obriga o usuário a descobrir o que está disponível pelo 'teste e lançamento manual', qualquer rota de codificação que você seguir, porque o usuário não descobre que algo não está disponível até que um aviso pop-up seja exibido.
Portanto, para uma interface do usuário, convém analisar como você gerencia o recurso e vincular a escolha das implementações disponíveis a isso, em vez de fazer com que as implementações selecionadas controlem quais recursos podem ser gerenciados. Você pode querer examinar estruturas para compor dependências de recursos e definir explicitamente os recursos como entidades em seu modelo de domínio. Isso pode até estar vinculado à autorização. Essencialmente, decidir se um recurso está disponível ou não com base no nível de autorização pode ser estendido para decidir se um recurso é realmente implementado e, em seguida, os 'recursos' da interface do usuário de alto nível podem ter mapeamentos explícitos para os conjuntos de recursos.
Se essa é uma API da Web, a escolha geral do design pode ser complicada ao oferecer suporte a várias versões públicas da API 'Gerenciar usuário' ou do recurso REST 'Usuário', à medida que os recursos se expandem ao longo do tempo.
Para resumir, no mundo .NET, você pode explorar várias formas de Reflexão / Atributo para determinar antecipadamente quais classes implementam o que, mas, de qualquer forma, parece que os problemas reais estarão no que você faz com essas informações.
fonte