The current situation
The current setup violates the Interface Segregation Principle (the 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 other words, if this is your interface:
public interface IUserBackend
{
User getUser(int uid);
User createUser(int uid);
void deleteUser(int uid);
void setPassword(int uid, string password);
}
Then every class that implements this interface must utilize every listed method of the interface. No exception.
Imagine if there's a generalized method:
public void HaveUserDeleted(IUserBackend backendService, User user)
{
backendService.deleteUser(user.Uid);
}
If you were to actually make it so that only some of the implementing classes are actually able to delete a user, then this method will occasionally blow up in your face (or do nothing at all). That is not good design.
Your proposed solution
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.
What you essentially want to do is:
public void HaveUserDeleted(IUserBackend backendService, User user)
{
if(backendService.canDeleteUser())
backendService.deleteUser(user.Uid);
}
I'm ignoring how exactly we determine whether a given class is able to delete a user. Whether it's a boolean, a bit flag, ... doesn't matter. It all boils down to a binary answer: can it delete a user, yes or no?
That would solve the problem, right? Well, technically, it does. But now, you're violating the Liskov Substitution Principle (the L in SOLID).
Forgoing the rather complex Wikipedia explanation, I found a decent example on StackOverflow. Take note of the "bad" example:
void MakeDuckSwim(IDuck duck)
{
if (duck is ElectricDuck)
((ElectricDuck)duck).TurnOn();
duck.Swim();
}
I assume you see the similarity here. It's a method that is supposed to handle an abstracted object (IDuck
, IUserBackend
), but because of a compromised class design, it is forced to first handle specific implementations (ElectricDuck
, ensure it's not a IUserBackend
class that can't delete users).
This defeats the purpose of developing an abstracted approach.
Note: The example here is easier to fix than your case. For the example, it suffices to have the ElectricDuck
turn itself on inside the Swim()
method. Both ducks are still able to swim, so the functional result is the same.
You may want to do something similar. Don't. You can't just pretend to delete a user but in reality have an empty method body. While this does work from a technical perspective, it makes it impossible to know whether or not your implementing class will actually do something when asked to do something. That is a breeding ground for unmaintainable code.
My proposed solution
But you said that it's possible (and correct) for an implementing class to only handle some of these methods.
For the sake of example, let's say that for every possible combination of these methods, there is a class that will implement it. It covers all our bases.
The solution here is to split the 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 that you could've seen this coming in the beginning of my answer. The Interface Segregation Principle name already reveals that this principle is designed to make you segregate the interfaces to a sufficient degree.
This allows you to mix-and-match interfaces as you please:
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
}
Every class can decide which they want to do, without every breaking the contract of their interface.
This also means that we don't need to check if a certain class is able to delete a user. Every class that implements the IDeleteUserService
interface will be able to delete a user = No violation of the Liskov Substitution Principle.
public void HaveUserDeleted(IDeleteUserService backendService, User user)
{
backendService.deleteUser(user.Uid); //guaranteed to work
}
If anyone tries to pass an object that does not implement IDeleteUserService
, the program will refuse to compile. This is why we like having type safety.
HaveUserDeleted(new DoesEverythingService()); // No problem.
HaveUserDeleted(new UserDeleteService()); // No problem.
HaveUserDeleted(new UserRetrievalService()); // COMPILE ERROR
Footnote
I took the example to an extreme, segregating the interface into the smallest possible chunks. However, if your situation is different, you can get away with bigger chunks.
For example, if every service that can create a user is always capable of deleting a user (and vice versa), you can keep these methods as part of a single interface:
public interface IManageUserService
{
User createUser(int uid);
void deleteUser(int uid);
}
There is no technical benefit do doing this instead of separating to the smaller chunks; but it will make development slightly easier because it requires less boilerplating.
IUserBackend
should not contain thedeleteUser
method at all. That should be part ofIUserDeleteBackend
(or whatever you want to call it). Code that needs to delete users will have arguments ofIUserDeleteBackend
, code that doesn't need that functionality will useIUserBackend
and wont have any troubles with unimplemented methods.