How should I model my code to maximize code re-use

2020-02-06 05:42发布

Updated: See end of question for how I implemented the solution.

Sorry for the poorly-worded question, but I wasn't sure how best to ask it. I'm not sure how to design a solution that can be re-used where most of the code is the exact same each time it is implemented, but part of the implementation will change every time, but follow similar patterns. I'm trying to avoid copying and pasting code.

We have an internal data messaging system for updating tables across databases on different machines. We're expanding our messaging service to send data to external vendors and I want to code a simple solution that can be re-used should we decide to send data to more than one vendor. The code will be compiled into an EXE and run on a regular basis to send messages to the vendor's data service.

Here's a rough outline of what the code does:

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.
    }
}

I want to write this code in such a way that I can re-use the parts that don't change without having to resort to copying and pasting and filling in the code for SendMessagesToVendor(). I want a developer to be able to use an OutboxManager and have all of the database code written already written, but be forced to supply their own implementation of sending data to the vendor.

I'm sure there are good object-oriented principles that can help me solve that problem, but I'm not sure which one(s) would be best to use.


This is the solution I ended up going with, inspired by Victor's answer and Reed's answer (and comments) to use an interface model. All of the same methods are there, but now they are tucked away into interfaces that the consumer can update if necessary.

I didn't realize the power of the interface implementation until I realized that I allow the consumer of the class to plug in their own classes for the data access (IOutboxMgrDataProvider) and error logging (IErrorLogger). While I still provide default implementations since I don't expect this code to change, it's still possible for the consumer to override them with their own code. Except for writing out multiple constructors (which I may change to named and optional parameters), it really didn't take a lot of time to change my implementation.

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()

7条回答
欢心
2楼-- · 2020-02-06 06:14

I would say use Dependecy Injection. Basically, you pass an abstraction of the send method.

Something like:

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
    }

    ...
}

Another approach, as already mentioned, inheritance.

In either case: try to remove DB retrieval code from this class. Use another abstraction for that (ie: passing an IDataProvider interface or something like that to the constructor). It will make your code more testable.

查看更多
Emotional °昔
3楼-- · 2020-02-06 06:19

There are two very simple approaches:

  1. Make OutboxManager an abstract class, and provide a subclass per vendor. The SendMessagesToVendor can be marked abstract, forcing it to be reimplemented by each vendor. This approach is simple, fits OO principles well, and also has the advantage of allowing you to supply the implementation for the other methods, but still allowing overriding for a vendor specific version if you want to allow that later.

  2. Have OutboxManager encapsulate some other class or interface which provides the vendor-specific information required in SendMessagesToVendor. This could easily be a small interface that is implemented per-vendor, and SendMessagesToVendor could use this interface implementation to send its messages. This has the advantage of allowing you to write some of the code here - potentially reducing duplication across vendors. It also potentially allows your SendMessagesToVendor method to be more consistent, and more easily testable, since you only have to rely on the specific vendor functionality required here. This could also, potentially, be implemented as a delegate passed in as a related (but slightly different) approach (I personally prefer an interface to be implemented over a delegate, however).

查看更多
做自己的国王
4楼-- · 2020-02-06 06:19

Ditto to Mr Copsey. Manifest solution #1 is indeed sub-classing. You have, whether by luck or skill, already structured your code to make this easy to implement.

Depending on the nature of the differences between vendors, if there is a lot of common functionality, another alternative might be to have a database with a record for each vendor, and have a couple of flags that control processing. If you can break it down to "if flag1 is true do thing A else do thing B; always do thing C; if flag2 is true do thing D else we're done", then rather than repeating a bunch of code across vendors you may be able to let data control the processing.

Oh, and I might add the perhaps obvious: If the only difference is data values, then of course just store the data values somewhere. Like to take a trivial example, if the only difference between vendors is the domain name that you connect to, then just create a table with vendorid and domain name, read the value and plug it in.

查看更多
Anthone
5楼-- · 2020-02-06 06:25

It looks to me like you're most of the way there.

Some basic steps:

1 Figure out what parts of your code are the same no matter what the vendor.
2 Write those into a re-usable module (probably a .dll)
3 Determine what changes per vendor.
4 Determine what (of the above) is code - write specific modules for that.
5 Determine what (of the above) is configuration - create a config scheme for those.

Your .exe will then acually call the appropriate OutboxManager object for the correct vendor.

查看更多
我想做一个坏孩纸
6楼-- · 2020-02-06 06:35

Create an abstract base class and have the method that needs to be changed as abstract protected e.g.

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.
    }
}

Each implementation inherits from this abstract class but only provides the implementation for SendMessagesToVendor() the shared implementation is defined in the abstract base class.

查看更多
Root(大扎)
7楼-- · 2020-02-06 06:36

If you make this an abstract base class so it has to be inherited you can force this method to be implemented in the concrete object.

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();
    }
}
查看更多
登录 后发表回答