使用工厂和构建器模式创建一些 class 最佳方法

Use factory and builder pattern to create some class the best way approach

我有工厂 class 并使用构建器为工厂 class 方法创建 class。在下面的示例中,我将自定义生成器替换为 StringBuilder 以简化示例。

在为 class 创建构建器之前,有些行为是相同的。我不想写重复的代码,所以我创建基工厂class来封装方法和派生方法或委托给构建器。
所以childclass可以覆盖方法来hook builder和操作builder。

完整代码。

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不需要每次都加一个字符串或者做一些事情所以我没有使用抽象方法强制执行childclass 必须覆盖。

我对这种情况有3个理想。

  1. GetSomeStringAInnerHookSomeStringhook StringBuilder和childclass可以操作builder但是这种写法可能用户不不知道这种方法所以需要使用标签 <see cref>.

  2. GetSomeStringB 使用override hook StringBuilder 可以在外面hook builder 但是这种写法很难看

  3. GetSomeStringC 类似于GetSomeStringA,是通过注册一个delegate来完成的,也需要使用tag来提示用户。

以上三种方法,哪一种更好维护或可读?
有没有人有更好的想法或建议?

这取决于你的意图。通常这三种解决方案都是糟糕的设计。
此外,鉴于您提供的上下文,术语或名称 Factory 似乎不合适。我没有看到正在创建任何实例。我只看到一些字符串组件。这个class应该命名为SomeStringCreator。命名一个 class ...Factory 意味着该类型是工厂模式的实现,就像命名一个 class ...Builder 意味着 class 实现了 Builder 模式。

为了更好地理解,我们假设我们要实现 Logger class。这个记录器有一个 public Log(string message) 方法。在内部 Logger 能够将输出路由到特定的数据接收器,例如文件或数据库。 Logger 的客户端是想要记录消息的普通开发人员。但是 developers/inheritors 可以扩展或修改 Logger 的行为,例如更改数据接收器。


如果您打算抽象基础classprovides/encapsulates一些常见行为,那么2)3) 不起作用(很好)。

abstract class 表示 class 不会提供 ready-to 使用行为。缺少的逻辑需要由继承者实现,尽管一些基本逻辑已经通过 privateprotectedvirtual 成员提供。
如果 class 是 ready-to-use,那么它不会被声明为 abstract,并且只会在需要扩展性的地方提供 virtual 成员。

2)
此解决方案通过 public 方法的参数公开可扩展行为,使行为 public:

// 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 的调用者关心内部结构 (low-level),即用于实现 class 目标的逻辑,即记录消息(high-level)。这不是基础 class 的用途(将内部结构委托给 public API),也不是通常应该如何设计干净的 class API。

必须隐藏内部结构(class 实现其目标的逻辑)(privateprotected)。这就是封装。
当方法打算在 high-level 上下文中运行时,class 的逻辑(low-level 细节)不应作为方法参数注入。在我们的示例中,客户端只想记录一条消息,而不是实现或提供持久性逻辑的实现。他不想混合使用日志记录 (high-level) 和记录器实现 (low-level)。

3)
不是很方便。请注意,通常 base class 应该始终提供有用的默认逻辑来实现其目的。这意味着至少必须初始化委托。使委托成为错误选择的原因是它在提供可扩展性时不是预期的方式。开发人员总是在寻找要覆盖的虚拟方法。委托允许 caller/client 定义回调。

1)
在打算由继承者扩展的 class 的上下文中,解决方案 1) 是正确的方法。但是您当前的实现很容易出错。
请注意,通常 base class 应该始终提供有用的默认逻辑来实现其目的(否则使用接口)。 abstract 基础 class 应该声明所有必需的成员来实现目标也 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);

或者让未实现的成员抛出异常。
仅当前面两种解决方案都不起作用时才使用此解决方案,因此一般不要使用此解决方案。关键是异常仅在 run-time 处抛出,而 abstract class 的缺失覆盖正在生成 compile-time 错误:

// 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();
}

如果你想让 class 对客户端可扩展,当与 API 交互时,当然 2) 是解决方案一起去。
例如,如果您希望客户端能够修改记录消息的格式,例如要使用哪个 headers 或标签或它们的出现顺序,那么您将允许该方法接受相关逻辑或配置作为参数。此参数可以是委托、配置 object 或使用 "<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 干净,考虑公开 public 属性 and/or 构造函数重载以使用例如委托或配置 object/parameter 配置实例:

// 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);
}

请注意,这两种解决方案都在相同级别的细节上运行:所有参数都与日志消息相关,而不与内部实现细节相关,例如 如何 消息实际上是持久化的。在这种情况下,关于日志记录本身的合理详细程度将是一个配置参数,用于控制使用哪个数据接收器,例如电子邮件或数据库。