如何实现多类型的泛型方法?



我在做会计软件,我需要泛型方面的帮助。我有多种文档类型,根据不同的类型,不同的发布规则将适用。现在我的问题是如何让所有东西都通用?

我已经试过了

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

这里困难的事情是,我将有多个项目类,因为批发或零售的项目是不一样的(但每个项目类将有一些共同的属性,如Net, Vat和Gross)。如何绕过这个问题,让这个泛型方法适用于所有文档类型,而不必为应用程序的每种文档类型都写一个方法?

如果您不希望有数千个文档类型,那么解决这个问题的合理方法是使用策略模式。有几种不同的方法可以解决这个问题,但简而言之,它的目的是将一种算法与另一种具有相同接口的算法交换。

模式还可以用于基于模型对象在具有类似目的的服务之间切换,类似于您的IDocument<IItem>类型。

假设你想要坚持单一职责原则和开/闭原则,并利用。net泛型。

使用强制类型转换当然可以实现以下目标,但实际上每次更改时都需要修改多个类型。

文档和项目接口

首先,我们需要对接口进行一些重做。为了使设计工作,我们需要通用和非通用文档接口。这让我们避免了一个常见的陷阱,即试图在不允许的地方指定关闭类型,因为c#泛型非常严格,不像其他语言那样允许通配符。

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

我们声明IDocument<TItem> : IDocument where TItem : IItem是为了确保IItem的具体实现的属性对我们的其余代码可用,而不需要强制转换。

注意DocumentType枚举被删除了,因为它不是必要的和冗余的,因为我们可以按照以下方式检查文档类型:

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

如果你觉得需要的话,你可以把它添加回去。

具体的文档和项目实现

正如其他人在评论中指出的那样,在处理货币时,我们通常将类型声明为decimal而不是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和DocumentPoster接口

要使策略类(处理post的部分)泛型,我们需要为它提供一个接口。我们还提供了一个(可选的)IDocumentPoster接口,如果你正在使用依赖注入,它将派上用场。

PostDocument被命名为DocumentPoster,因为在c#中我们将方法命名在动词之后,将类/属性命名在名词之后。

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

DocumentStrategy抽象这是一个抽象类,用于隐藏转换到具体的IDocument类型的丑陋细节,以便我们可以访问策略实现中的强类型属性。

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

具体文档策略实现

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

注意:你指定你不想为每种文档类型都写一个方法,但如果你在每种情况下读取不同的属性你就必须这么做。如果你想在策略实现之间共享任何公共处理代码,通常最好在构造函数中注入处理公共功能的服务。将通用代码放在DocumentStrategy<TDoc>中是比较容易的。这样,如果你有一个不使用公共功能的新策略,你可以简单地忽略那个类上的注入。

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

请参阅下面的用法部分,了解如何将这些策略连接起来。注意,我没有显示您需要对RetailStrategyWholesaleStrategy类进行修改以接受额外的服务参数,但它与下面的DocumentPoster类类似。

DocumentPoster实现这是将它们连接在一起的类。它的主要目的是在将处理文档的任务委托给策略之前,根据传递给它的文档类型选择策略。

我们通过构造函数提供策略实现,这样我们就可以在以后添加/删除文档策略,而不需要更改现有的策略实现或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;
}
}
使用

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

最新更新