La situazione attuale
La configurazione corrente viola il principio di segregazione dell'interfaccia (I in SOLID).
Reference
According to Wikipedia the interface segregation principle (ISP) states that no client should be forced to depend on methods it does not use. The interface segregation principle was formulated by Robert Martin in the mid 1990s.
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 la classe ogni che implementa questa interfaccia deve utilizzare il ogni metodo elencato 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, questo metodo a volte esploderà in faccia (o non farà nulla) . Non è un buon design.
La tua soluzione proposta
I have seen a solution where the IUserInterface has a implementedActions method that returns a integer which is the result of bitwise ORs of the actions bitwise ANDed with the requested actions.
Ciò che essenzialmente vuoi fare è:
public void HaveUserDeleted(IUserBackend backendService, User user)
{
if(backendService.canDeleteUser())
backendService.deleteUser(user.Uid);
}
Sto ignorando in che modo 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ò cancellare un utente, sì o no?
Questo risolverebbe il problema, giusto? Bene, tecnicamente, lo fa. Ma ora stai violando il Principio di sostituzione di Liskov (la L in SOLID).
Rinunciare alla spiegazione di Wikipedia piuttosto complessa, 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();
}
Suppongo 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 specifico implementazioni ( ElectricDuck
, assicurati che non sia una classe IUserBackend
che non può eliminare utenti).
Questo sconfigge lo scopo di sviluppare un approccio astratto.
Nota: l'esempio qui è più facile da correggere rispetto al tuo caso. Per l'esempio, è sufficiente che ElectricDuck
giri su dentro il metodo Swim()
. Entrambe le anatre sono ancora in grado di nuotare, quindi il risultato funzionale è lo stesso.
Potresti voler fare qualcosa di simile. Non . Non puoi semplicemente fingere di eliminare un utente, ma in realtà avere un corpo di metodo vuoto. Sebbene funzioni da un punto di vista tecnico, rende impossibile sapere se la tua classe di implementazione farà effettivamente qualcosa quando viene richiesto 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 combinazione possibile di questi metodi, c'è una classe che la 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 vedere questo arrivo all'inizio della mia risposta. Il nome Interfaccia Segregation Principle rivela già che questo principio è progettato per farti 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 quale vuole fare, senza interrompere il contratto della sua interfaccia.
Questo significa anche che non è necessario verificare se una determinata classe è in grado di eliminare un utente. Ogni classe che implementa l'interfaccia IDeleteUserService
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 implementa IDeleteUserService
, il programma rifiuterà di compilare. Questo è il motivo per cui ci piace avere la sicurezza del tipo.
HaveUserDeleted(new DoesEverythingService()); // No problem.
HaveUserDeleted(new UserDeleteService()); // No problem.
HaveUserDeleted(new UserRetrievalService()); // COMPILE ERROR
nota
Ho portato l'esempio a un estremo, separando l'interfaccia in blocchi più piccoli possibili. Tuttavia, se la tua situazione è diversa, puoi farla franca con pezzi più grandi.
Ad esempio, se ogni servizio che crea un utente è sempre in grado di eliminare un utente (e viceversa), puoi mantenere questi metodi come parte di un interfaccia singola:
public interface IManageUserService
{
User createUser(int uid);
void deleteUser(int uid);
}
Non c'è alcun vantaggio tecnico a fare questo invece di separare i pezzi più piccoli; ma renderà lo sviluppo un po 'più facile perché richiede meno calcoli.