使用接口时引用对象内部实现的最佳方式是什么?

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

我有一个支持插件的项目。至于使插件可单元测试,它们仅通过接口与主应用程序交互。

主应用程序提供了这些接口的实现。此应用程序的不同组件之间也存在依赖关系。

我想通过只拥有插件开发所需的接口成员来限制接口成员的数量。问题是,有时一个实现需要调用一个我不想暴露给插件的方法。在“class”的世界中,我会为这些方法使用 internal 关键字。

这是一个可能更清楚的基本示例:

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
    }
}

但这对我来说感觉不对...不仅我们现在有一个可能会失败的转换,而且我们无法为实现单元测试模拟 Delete() 函数。我能想到的最好的事情是添加一个“内部”界面,但我仍然坚持演员表。

interface IInternalUser : IUser
{
    void Delete();
}

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

    public void Delete()
    {
    }
}

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

我可以解决这个问题,我认为它会很好,但是这个小细节让我觉得我没有采取最好的方法。我正在寻找有关如何执行此操作的更好的想法。

interface IDelete 
{
    void Delete();
}

interface IUser : IDelete
{
    string Name { get; }
}

像这样?

interface IUserBehavior : IDelete , ICreate ....
{
    IUser Data;
}

IReadOnlyCollection<IUserBehavior> Users { get; } 

还是这个?

你可以引入一个接口IUserWithDelete比如:

interface IUserWithDelete: IUser
{
    void Delete();
}

此接口不会传递给任何插件,因此它们永远不会看到 Delete() 方法。

然后 class UserManager 可以有一个 IReadOnlyCollection<IUserWithDelete> 字段公开为 IReadOnlyCollection<IUser>.

UserImpl class 将实现 IUserWithDelete,如果需要,UserImpl 可以是内部的。

所以你会像这样实现 UserImpl

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

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

和class UserManager 沿线:

class UserManager : IUserManager
{
    public UserManager(IReadOnlyCollection<IUserWithDelete> users)
    {
        _users = users;
    }

    public IReadOnlyCollection<IUser> Users => _users;

    public void DeleteAllUsers()
    {
        foreach (var user in _users)
        {
            user.Delete();
        }
    }

    readonly IReadOnlyCollection<IUserWithDelete> _users;
}

不需要转换,插件只会将 Users 视为 IReadOnlyCollection<IUser>,因此 IUserWithDelete 不会暴露给它们。

Example on .net Fiddle


回答您修改后的问题:

首先,我认为删除操作应该在用户管理器中class - 毕竟,那是它的工作!

其次,这样做你可以引入另一个新接口,IUserManagerWithDelete(如果 NetworkController 的实现是内部的,它可以是内部的)像这样:

interface IUserManagerWithDelete: IUserManager
{
    bool DeleteUser(string name);
}

然后您的 UserManager 实施变为:

class UserManager : IUserManagerWithDelete
{
    public UserManager(IReadOnlyCollection<IUserWithDelete> users)
    {
        _users = users;
    }

    public IReadOnlyCollection<IUser> Users => _users;

    public bool DeleteUser(string name)
    {
        var user = _users.SingleOrDefault(user => user.Name == name);
        user?.Delete();
        return user != null;
    }

    public void DeleteAllUsers()
    {
        foreach (var user in _users)
        {
            user.Delete();
        }
    }

    readonly IReadOnlyCollection<IUserWithDelete> _users;
}

内部 class NetworkController 类似于:

class NetworkController
{
    public NetworkController(IUserManagerWithDelete userManager)
    {
        _userManager = userManager;
    }

    public bool ReceiveDeleteMessage(string name)
    {
        return _userManager.DeleteUser(name);
    }

    readonly IUserManagerWithDelete _userManager;
}

请注意,出于说明目的,我将 DeleteUser() 的 return 类型设为 bool 以指示用户是否实际被删除(但它仍会抛出异常如果列表中有多个同名用户)。

我想我找到了一个与@matthew-watson 的回答更进一步的解决方案

public interface IInternalUserManager : IUserManager
{
    new IReadOnlyCollection<IUserWithDelete> Users { get; }
}

public class UserManager : IInternalUserManager
{
    public IReadOnlyCollection<IUserWithDelete> Users => _users;
    IReadOnlyCollection<IUser> IUserManager.Users => _users;
}

这允许我将 IInternalUserManager 注入 NetworkController,同时使用与插件容器中的 IUserManager 相同的对象,而无需为同一集合使用重复的 属性 名称。