简体   繁体   中英

How to implement a generic method with multiple types?

I am working on accounting software, and I need help with generics. I have multiple document types and depending on the type different posting rules will apply. Now my question is how do I make everything generic?

I have tried this

public interface IDocument<IItem>
{
   public Guid Id {get;set;}
   public DocumentType DocumentType {get;set;} // enum
   public List<IItem> Items {get;set;}
}
public interface IItem
{
   public Guid Id {get;set;}
   public double Net {get;set;}
   public double Vat {get;set;}
   public double Gross {get;set;}
}
public class PostDocument
{
   public bool Post(IDocument<IItem> document)
   {
      foreach(item in document.Items)
      {
          // do something
      }
   }
}

The difficult thing here is that I will have multiple item classes because items for wholesale or retail are not the same(But every item class will have some common properties such as Net, Vat, and Gross). How would I get around it so I can have this generic method for all document types, so I don't have to write a method for every document type that my application will have?

If you don't expect to have thousands of document types, a reasonable way to approach this problem is to use the strategy pattern . There are a few different ways to approach the problem, but in a nutshell it is designed to swap one algorithm with another that has the same interface.

The pattern can also be used to switch between services that have a similar purpose based on a model object , similar to your IDocument<IItem> type.

Assumptions

You want to adhere to the Single Responsiblity Principle and Open/Closed Principle and take advantage of .NET generics.

It is certainly possible to achieve the following using casting, but it would involve modifying multiple types virtually every time you change something.

Document and Item Interfaces

First of all, we need some rework of the interfaces. For the design to work, we need both a generic and non-generic document interface. This gets us out of a common pitfall with trying to specify the closing type in places where it is not allowed, since C# generics are very strict and don't allow wildcards like some other languages do.

public interface IDocument
{
    public Guid Id { get; set; }
}

public interface IDocument<TItem> : IDocument
    where TItem : IItem
{
    public IList<TItem> Items { get; set; }
}

public interface IItem
{
    public Guid Id { get; set; }
    public decimal Net { get; set; }
    public decimal Vat { get; set; }
    public decimal Gross { get; set; }
}

We declare IDocument<TItem>: IDocument where TItem: IItem to ensure that the properties of the concrete implementation of IItem are available to the rest of our code without casting.

Note the DocumentType enum was removed, as it is not necessary and redundant since we can check a document type as follows:

IDocument document = new RetailDocument();
if (document is RetailDocument retailDocument)
    // do something with retailDocument

You can add it back if you feel you need it, though.

Concrete Document and Item Implementations

As others have pointed out in the comments, when dealing with currency we typically declare the type as decimal instead of double .

public class RetailItem : IItem
{
    public Guid Id { get; set; }
    public decimal Net { get; set; }
    public decimal Vat { get; set; }
    public decimal Gross { get; set; }

    // Other properties
    public string RetailStuff { get; set; }
}

public class WholesaleItem : IItem
{
    public Guid Id { get; set; }
    public decimal Net { get; set; }
    public decimal Vat { get; set; }
    public decimal Gross { get; set; }

    // Other properties
    public string WholesaleStuff { get; set; }
}

public class RetailDocument : IDocument<RetailItem>
{
    public Guid Id { get; set; }
    public IList<RetailItem> Items { get; set; }
}

public class WholesaleDocument : IDocument<WholesaleItem>
{
    public Guid Id { get; set; }
    public IList<WholesaleItem> Items { get; set; }
}

DocumentStrategy and DocumentPoster Interfaces

To make your strategy classes (the part that handles the post) generic, we need an interface for it. We also provide an (optional) IDocumentPoster interface, which would come in handy if you are using dependency injection.

PostDocument was named DocumentPoster because in C# we name methods after verbs and classes/properties after nouns.

public interface IDocumentStrategy
{
    bool Post<TDocument>(TDocument document) where TDocument : IDocument;
    bool AppliesTo(IDocument document);
}

public interface IDocumentPoster
{
    bool Post(IDocument document);
}

DocumentStrategy Abstraction

Here is an abstract class that is used to hide the ugly details of casting to the concrete IDocument type so we can access the strongly-typed properties within the strategy implementations.

