我有工厂类,并使用生成器为工厂类方法创建类。在下面的示例中,我将自定义生成器替换为StringBuilder以简化示例。
在创建生成器到类之前,有一些行为是相同的。我不想写重复的代码,所以我创建了基类来封装方法和派生方法,或者委托给生成器
因此,子类可以重写方法来挂接生成器和操作生成器
完整代码。
public abstract class FactoryBase
{
protected delegate void HookSomeStringHandler(StringBuilder builder);
protected HookSomeStringHandler OnHookSomeStringHandler;
/// <summary>
/// You can override <see cref="InnerHookSomeString"/> to hook builder.
/// </summary>
public string GetSomeStringA()
{
var sb = new StringBuilder();
sb.Append(GetType().Name); // need all child class name
InnerHookSomeString(sb); // hook StringBuilder to append some string
return sb.ToString();
}
/// <summary>
/// Child class can override this to hook StringBuilder <see cref="GetSomeStringA"/>
/// </summary>
protected virtual void InnerHookSomeString(StringBuilder builder)
{
}
/// <summary>
/// You can override method to hook stringBuilder or using delegate action to hook stringBuilder.
/// </summary>
public virtual string GetSomeStringB(Action<StringBuilder> outerHook)
{
var sb = new StringBuilder();
sb.Append(GetType().Name); // need all child class name
outerHook?.Invoke(sb); // hook StringBuilder to append some string
return sb.ToString();
}
/// <summary>
/// Use register delegate to hook stringBuilder. <see cref="OnHookSomeStringHandler"/>
/// </summary>
public string GetSomeStringC()
{
var sb = new StringBuilder();
sb.Append(GetType().Name); // need all child class name
OnHookSomeStringHandler?.Invoke(sb); // hook StringBuilder to append some string
return sb.ToString();
}
}
public class ChildA : FactoryBase
{
public ChildA()
{
OnHookSomeStringHandler += (sb) =>
{
// TODO do something by GetSomeStringC
};
}
protected override void InnerHookSomeString(StringBuilder builder)
{
// TODO do something by GetSomeStringA
}
public override string GetSomeStringB(Action<StringBuilder> outerHook)
{
return base.GetSomeStringB((sb) =>
{
// TODO do something by GetSomeStringB
});
}
}
注意:GetSomeString
中的Builder不需要每次都添加字符串或做一些事情,所以我不需要使用抽象方法来强制必须重写的子类
我对这种情况有三个理想
GetSomeStringA
使用InnerHookSomeString
来挂接StringBuilder
,子类可以操作生成器,但这种编写方式可能用户不知道这种方法,因此需要使用标记<see cref>
GetSomeStringB
使用override来挂接StringBuilder
,并且可以在外部挂接构建器,但这种编写方式看起来很难看GetSomeStringC
类似于GetSomeStringA
,它是通过注册一个委托来完成的,也需要使用标记来提示用户
以上三种方法中哪一种更易于维护或可读
有人有更好的主意或建议吗?
这取决于您的意愿。一般来说,这三种解决方案都是糟糕的设计
此外,如果您提供了上下文,则术语或名称Factory似乎不合适。我没有看到任何正在创建的实例。我只是看到一些线组。这个类应该命名为SomeStringCreator
。正在命名一个类。。。Factory意味着该类型是Factory模式的实现,就像命名一个类。。。Builder意味着该类实现了Builder模式。
为了更好地理解,让我们假设我们想要实现一个Logger
类。此记录器有一个公共Log(string message)
方法。在内部,Logger
能够将输出路由到特定的数据宿,例如文件或数据库。Logger
的客户端是想要记录消息的普通开发人员。但是允许开发者/继承者扩展或修改CCD_ 15的行为,例如改变数据汇。
如果您想要一个抽象基类来提供/封装一些常见行为,那么2)和3)就不起作用(好)。
abstract
类意味着该类将不提供现成的行为。虽然已经通过private
、protected
或virtual
成员提供了一些基本逻辑,但缺少的逻辑需要由继承者来实现
如果该类已准备好使用,则不会将其声明为abstract
,只会在需要扩展性的地方提供virtual
成员。
2)
此解决方案通过公共方法的参数公开可扩展行为,使行为公开:
// Forces the caller to mix high-level and low-level details in a high-level context
public void Log(string message, Action<string> persistMessage)
{
var formattedMessage = AddHeaderToMessage(message);
persistMessage.Invoke(formattedMessage);
}
此示例强制API的调用方关心内部(低级),即用于实现类目标的逻辑,即记录消息(高级)。这不是基类的用途(将内部委托给公共API),也不是干净类API的设计方式。
内部(类如何实现其目标的逻辑)必须隐藏(private
或protected
)。这就是封装
当方法要在高级上下文中操作时,类的逻辑(低级细节)不应作为方法参数注入。在我们的示例中,客户端只想记录消息,而不实现或提供持久性逻辑的实现。他不想混合日志记录(高级)和日志记录实现(低级)。
3)
不是很方便。请注意,通常基类应该始终提供有用的默认逻辑来实现其目的。这意味着委托必须至少初始化。委托之所以是一个糟糕的选择,是因为它不是提供可扩展性时所期望的方式。开发人员总是在寻找要覆盖的虚拟方法。委托可以允许调用者/客户端定义回调。
1)
在打算由继承人扩展的类的上下文中,解决方案1)是正确的方法。但是您当前的实现很容易出错
请注意,通常基类应该始终提供有用的默认逻辑来实现其目的(否则使用接口)。abstract
基类应该声明实现目标所需的所有成员abstract
,以便强制继承者提供实现或提供virtual
默认实现:
// WRONG
public void Log(string message)
{
var formattedMessage = AddHeaderToMessage(message);
// Will fail silently, if the inheritor forgets to override this member
PersistMessage(formattedMessage);
}
protected virtual void PersistMessage(string message)
{
}
要么提供默认实现:
// Right
public void Log(string message)
{
var formattedMessage = AddHeaderToMessage(message);
// Can't fail, because the base class provides a default implementation
PersistMessage(formattedMessage);
}
protected virtual void PersistMessage(string message)
{
// Default implementation
SaveToFile(message);
}
或者使成员abstract
:
// Right
public void Log(string message)
{
var formattedMessage = AddHeaderToMessage(message);
// Can't fail, because the inheritor is forced by the compiler to override this member
PersistMessage(formattedMessage);
}
protected abstract void PersistMessage(string message);
或者让未实现的成员抛出异常
只有在前两个解决方案不起作用时才使用此解决方案,因此通常不要使用此解决方法。关键是,只有在运行时才会抛出异常,而abstract
类的缺失覆盖正在生成编译时错误:
// Right
public void Log(string message)
{
var formattedMessage = AddHeaderToMessage(message);
// Forced to fail at run-time, because the default implementation
// will throw a NotImplementedException (non-silent fail)
PersistMessage(formattedMessage);
}
protected virtual void PersistMessage(string message)
{
throw new NotImplementedException();
}
如果您想使类在与API交互时可为客户端扩展,那么2)当然是解决方案
例如,如果您希望客户端能够修改记录消息的格式,例如要使用哪些标头或标记或它们的出现顺序,那么您将允许该方法接受相关逻辑或配置作为参数。此参数可以是委托、配置对象或使用占位符(如"<timestamp><callerContext><errorLevel> - <message>"
:)的格式字符串
public void Log(string message, string formatPattern)
{
var formattedMessage = AddHeaderToMessage(message, formatPattern);
PersistMessage(formattedMessage);
}
protected virtual void PersistMessage(string formattedMessage)
{
SaveToFile(message);
}
为了保持API的干净,考虑公开公共属性和/或构造函数重载,以使用例如委托或配置对象/参数来配置实例
// Constructor
public Logger(string formatPattern)
{
_formatPattern = formatPattern;
}
public void Log(string message)
{
var formattedMessage = AddHeaderToMessage(message, _formatPattern);
PersistMessage(formattedMessage);
}
protected virtual void PersistMessage(string formattedMessage)
{
SaveToFile(message);
}
请注意,这两个解决方案在相同级别的详细信息上运行:所有参数都与日志消息有关,而与内部实现详细信息无关,如消息的实际持久化方式。在这种情况下,关于日志记录本身的合理详细程度将是一个配置参数,用于控制使用哪个数据汇,例如电子邮件或数据库。