c# - 使用工厂和构建器模式创建一些类的最佳方法
问题描述
我有工厂类并使用构建器为工厂类方法创建类。在下面的示例中,我将自定义构建器替换为 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
});
}
}
注意:中的BuilderGetSomeString
不需要每次都添加字符串或做某事,所以我不使用抽象方法来强制必须覆盖的子类。
我对这种情况有三个理想。
GetSomeStringA
使用InnerHookSomeString
to hookStringBuilder
和子类可以操作 builder 但是这种写法可能用户不知道这种方法所以需要使用 tag<see cref>
。GetSomeStringB
使用 override 来挂钩StringBuilder
并且可以在外部挂钩 builder 但这种写法看起来很难看。GetSomeStringC
与 类似GetSomeStringA
,通过注册一个委托来完成,也需要使用标签来提示用户。
上述三种方法中哪一种更好维护或可读性更好?
有没有人有更好的想法或建议?
解决方案
这取决于你的意图。一般来说,这三种解决方案都是糟糕的设计。
此外,鉴于您提供的上下文,工厂一词或名称似乎不合适。我没有看到正在创建任何实例。我只是看到一些字符串汇编。这个类应该命名为SomeStringCreator
. 命名一个类...Factory意味着该类型是 Factory 模式的实现,就像命名一个类...Builder 意味着该类实现了 Builder 模式。
为了更好地理解,让我们假设我们想要实现一个Logger
类。这个记录器有一个公共Log(string message)
方法。在内部,Logger
能够将输出路由到特定的数据接收器,例如文件或数据库。的客户端Logger
是想要记录消息的普通开发人员。但是允许开发人员/继承者扩展或修改Logger
例如更改数据接收器的行为。
如果您打算拥有一个提供/封装一些常见行为的抽象基类,那么2)和3)不起作用(很好)。
abstract
class 表示该类将不提供即用型行为。缺少的逻辑需要由继承者实现,尽管一些基本逻辑已经通过private
, protected
orvirtual
成员提供。
如果该类已准备好使用,则不会声明它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);
}
请注意,两种解决方案都在相同的细节级别上运行:所有参数都与日志消息相关,而不是与内部实现细节(例如消息实际持久化的方式)相关。在这种情况下,关于日志记录本身的合理细节级别将是一个配置参数,用于控制使用哪个数据接收器,例如电子邮件或数据库。
推荐阅读
- flutter - 如何使用 Flutter 定期显示本地通知?
- in-app-purchase - 为什么在Apple App购买中找不到服务器端的交易记录?
- python - Memcached 服务按预期工作,但统计项目为空
- mysql - 如何在mysql的表字段中搜索和替换特定段落?
- json - Fetch 无法收到大量响应
- javascript - 用于解析 Gcode 值的 javascript 正则表达式
- django - Django - 从 dev 中的 collect_static 文件夹中提供静态文件
- database - 如何在不损害整个数据库的情况下展示特定数据?
- linux - 当助手说“对于每个名称”时,它指的是什么?
- linux - 如何将自签名客户端证书添加到 haproxy 以进行双向 tls?