Die aktuelle Situation
Das aktuelle Setup verstößt gegen das Prinzip der Schnittstellentrennung (das I in SOLID).
Referenz
Laut Wikipedia sollte nach dem Prinzip der Schnittstellentrennung (ISP) kein Client gezwungen werden, von Methoden abhängig zu sein, die er nicht verwendet . Das Prinzip der Grenzflächentrennung wurde Mitte der 90er Jahre von Robert Martin formuliert.
Mit anderen Worten, wenn dies Ihre Schnittstelle ist:
public interface IUserBackend
{
User getUser(int uid);
User createUser(int uid);
void deleteUser(int uid);
void setPassword(int uid, string password);
}
Dann muss jede Klasse, die diese Schnittstelle implementiert, jede aufgeführte Methode der Schnittstelle verwenden. Keine Ausnahmen.
Stellen Sie sich vor, es gibt eine verallgemeinerte Methode:
public void HaveUserDeleted(IUserBackend backendService, User user)
{
backendService.deleteUser(user.Uid);
}
Wenn Sie es tatsächlich so machen, dass nur einige der implementierenden Klassen tatsächlich in der Lage sind, einen Benutzer zu löschen, wird diese Methode gelegentlich in die Luft jagen (oder gar nichts tun). Das ist kein gutes Design.
Ihre vorgeschlagene Lösung
Ich habe eine Lösung gesehen, bei der das IUserInterface eine implementierteActions-Methode hat, die eine Ganzzahl zurückgibt, die das Ergebnis von bitweisen ODER-Verknüpfungen der Aktionen ist, die bitweise mit den angeforderten Aktionen UND-verknüpft sind.
Was Sie im Wesentlichen tun möchten, ist:
public void HaveUserDeleted(IUserBackend backendService, User user)
{
if(backendService.canDeleteUser())
backendService.deleteUser(user.Uid);
}
Ich ignoriere, wie genau wir feststellen, ob eine bestimmte Klasse einen Benutzer löschen kann. Ob es ein Boolescher Wert ist, ein bisschen Flagge, ... spielt keine Rolle. Alles läuft auf eine binäre Antwort hinaus: Kann ein Benutzer gelöscht werden, ja oder nein?
Das würde das Problem lösen, oder? Nun, technisch gesehen schon. Aber jetzt verletzen Sie das Liskov-Substitutionsprinzip (das L in SOLID).
Auf die recht komplexe Wikipedia-Erklärung verzichtend, fand ich ein anständiges Beispiel für StackOverflow . Beachten Sie das "schlechte" Beispiel:
void MakeDuckSwim(IDuck duck)
{
if (duck is ElectricDuck)
((ElectricDuck)duck).TurnOn();
duck.Swim();
}
Ich nehme an, Sie sehen die Ähnlichkeit hier. Es ist eine Methode, die ein abstrahiertes Objekt ( IDuck
, IUserBackend
) behandeln soll, aber aufgrund eines beeinträchtigten Klassendesigns ist sie gezwungen, zuerst bestimmte Implementierungen zu behandeln ( ElectricDuck
stellen Sie sicher, dass es keine IUserBackend
Klasse ist, die Benutzer nicht löschen kann).
Dies ist gegen den Zweck der Entwicklung eines abstrakten Ansatzes gerichtet.
Hinweis: Das Beispiel hier ist einfacher zu reparieren als Ihr Fall. Für das Beispiel ist es ausreichend, die ElectricDuck
Option selbst in der Swim()
Methode zu aktivieren. Beide Enten können noch schwimmen, das funktionale Ergebnis ist also dasselbe.
Möglicherweise möchten Sie etwas Ähnliches tun. Nicht . Sie können nicht nur so tun , als ob Sie einen Benutzer löschen würden, sondern haben in Wirklichkeit einen leeren Methodenkörper. Dies funktioniert zwar aus technischer Sicht, macht es jedoch unmöglich zu wissen, ob Ihre implementierende Klasse tatsächlich etwas tut, wenn Sie aufgefordert werden, etwas zu tun. Das ist ein Nährboden für unerreichbaren Code.
Mein Lösungsvorschlag
Sie sagten jedoch, dass es für eine implementierende Klasse möglich (und richtig) ist, nur einige dieser Methoden zu verarbeiten.
Nehmen wir zum Beispiel an, dass es für jede mögliche Kombination dieser Methoden eine Klasse gibt, die sie implementiert. Es deckt alle unsere Basen ab.
Die Lösung besteht darin , die Schnittstelle zu teilen .
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);
}
Beachten Sie, dass Sie dies am Anfang meiner Antwort gesehen haben könnten. Die Schnittstelle Segregationsprinzip Name verrät bereits , dass dieses Prinzip entworfen, um Sie zu machen die Schnittstellen entmischen in ausreichendem Maße.
Auf diese Weise können Sie Schnittstellen beliebig kombinieren:
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
}
Jede Klasse kann entscheiden, was sie tun möchte, ohne den Vertrag ihrer Schnittstelle zu brechen.
Dies bedeutet auch, dass wir nicht prüfen müssen, ob eine bestimmte Klasse einen Benutzer löschen kann. Jede Klasse, die die IDeleteUserService
Schnittstelle implementiert , kann einen Benutzer löschen = Keine Verletzung des Liskov-Substitutionsprinzips .
public void HaveUserDeleted(IDeleteUserService backendService, User user)
{
backendService.deleteUser(user.Uid); //guaranteed to work
}
Wenn jemand versucht, ein Objekt zu übergeben, das nicht implementiert ist IDeleteUserService
, lehnt das Programm die Kompilierung ab. Deshalb haben wir gerne Typensicherheit.
HaveUserDeleted(new DoesEverythingService()); // No problem.
HaveUserDeleted(new UserDeleteService()); // No problem.
HaveUserDeleted(new UserRetrievalService()); // COMPILE ERROR
Fußnote
Ich habe das Beispiel auf die Spitze getrieben und die Schnittstelle in die kleinstmöglichen Teile aufgeteilt. Wenn Ihre Situation jedoch anders ist, können Sie mit größeren Stücken davonkommen.
Wenn beispielsweise jeder Dienst, der einen Benutzer erstellen kann , einen Benutzer immer löschen kann (und umgekehrt), können Sie diese Methoden als Teil einer einzelnen Schnittstelle beibehalten:
public interface IManageUserService
{
User createUser(int uid);
void deleteUser(int uid);
}
Es gibt keinen technischen Vorteil, dies zu tun, anstatt sich in die kleineren Stücke zu trennen. Dies wird jedoch die Entwicklung etwas erleichtern, da weniger Boilerplating erforderlich ist.
IUserBackend
sollte diedeleteUser
Methode überhaupt nicht enthalten . Das sollte Teil seinIUserDeleteBackend
(oder wie auch immer du es nennen willst). Code, der Benutzer löschen muss, hat Argumente vonIUserDeleteBackend
, Code, der diese Funktionalität nicht benötigt, wirdIUserBackend
Probleme mit nicht implementierten Methoden haben.