问题 我应该如何建模我的代码以在这种特定情况下最大化代码重用?


更新:请参阅问题的结尾,了解我如何实施解决方案。

对于措辞不好的问题,我很抱歉,但我不确定如何最好地提出这个问题。我不确定如何设计一个可以重复使用的解决方案,其中大多数代码在每次实现时都完全相同,但实现的一部分每次都会改变,但遵循类似的模式。我试图避免复制和粘贴代码。

我们有一个内部数据消息系统,用于在不同机器上跨数据库更新表。我们正在扩展我们的消息服务以向外部供应商发送数据,我想编写一个简单的解决方案,如果我们决定向多个供应商发送数据,可以重复使用。代码将被编译为EXE并定期运行以将消息发送到供应商的数据服务。

以下是代码的大致概述:

public class OutboxManager 
{
    private List<OutboxMsg> _OutboxMsgs;

    public void DistributeOutboxMessages()
    {
        try {
            RetrieveMessages();
            SendMessagesToVendor();
            MarkMessagesAsProcessed();
        }
        catch Exception ex {
            LogErrorMessageInDb(ex);
        }
    }

    private void RetrieveMessages() 
    {
      //retrieve messages from the database; poplate _OutboxMsgs.
      //This code stays the same in each implementation.
    }

    private void SendMessagesToVendor()   // <== THIS CODE CHANGES EACH IMPLEMENTATION
    {
      //vendor-specific code goes here.
      //This code is specific to each implementation.
    }

    private void MarkMessagesAsProcessed()
    {
      //If SendMessageToVendor() worked, run this method to update this db.
      //This code stays the same in each implementation.
    }

    private void LogErrorMessageInDb(Exception ex)
    {
      //This code writes an error message to the database
      //This code stays the same in each implementation.
    }
}

我想以这样一种方式编写这段代码,即我可以重新使用不会改变的部分,而不必诉诸于复制和粘贴并填写代码 SendMessagesToVendor()。我希望开发人员能够使用 OutboxManager 并且已经编写了所有编写的数据库代码,但是被迫提供他们自己的向供应商发送数据的实现。

我确信有很好的面向对象原则可以帮助我解决这个问题,但我不确定哪一个最好用。


这是我最终选择的解决方案,受到启发 维克多的答案 和 里德的回答(和评论) 使用接口模型。所有相同的方法都存在,但现在它们隐藏在消费者可以根据需要更新的界面中。

直到我意识到我允许类的使用者插入自己的类进行数据访问时,我才意识到接口实现的强大功能(IOutboxMgrDataProvider)和错误记录(IErrorLogger)。虽然我仍然提供默认实现,因为我不希望这些代码发生变化,但消费者仍然可以使用自己的代码覆盖它们。除了写出多个构造函数(其中 我可以更改为命名和可选参数),它确实没有花很多时间来改变我的实现。

public class OutboxManager
{
    private IEnumerable<OutboxMsg> _OutboxMsgs;
    private IOutboxMgrDataProvider _OutboxMgrDataProvider;
    private IVendorMessenger _VendorMessenger;
    private IErrorLogger _ErrorLogger;

    //This is the default constructor, forcing the consumer to provide
    //the implementation of IVendorMessenger.
    public OutboxManager(IVendorMessenger messenger)
    {
         _VendorMessenger = messenger;
         _OutboxMgrDataProvider = new DefaultOutboxMgrDataProvider();
         _ErrorLogger = new DefaultErrorLogger();
    }

    //... Other constructors here that have parameters for DataProvider
    //    and ErrorLogger.

    public void DistributeOutboxMessages()
    {
         try {
              _OutboxMsgs = _OutboxMgrDataProvider.RetrieveMessages();
              foreach om in _OutboxMsgs
              {
                  if (_VendorMessenger.SendMessageToVendor(om))
                      _OutboxMgrDataProvider.MarkMessageAsProcessed(om)
              }
         }
         catch Exception ex {
             _ErrorLogger.LogErrorMessage(ex)
         }
    }

}

//...interface code: IVendorMessenger, IOutboxMgrDataProvider, IErrorLogger
//...default implementations: DefaultOutboxMgrDataProvider(),
//                            DefaultErrorLogger()

8690
2017-08-02 16:12


起源



