如何处理因实现类而异的静态字段
我一直都遇到这个问题。 假设我正在创建一个命令行界面(Java或C#,我认为问题是一样的,我会在这里展示C#)。
- 我定义了一个接口ICommand
- 我创建了一个抽象基类CommandBase,它实现了ICommand,以包含公共代码。
- 我创建了几个实现类,每个类都扩展了基类(扩展了接口)。
现在 – 假设接口指定所有命令都实现Name属性和Execute方法……
对于Name,我的每个实例类都必须返回一个字符串,该字符串是该命令的名称。 该字符串(“HELP”,“PRINT”等)对相关类是静态的。 我希望能够做的是定义:
public abstract static const string Name;
但是(遗憾的是)您无法在界面中定义静态成员。
多年来我一直在努力解决这个问题(几乎任何地方我都有类似的家庭),所以我将在下面发布我自己的3个可能的解决方案进行投票。 然而,因为它们都不是理想的我希望有人会发布一个更优雅的解决方案。
更新:
- 我无法让代码格式化正常工作(Safari / Mac?)。 道歉。
-
我使用的例子是微不足道的。 在现实生活中,有时会有几十个实现类和几个半静态类型的字段(即实现类的静态)。
-
我忘了提 – 理想情况下我希望能够静态查询这些信息:
string name = CommandHelp.Name;
我提出的3个解决方案中有2个要求在找到丑陋的静态信息之前对类进行实例化。
您可以考虑使用属性而不是字段。
[Command("HELP")] class HelpCommand : ICommand { }
如您所述,无法从界面级别强制执行此操作。 但是,由于您使用的是抽象类,因此您可以做的是在基类中将该属性声明为抽象,这将强制它覆盖它的inheritance类。 在C#中,它看起来像这样:
public abstract class MyBaseClass { public abstract string Name { get; protected set; } } public class MyClass : MyBaseClass { public override string Name { get { return "CommandName"; } protected set { } } }
(请注意, 受保护的集会阻止外部代码更改名称。)
这可能不是你想要的,但它与我认为你能得到的一样接近。 根据定义,静态字段不会变化; 对于给定的类,您根本不能拥有静态和可覆盖的成员。
public interface ICommand { String getName(); } public class RealCommand implements ICommand { public String getName() { return "name"; } }
就那么简单。 为什么要打扰静态字段呢?
Obs。:不要在抽象类中使用应该在子类中启动的字段(如David B建议)。 如果有人扩展抽象类并忘记启动该字段会怎么样?
只需将name属性添加到基类并将其传递给基类的构造函数,并让派生类的构造函数传递给它的命令名称
我通常做的(伪):
abstract class: private const string nameConstant = "ABSTRACT"; public string Name { get {return this.GetName();} } protected virtual string GetName() { return MyAbstractClass.nameConstant; } ---- class ChildClass : MyAbstractClass { private const string nameConstant = "ChildClass"; protected override string GetName() { return ChildClass.nameConstant; } }
当然,如果这是其他开发人员将使用的库,那么如果在属性中添加一些reflection来validation当前实例确实实现了覆盖或抛出exception“未实现”,则不会受到影响。
我的回答将与Java有关,因为这就是我所知道的。 接口描述行为,而不是实现。 此外,静态字段与类绑定,而不是与实例绑定。 如果您声明了以下内容:
interface A { abstract static NAME } class B { NAME = "HELP" } class C { NAME = "PRINT" }
那么这段代码怎么知道链接到哪个NAME:
void test(A a) { a.NAME; }
我建议如何实现这一点,是以下方法之一:
- 类名约定,基类从类名派生名称。 如果您希望偏离此,请直接覆盖界面。
- 基类有一个名字的构造函数
- 使用注释并通过基类强制执行它们的存在。
但是,一个更好的解决方案可能是使用枚举:
public enum Command { HELP { execute() }, PRINT { execute() }; abstract void execute(); }
这样更清晰,并允许您使用switch语句,并且可以轻松派生NAME。 但是,您无法扩展选项运行时的数量,而是从您甚至可能不需要的方案描述中扩展。
[建议回答#3 of 3]
我还没有尝试过,它在Java中不会那么好(我想?)但是我可以用Attributes标记我的类:
[CammandAttribute(名称= “HELP”)]
然后我可以使用reflection来获取静态信息。 需要一些简单的帮助方法来使类的客户端可以轻松获得信息,但这可以放在基类中。
从设计的角度来看,我认为要求一个静态实现成员是错误的…静态和非示例字符串之间的性能和内存使用之间的相对差异是最小的。 除此之外,我理解在实施过程中,有问题的对象可能会有更大的足迹……
基本问题是,通过尝试设置模型以支持静态实现成员,这些成员在C#的基础或接口级别是可用的,我们的选项是有限的…在接口级别只有属性和方法可用。
下一个设计挑战是代码是基于还是特定于实现。 通过实现,您的模型将在编译时获得一些在所有实现中必须包含类似逻辑的代码的valdiation。 有了base,你的valdiation将在运行时发生,但逻辑将集中在一个地方。 不幸的是,给定的示例是实现特定代码的完美展示案例,因为没有与数据相关联的逻辑。
因此,为了示例,我们假设有一些与数据相关联的实际逻辑,并且它足够广泛或足够复杂,无法为基类提供展示。 假设基类逻辑是否使用任何强制细节,我们遇到了确保实现静态初始化的问题。 我建议在基类中使用受保护的抽象来强制所有实现创建所需的静态数据,这些数据将在编译时进行validation。 我使用的所有IDE都可以非常快速地完成。 对于Visual Studio,只需点击几下鼠标,然后只是改变返回值。
回到问题的特定性质并忽略了许多其他设计问题……如果你真的必须保持这一整个静态数据的性质,并仍然通过问题的自然范围强制执行…绝对去使用属性方法,因为有很多副作用可以使用属性。 在基类上使用静态成员,并在实现上使用静态构造函数来设置名称。 现在请记住,您必须在运行时validation名称而不是编译时间。 基本上,基类上的GetName方法需要处理实现未设置名称时发生的情况。 它可能会引发一个例外,这使得它显然出现了一些问题,这个问题实际上是由测试/ QA而不是用户造成的。 或者您可以使用reflection来获取实现名称并尝试生成名称…reflection的问题是它可能影响子类并设置一个初级开发人员难以理解和维护的代码情况。 ..
对于这个问题,你总是可以通过reflection从类名生成名称……虽然从长远来看这可能是一个维持的噩梦……但是它会减少实现所需的代码量,这似乎更重要比任何其他问题。 你也可以在这里使用属性,但是你在实现中添加代码作为静态构造函数在时间/精力上是等效的,并且在实现不包含该信息时仍然存在问题。
这样的事情怎么样:
abstract class Command { abstract CommandInfo getInfo(); } class CommandInfo { string Name; string Description; Foo Bar; } class RunCommand { static CommandInfo Info = new CommandInfo() { Name = "Run", Foo = new Foo(42) }; override commandInfo getInfo() { return Info; } }
现在您可以静态访问信息:
RunCommand.Info.Name;
从你的基类:
getInfo().Name;
[建议的解决方案#1 of 3]
- 在接口中定义一个抽象属性Name,以强制所有实现类实现name属性。
- (在c#中)在基类中将此属性添加为abstract。
-
在实现中实现如下:
public string Name { get {return COMMAND_NAME;} }
其中name是该类中定义的常量。
好处:
- 名称本身定义为常量。
- 接口要求创建属性。
缺点:
- 重复(我讨厌)。 完全相同的属性访问器代码粘贴到我的每个实现中。 为什么不能进入基类以避免混乱?
[建议的解决方案#2 of 3]
- 创建一个私有成员变量名称。
- 在接口中定义抽象属性Name。
-
在基类中实现属性,如下所示:
public string Name { get {return Name;} }
-
在调用抽象基类构造函数时,强制所有实现将name作为构造函数参数传递:
public abstract class CommandBase(string commandName) : ICommand { name = commandName; }
-
现在我的所有实现都在构造函数中设置了名称:
public class CommandHelp : CommandBase(COMMAND_NAME) {}
好处:
- 我的访问者代码集中在基类中。
- 名称定义为常量
缺点
- Name现在是一个实例变量 – 我的Command类的每个实例都创建一个新的引用,而不是共享一个静态变量。