【问题标题】:Use factory and builder pattern to create some class the best way approach使用工厂和构建器模式创建一些类的最佳方法
【发布时间】:2020-11-22 01:23:14
【问题描述】:

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

在为类创建构建器之前,有些行为是相同的。我不想编写重复的代码,所以我创建了基工厂类来封装方法和派生方法或委托给构建器。
所以子类可以重写方法来钩住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 不需要每次都添加字符串或做某事,所以我不使用抽象方法来强制必须覆盖的子类。

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

  1. GetSomeStringA使用InnerHookSomeString钩住StringBuilder,子类可以操作builder,但是这种写法可能用户不知道这种方法,所以需要使用标签&lt;see cref&gt;

  2. GetSomeStringB 使用 override 来挂钩 StringBuilder 并且可以在外部挂钩 builder 但这种写法看起来很难看。

  3. GetSomeStringCGetSomeStringA类似,都是通过注册一个delegate来实现的,也需要使用tag来提示用户。

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

【问题讨论】:

    标签: c# oop inheritance design-patterns hook


    【解决方案1】:

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

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


    如果您打算拥有一个提供/封装一些常见行为的 abstract 基类,那么 2)3) 不要' t 工作(很好)。

    abstract 类表示该类将不提供即用型行为。尽管privateprotectedvirtual 成员已经提供了一些基本逻辑,但缺少的逻辑需要由继承者实现。
    如果该类已准备好使用,则不会将其声明为 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。

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

    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) 是可以使用的解决方案。
    例如,如果您希望客户端能够修改记录消息的格式,例如要使用哪些标头或标签或它们的出现顺序,那么您将允许该方法接受相关的逻辑或配置作为参数。此参数可以是委托、配置对象或使用占位符的格式字符串,例如"&lt;timestamp&gt;&lt;callerContext&gt;&lt;errorLevel&gt; - &lt;message&gt;"

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

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

    【讨论】:

    • 不错的答案,但我很抱歉,也许我的例子是误解。实际上我使用 builder Pattern 创建了一些类,但在这个例子中,我将自定义构建器替换为 StringBuilder 以简化示例..
    • 这并不重要。如果您将 StringReader 传递给方法,结果仍然相同:使用示例的修改后的第一个版本。请参阅我关于如何修改它的答案。你的问题有点令人困惑,所以我选择了一个像记录器这样的一般场景。离开你的场景似乎太抽象了。您在使用StringBuilder 作为参数而不是日志消息将我的答案转移到您的示例时遇到问题吗?重点不在于参数类型,而在于您设计旨在使用继承进行扩展的类的方式。
    • 最后我使用默认实现。我将相同的行为 sb.Append(GetType().Name) 移动到 InnerHookSomeString 方法中作为默认实现。就我而言,抽象方法不适合,因为我的子类可能不需要覆盖 InnerHookSomeString 方法。感谢您的详细解释。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-09-28
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多