mercredi 15 septembre 2021

What is the best way to refer to an object's internal implementation when working with interfaces?

I have a project that supports plugins. As to make plugins unit-testable, they only interact with the main application through interfaces.

The main application provides an implementation to these interfaces. The different components of this application also have dependencies between each others.

I want to limit the amount of interface members by only having the ones needed for plugin development. Problem is, sometimes an implementation needs to call a method that I do not want to expose to the plugins. In a "class" world, I would use the internal keyword for these methods.

Here's a rudimentary example that might be clearer:

interface IUserManager
{
    IReadOnlyCollection<IUser> Users { get; }
}

interface IUser
{
    string Name { get; }
}

class UserImpl : IUser
{
    public string Name { get; }

    public void Delete()
    {
        // ...
    }
}

class UserManager : IUserManager
{
    IReadOnlyCollection<IUser> Users { get; }

    public void DeleteAllUsers()
    {
        foreach(var user in Users)
        {
            if(user is UserImpl impl)
            {
                impl.Delete();
            }
        }
    }
}

class Plugin
{
    public Plugin(IUserManager userManager)
    {
        // I want the plugin to be able to access the user's name, but not its Delete() method
        Console.WriteLine(userManager.Users.First().Name);
    }
}

class NetworkController
{
    private readonly IUserManager _userManager;
    public ReceiveDeleteMessage(string name)
    {
        var user = _userManager.Users.Single(x => x.Name == name);
        user.Delete(); // not possible, needs a cast
    }
}

But this feels wrong to me... Not only we now have a cast that could fail, we have no way to mock the Delete() function for implementation unit tests. The best thing I could come with is adding an "internal" interface, but I am still stuck with the cast.

interface IInternalUser : IUser
{
    void Delete();
}

class UserImpl : IInternalUser
{
    public string Name { get; }

    public void Delete()
    {
    }
}

// in UserManager ...
if(user is IInternalUser internalUser)
{
    internalUser.Delete();
}

I could settle with this and I think it will be fine, but this little detail makes me feel like I am not taking the best approach. I am looking for better ideas on how to do this.

Aucun commentaire:

Enregistrer un commentaire