La situazione attuale
L'impostazione corrente viola il principio di segregazione dell'interfaccia (I in SOLID).
Riferimento
Secondo Wikipedia, il principio di segregazione dell'interfaccia (ISP) afferma che nessun cliente dovrebbe essere costretto a dipendere da metodi che non utilizza . Il principio di segregazione dell'interfaccia è stato formulato da Robert Martin a metà degli anni '90.
In altre parole, se questa è la tua interfaccia:
public interface IUserBackend
{
User getUser(int uid);
User createUser(int uid);
void deleteUser(int uid);
void setPassword(int uid, string password);
}
Quindi ogni classe che implementa questa interfaccia deve utilizzare tutti i metodi elencati dell'interfaccia. Nessuna eccezione.
Immagina se esiste un metodo generalizzato:
public void HaveUserDeleted(IUserBackend backendService, User user)
{
backendService.deleteUser(user.Uid);
}
Se dovessi effettivamente farlo in modo che solo alcune delle classi di implementazione siano effettivamente in grado di eliminare un utente, allora questo metodo occasionalmente ti esploderà in faccia (o non farà nulla). Questo non è un buon design.
La tua soluzione proposta
Ho visto una soluzione in cui IUserInterface ha un metodo implementatoActions che restituisce un numero intero che è il risultato di OR bit a bit delle azioni AND bit a bit con le azioni richieste.
Quello che essenzialmente vuoi fare è:
public void HaveUserDeleted(IUserBackend backendService, User user)
{
if(backendService.canDeleteUser())
backendService.deleteUser(user.Uid);
}
Sto ignorando il modo in cui determiniamo esattamente se una determinata classe è in grado di eliminare un utente. Che si tratti di un booleano, un po 'di bandiera, ... non importa. Tutto si riduce a una risposta binaria: può eliminare un utente, sì o no?
Ciò risolverebbe il problema, giusto? Beh, tecnicamente, lo fa. Ma ora stai violando il principio di sostituzione di Liskov (la L in SOLID).
Rinunciando alla spiegazione piuttosto complessa di Wikipedia, ho trovato un esempio decente su StackOverflow . Prendi nota dell'esempio "cattivo":
void MakeDuckSwim(IDuck duck)
{
if (duck is ElectricDuck)
((ElectricDuck)duck).TurnOn();
duck.Swim();
}
Presumo che tu veda la somiglianza qui. È un metodo che dovrebbe gestire un oggetto astratto ( IDuck
, IUserBackend
), ma a causa di un design di classe compromesso, è costretto a gestire prima implementazioni specifiche ( ElectricDuck
, assicurarsi che non sia una IUserBackend
classe che non può eliminare gli utenti).
Ciò vanifica lo scopo di sviluppare un approccio astratto.
Nota: l'esempio qui è più semplice da risolvere rispetto al caso. Per l'esempio, è sufficiente ElectricDuck
attivare l'attivazione all'interno del Swim()
metodo. Entrambe le anatre sono ancora in grado di nuotare, quindi il risultato funzionale è lo stesso.
Potresti voler fare qualcosa di simile. Non farlo . Non puoi semplicemente fingere di eliminare un utente ma in realtà hai un corpo del metodo vuoto. Sebbene funzioni dal punto di vista tecnico, è impossibile sapere se la tua classe di implementazione farà effettivamente qualcosa quando gli viene chiesto di fare qualcosa. Questo è un terreno fertile per il codice non mantenibile.
La mia soluzione proposta
Ma hai detto che è possibile (e corretto) per una classe di implementazione gestire solo alcuni di questi metodi.
Per esempio, diciamo che per ogni possibile combinazione di questi metodi, esiste una classe che lo implementerà. Copre tutte le nostre basi.
La soluzione qui è dividere l'interfaccia .
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);
}
Nota che avresti potuto vederlo arrivare all'inizio della mia risposta. Il nome del Principio di segregazione dell'interfaccia rivela già che questo principio è progettato per separare le interfacce in misura sufficiente.
Ciò ti consente di combinare le interfacce come preferisci:
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
}
Ogni classe può decidere cosa vogliono fare, senza interrompere il contratto della loro interfaccia.
Ciò significa anche che non è necessario verificare se una determinata classe è in grado di eliminare un utente. Ogni classe che implementa l' IDeleteUserService
interfaccia sarà in grado di eliminare un utente = nessuna violazione del principio di sostituzione di Liskov .
public void HaveUserDeleted(IDeleteUserService backendService, User user)
{
backendService.deleteUser(user.Uid); //guaranteed to work
}
Se qualcuno tenta di passare un oggetto che non viene implementato IDeleteUserService
, il programma rifiuterà di compilare. Ecco perché ci piace avere la sicurezza dei tipi.
HaveUserDeleted(new DoesEverythingService()); // No problem.
HaveUserDeleted(new UserDeleteService()); // No problem.
HaveUserDeleted(new UserRetrievalService()); // COMPILE ERROR
Nota
Ho portato l'esempio all'estremo, separando l'interfaccia nei blocchi più piccoli possibili. Tuttavia, se la tua situazione è diversa, puoi cavartela con pezzi più grandi.
Ad esempio, se ogni servizio in grado di creare un utente è sempre in grado di eliminare un utente (e viceversa), è possibile mantenere questi metodi come parte di un'unica interfaccia:
public interface IManageUserService
{
User createUser(int uid);
void deleteUser(int uid);
}
Non vi è alcun vantaggio tecnico nel fare questo invece di separarsi dai blocchi più piccoli; ma renderà lo sviluppo leggermente più semplice perché richiede meno galvaniche.
IUserBackend
dovrebbe contenere affatto ildeleteUser
metodo. Questo dovrebbe far parte diIUserDeleteBackend
(o come vuoi chiamarlo). Il codice che deve eliminare gli utenti avrà argomenti diIUserDeleteBackend
, il codice che non ha bisogno di quella funzionalità utilizzeràIUserBackend
e non avrà problemi con metodi non implementati.