La situación actual
La configuración actual viola el Principio de segregación de interfaz (la I en SÓLIDO).
Referencia
Según Wikipedia, el principio de segregación de interfaz (ISP) establece que ningún cliente debería verse obligado a depender de métodos que no utiliza . El principio de segregación de interfaz fue formulado por Robert Martin a mediados de la década de 1990.
En otras palabras, si esta es su interfaz:
public interface IUserBackend
{
User getUser(int uid);
User createUser(int uid);
void deleteUser(int uid);
void setPassword(int uid, string password);
}
Entonces, cada clase que implemente esta interfaz debe utilizar todos los métodos enumerados de la interfaz. Sin excepción.
Imagínese si hay un método generalizado:
public void HaveUserDeleted(IUserBackend backendService, User user)
{
backendService.deleteUser(user.Uid);
}
Si realmente lograra que solo algunas de las clases implementadoras puedan eliminar a un usuario, este método ocasionalmente explotará en su cara (o no hará nada). Eso no es buen diseño.
Tu solución propuesta
He visto una solución en la que IUserInterface tiene un método implementActions que devuelve un entero que es el resultado de OR bit a bit de las acciones AND bit a bit con las acciones solicitadas.
Lo que esencialmente quieres hacer es:
public void HaveUserDeleted(IUserBackend backendService, User user)
{
if(backendService.canDeleteUser())
backendService.deleteUser(user.Uid);
}
Estoy ignorando cómo exactamente determinamos si una clase determinada puede eliminar un usuario. Si es un booleano, un poco de bandera, ... no importa. Todo se reduce a una respuesta binaria: ¿puede eliminar un usuario, sí o no?
Eso resolvería el problema, ¿verdad? Bueno, técnicamente, lo hace. Pero ahora, estás violando el Principio de sustitución de Liskov (la L en SÓLIDO).
Renunciando a la explicación bastante compleja de Wikipedia, encontré un ejemplo decente en StackOverflow . Tome nota del ejemplo "malo":
void MakeDuckSwim(IDuck duck)
{
if (duck is ElectricDuck)
((ElectricDuck)duck).TurnOn();
duck.Swim();
}
Supongo que ves la similitud aquí. Es un método que se supone que maneja un objeto abstraído ( IDuck, IUserBackend), pero debido a un diseño de clase comprometido, se ve obligado a manejar primero implementaciones específicas ( ElectricDuck, asegúrese de que no sea una IUserBackendclase que no pueda eliminar usuarios).
Esto anula el propósito de desarrollar un enfoque abstracto.
Nota: El ejemplo aquí es más fácil de solucionar que su caso. Por ejemplo, es suficiente tener el ElectricDuckencendido dentro del Swim()método. Ambos patos todavía pueden nadar, por lo que el resultado funcional es el mismo.
Es posible que desee hacer algo similar. No hacerlo . No puede simplemente pretender eliminar un usuario, sino que en realidad tiene un cuerpo de método vacío. Si bien esto funciona desde una perspectiva técnica, hace que sea imposible saber si su clase implementadora realmente hará algo cuando se le pida que haga algo. Ese es un caldo de cultivo para el código imposible de mantener.
Mi propuesta de solución
Pero usted dijo que es posible (y correcto) que una clase implementadora solo maneje algunos de estos métodos.
Por el bien de ejemplo, digamos que para cada combinación posible de estos métodos, hay una clase que lo implementará. Cubre todas nuestras bases.
La solución aquí es dividir la interfaz .
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);
}
Tenga en cuenta que podría haberlo visto venir al comienzo de mi respuesta. El nombre del Principio de segregación de interfaz ya revela que este principio está diseñado para hacer que segregue las interfaces en un grado suficiente.
Esto le permite mezclar y combinar interfaces como desee:
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
}
Cada clase puede decidir lo que quiere hacer, sin romper el contrato de su interfaz.
Esto también significa que no necesitamos verificar si cierta clase puede eliminar un usuario. Cada clase que implemente la IDeleteUserServiceinterfaz podrá eliminar un usuario = Sin violación del Principio de sustitución de Liskov .
public void HaveUserDeleted(IDeleteUserService backendService, User user)
{
backendService.deleteUser(user.Uid); //guaranteed to work
}
Si alguien intenta pasar un objeto que no se implementa IDeleteUserService, el programa se negará a compilar. Por eso nos gusta tener la seguridad de los tipos.
HaveUserDeleted(new DoesEverythingService()); // No problem.
HaveUserDeleted(new UserDeleteService()); // No problem.
HaveUserDeleted(new UserRetrievalService()); // COMPILE ERROR
Nota
Llevé el ejemplo al extremo, segregando la interfaz en los trozos más pequeños posibles. Sin embargo, si su situación es diferente, puede salirse con la suya con trozos más grandes.
Por ejemplo, si cada servicio que puede crear un usuario siempre es capaz de eliminar un usuario (y viceversa), puede mantener estos métodos como parte de una única interfaz:
public interface IManageUserService
{
User createUser(int uid);
void deleteUser(int uid);
}
No hay ningún beneficio técnico al hacer esto en lugar de separarse a los trozos más pequeños; pero hará que el desarrollo sea un poco más fácil porque requiere menos repeticiones.
IUserBackendno debe contener eldeleteUsermétodo en absoluto. Eso debería ser parte deIUserDeleteBackend(o como quieras llamarlo). El código que necesita eliminar usuarios tendrá argumentosIUserDeleteBackend, el código que no necesita esa funcionalidad lo usaráIUserBackendy no tendrá problemas con los métodos no implementados.