答案:


我会说使用 依赖注射。基本上,您传递了send方法的抽象。

就像是:

interface IVendorMessageSender
{
    void SendMessage(Vendor v);
}

public class OutboxManager 
{
    IVendorMessageSender _sender;

    public  OutboxManager(IVendorMessageSender sender)
    {
        this._sender = sender; //Use it in other methods to call the concrete implementation
    }

    ...
}

另一种方法,如前所述,继承。

在任何一种情况下:尝试从此类中删除数据库检索代码。使用另一个抽象(即:将IDataProvider接口或类似的东西传递给构造函数)。它将使您的代码更易于测试。


1
2017-08-02 17:45



@Victor感谢您的建议。依赖注入优于抽象模型的优势是什么? - Ben McCormack
@Ben:他们真的只是两种工作方式。 DI的优势在于允许您的客户始终使用单个类,并且可能更容易进行测试。使用继承可能更清楚,因为用户使用单个类,Liskov Substition Principle表示它们可以在用户端互换使用 - 因此用户可以看到一个类与两个类。 - Reed Copsey
@Ben:我的答案基本上只是这个 - 选项1是“抽象模型”/继承,选项2是DI。 - Reed Copsey
我会说可测试性。对于派生类,您不能模拟基类,但在DI的情况下,您可以替换注入的类。 - Victor Hurdugaci
接口模型开始陷入困境,我真的很喜欢你如何在调用代码中提供接口到“基类”。我打算试一试。多谢你们! - Ben McCormack


答案:


我会说使用 依赖注射。基本上,您传递了send方法的抽象。

就像是:

interface IVendorMessageSender
{
    void SendMessage(Vendor v);
}

public class OutboxManager 
{
    IVendorMessageSender _sender;

    public  OutboxManager(IVendorMessageSender sender)
    {
        this._sender = sender; //Use it in other methods to call the concrete implementation
    }

    ...
}

另一种方法,如前所述,继承。

在任何一种情况下:尝试从此类中删除数据库检索代码。使用另一个抽象(即:将IDataProvider接口或类似的东西传递给构造函数)。它将使您的代码更易于测试。


1
2017-08-02 17:45



@Victor感谢您的建议。依赖注入优于抽象模型的优势是什么? - Ben McCormack
@Ben:他们真的只是两种工作方式。 DI的优势在于允许您的客户始终使用单个类,并且可能更容易进行测试。使用继承可能更清楚,因为用户使用单个类,Liskov Substition Principle表示它们可以在用户端互换使用 - 因此用户可以看到一个类与两个类。 - Reed Copsey
@Ben:我的答案基本上只是这个 - 选项1是“抽象模型”/继承,选项2是DI。 - Reed Copsey
我会说可测试性。对于派生类,您不能模拟基类,但在DI的情况下,您可以替换注入的类。 - Victor Hurdugaci
接口模型开始陷入困境,我真的很喜欢你如何在调用代码中提供接口到“基类”。我打算试一试。多谢你们! - Ben McCormack


有两种非常简单的方法:

  1. 使 OutboxManager 一个抽象类,并为每个供应商提供一个子类。该 SendMessagesToVendor 可以标记为抽象,强制每个供应商重新实现它。这种方法很简单,很好地符合OO原则,并且还具有允许您为其他方法提供实现的优点,但如果您希望稍后允许,仍允许覆盖供应商特定版本。

  2. OutboxManager 封装一些其他类或接口,提供所需的供应商特定信息 SendMessagesToVendor。这可以很容易地成为每个供应商实现的小型接口 SendMessagesToVendor 可以使用此接口实现来发送其消息。这样做的好处是允许您在此处编写一些代码 - 可能会减少供应商之间的重复。它也可能允许你的 SendMessagesToVendor 方法更加一致,更容易测试,因为您只需依赖此处所需的特定供应商功能。这也可以作为一个委托作为相关(但略有不同)的方法传入(我个人更喜欢通过委托实现的接口)。


9
2017-08-02 16:19



+1用于以简短明了的方式提出两个好的替代方案。 - Doc Brown
你能回答吗? stackoverflow.com/questions/9511137/... ? - Lijo


如果将其设为抽象基类,则必须继承它,可以强制在具体对象中实现此方法。