public abstract class DocumentStrategy<TDoc> : IDocumentStrategy
{
    bool IDocumentStrategy.AppliesTo(IDocument document)
    {
        // Map the RetailDocument to this strategy instance
        return document is TDoc;
    }

    bool IDocumentStrategy.Post<TDocument>(TDocument document)
    {
        return Post((TDoc)(object)document);
    }

    protected abstract bool Post(TDoc document);
}

Concrete Document Strategy Implementations

public class RetailDocumentStrategy : DocumentStrategy<RetailDocument>
{
    protected override bool Post(RetailDocument document)
    {
        // Post RetailDocument...
        // Note that all of the properties of RetailDocument will be avalable here.
        //var x = document.Items[0].RetailStuff;
        return true;
    }
}

public class WholesaleDocumentStrategy : DocumentStrategy<WholesaleDocument>
{
    protected override bool Post(WholesaleDocument document)
    {
        // Post WholesaleDocument...
        // Note that all of the properties of WholesaleDocument will be avalable here.
        //var x = document.Items[0].WholesaleStuff;
        return true;
    }
}

NOTE: You specified you don't want to write a method for every document type, but you sort of have to if you have different properties that you are reading in each case. If you have any common processing code that you want to share between your strategy implementations, it is usually better to inject a service that handles the common functionality into the constructor of the strategies than to put the common code in DocumentStrategy<TDoc> . That way, if you have a new strategy that doesn't use the common functionality, you can simply omit the injection on that one class.

var documentPosterService = new DocumentPosterService();
var strategies = new IDocumentStrategy[]
{
    new RetailStrategy(documentPosterService),
    new WholesaleStrategy(documentPosterService)
};

See the Usage section below to get an idea how to wire these strategies up. Note I am not showing the modifications to the RetailStrategy and WholesaleStrategy classes you will need to make to accept the extra service parameter, but it is similar to the DocumentPoster class below.

DocumentPoster Implementation

Here is the class that ties it all together. Its main purpose is to select the strategy based on the type of document that is being passed to it before delegating the task of processing the document to the strategy.

We provide the strategy implementations through the constructor so we can add/remove document strategies later without needing to change existing strategy implementations or DocumentPoster .

public class DocumentPoster : IDocumentPoster
{
    private readonly IEnumerable<IDocumentStrategy> documentStrategies;

    public DocumentPoster(IEnumerable<IDocumentStrategy> documentStrategies)
    {
        this.documentStrategies = documentStrategies
            ?? throw new ArgumentNullException(nameof(documentStrategies));
    }

    public bool Post(IDocument document)
    {
        return GetStrategy(document).Post(document);
    }

    private IDocumentStrategy GetStrategy(IDocument document)
    {
        var strategy = documentStrategies.FirstOrDefault(s => s.AppliesTo(document));
        if (strategy is null)
            throw new InvalidOperationException(
                $"Strategy for {document.GetType()} not registered.");
        return strategy;
    }
}

Usage

var poster = new DocumentPoster(
    new IDocumentStrategy[] {
        new RetailDocumentStrategy(),
        new WholesaleDocumentStrategy()
    });

var retailDocument = new RetailDocument()
{
    Id = Guid.NewGuid(),
    Items = new List<RetailItem>
    {
        new RetailItem() { Id = Guid.NewGuid(), Net = 1.1m, Gross = 2.2m, Vat = 3.3m, RetailStuff = "foo" },
        new RetailItem() { Id = Guid.NewGuid(), Net = 1.2m, Gross = 2.3m, Vat = 3.4m, RetailStuff = "bar" },
    }
};

poster.Post(retailDocument);

var wholesaleDocument = new WholesaleDocument()
{
    Id = Guid.NewGuid(),
    Items = new List<WholesaleItem>
    {
        new WholesaleItem() { Id = Guid.NewGuid(), Net = 2.1m, Gross = 3.2m, Vat = 4.3m, WholesaleStuff = "baz" },
        new WholesaleItem() { Id = Guid.NewGuid(), Net = 3.2m, Gross = 4.3m, Vat = 5.4m, WholesaleStuff = "port" },
    }
};

poster.Post(wholesaleDocument);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM