使用接口时引用对象内部实现的最佳方式是什么?
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
不会暴露给它们。
回答您修改后的问题:
首先,我认为删除操作应该在用户管理器中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 相同的对象,而无需为同一集合使用重复的 属性 名称。
我有一个支持插件的项目。至于使插件可单元测试,它们仅通过接口与主应用程序交互。
主应用程序提供了这些接口的实现。此应用程序的不同组件之间也存在依赖关系。
我想通过只拥有插件开发所需的接口成员来限制接口成员的数量。问题是,有时一个实现需要调用一个我不想暴露给插件的方法。在“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
不会暴露给它们。
回答您修改后的问题:
首先,我认为删除操作应该在用户管理器中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 相同的对象,而无需为同一集合使用重复的 属性 名称。