using System;
using System.Collections.Generic;

public abstract class OutboxManagerBase
{
private List<string> _OutboxMsgs;

public DistributeOutboxMessages()
{
    try {
        RetrieveMessages();
        SendMessagesToVendor();
        MarkMessagesAsProcessed();
    }
    catch Exception ex {
        LogErrorMessageInDb(ex);
    }
}

private void RetrieveMessages() 
{
  //retrieve messages from the database; poplate _OutboxMsgs.
  //This code stays the same in each implementation.
}

protected abstract void SendMessagesToVendor();

private void MarkMessagesAsProcessed()
{
  //If SendMessageToVendor() worked, run this method to update this db.
  //This code stays the same in each implementation.
}

private void LogErrorMessageInDb(Exception ex)
{
  //This code writes an error message to the database
  //This code stays the same in each implementation.
}
}



public class OutBoxImp1 : OutboxManagerBase
{
    protected override void SendMessagesToVendor()
    {
        throw new NotImplementedException();
    }
}

2
2017-08-02 16:22





你可以做到的一种方法是使用接口。

public interface IVendorSender
{
    IEnumerable<OutboxMsg> GetMessages();
}

然后在构造函数中将实例作为参数。

public class OutboxManager 
{
    private readonly IVendorSender _vendorSender; 

    public OutboxManager(IVendorSender vendorSender)
    {
        _vendorSender = vendorSender ?? new DefaultSender();
    }

    private void SendMessagesToVendor()   // <== THIS CODE CHANGES EACH IMPLEMENTATION
    {
        _vendorSender.GetMessages(); // Do stuff...
    }    
}

1
2017-08-02 16:19





它看起来像你在那里的大部分路。

一些基本步骤:

1无论供应商是什么,都要弄清楚代码的哪些部分是相同的。
2将它们写入可重复使用的模块(可能是.dll)
3确定每个供应商的更改。
4确定(上述)代码 - 为此编写特定模块。
5确定(上述)配置 - 为这些配置方案创建配置方案。

然后,您的.exe将正确地调用相应的 OutboxManager 对象为正确的供应商。


0
2017-08-02 16:20





创建一个抽象基类,并将需要更改的方法更改为抽象保护,例如

public abstract class OutboxManager 
{
    private List<OutboxMsg> _OutboxMsgs;

    public void DistributeOutboxMessages()
{
    try {
        RetrieveMessages();
        SendMessagesToVendor();
        MarkMessagesAsProcessed();
    }
    catch (Exception ex) {
        LogErrorMessageInDb(ex);
    }
 }

    private void RetrieveMessages() 
    {
      //retrieve messages from the database; poplate _OutboxMsgs.
      //This code stays the same in each implementation.
    }

    protected abstract void SendMessagesToVendor();   // <== THIS CODE CHANGES EACH IMPLEMENTATION


    private void MarkMessagesAsProcessed()
    {
      //If SendMessageToVendor() worked, run this method to update this db.
      //This code stays the same in each implementation.
    }

    private void LogErrorMessageInDb(Exception ex)
    {
      //This code writes an error message to the database
      //This code stays the same in each implementation.
    }
}

每个实现都继承自此抽象类,但仅提供SendMessagesToVendor()的实现,共享实现在抽象基类中定义。


0
2017-08-02 16:24



您应该从抽象方法中删除实现... - Reed Copsey
是的,你是对的,复制和粘贴太快了。我现在更新了。 - Ben Robinson


同样来自Copsey先生。清单解决方案#1确实是分类。无论是运气还是技巧,您已经构建了代码以使其易于实现。

根据供应商之间差异的性质,如果有很多共同的功能,另一种选择可能是拥有一个包含每个供应商记录的数据库,并且有几个控制处理的标志。如果你可以把它分解为“如果flag1是真的那么做其他事做B事情;总是做事情C;如果flag2是真的做事情D其他我们已经完成了”,然后而不是重复一堆代码跨供应商你或许可以让数据控制处理。

哦,我可能会添加或许显而易见:如果唯一的区别是数据值,那么当然只是将数据值存储在某处。想要一个简单的例子,如果供应商之间的唯一区别是您连接的域名,那么只需创建一个包含vendorid和域名的表,读取值并将其插入。


0
2017-08-02 17:00