A situação atual
A configuração atual viola o princípio de segregação de interface (o I no SOLID).
Referência
De acordo com a Wikipedia, o princípio de segregação de interface (ISP) declara que nenhum cliente deve ser forçado a depender dos métodos que não usa . O princípio de segregação de interface foi formulado por Robert Martin em meados dos anos 90.
Em outras palavras, se esta é sua interface:
public interface IUserBackend
{
User getUser(int uid);
User createUser(int uid);
void deleteUser(int uid);
void setPassword(int uid, string password);
}
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:
public void HaveUserDeleted(IUserBackend backendService, User user)
{
backendService.deleteUser(user.Uid);
}
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
Eu vi uma solução em que o IUserInterface possui um método managedActions 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.
O que você essencialmente quer fazer é:
public void HaveUserDeleted(IUserBackend backendService, User user)
{
if(backendService.canDeleteUser())
backendService.deleteUser(user.Uid);
}
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":
void MakeDuckSwim(IDuck duck)
{
if (duck is ElectricDuck)
((ElectricDuck)duck).TurnOn();
duck.Swim();
}
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 ( ElectricDuckverifique se não é uma IUserBackendclasse 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 ElectricDuckprópria ativação dentro do Swim()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 .
public interface IGetUserService
{
User getUser(int uid);
}
public interface ICreateUserService
{
User createUser(int uid);
}
public interface IDeleteUserService
{
void deleteUser(int uid);
}
public interface ISetPasswordService
{
void setPassword(int uid, string password);
}
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:
public class UserRetrievalService
: IGetUserService, ICreateUserService
{
//getUser and createUser methods implemented here
}
public class UserDeleteService
: IDeleteUserService
{
//deleteUser method implemented here
}
public class DoesEverythingService
: IGetUserService, ICreateUserService, IDeleteUserService, ISetPasswordService
{
//All methods implemented here
}
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 IDeleteUserServiceinterface poderá excluir um usuário = Nenhuma violação do Princípio de Substituição de Liskov .
public void HaveUserDeleted(IDeleteUserService backendService, User user)
{
backendService.deleteUser(user.Uid); //guaranteed to work
}
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.
HaveUserDeleted(new DoesEverythingService()); // No problem.
HaveUserDeleted(new UserDeleteService()); // No problem.
HaveUserDeleted(new UserRetrievalService()); // COMPILE ERROR
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:
public interface IManageUserService
{
User createUser(int uid);
void deleteUser(int uid);
}
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.
IUserBackendnão deve conter odeleteUsermé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á usadoIUserBackende não terá problemas com métodos não implementados.