How can I write this class to be fully generic and return different responses according to one request?

坚强是说给别人听的谎言 提交于 2020-01-04 04:54:10

问题


I was asked to create a series of reports for an application and as always, I'm looking for ways to reduce the amount of code written. I've started trying to come up with the easiest way to request a single report. Here's what I imagined:

var response = ReportGenerator.Generate(Reports.Report1);
//Reports would be an enum type with all of the available reports.

As soon as I tried to design that, the problems appeared. Every report has a different input and output. The input being the entity (or entities) on which the report is based and the output being the DTO holding the processed data.

Backing this up, I created this:

// The interface for every report
public interface IReport<INPUT, OUTPUT>
{
  public OUTPUT GenerateReport(INPUT input);
}

// A base class for every report to share a few methods
public abstract class BaseReport<INPUT, OUTPUT> : IReport<INPUT, OUTPUT>
{
  // The method required by the IReport interface
  public OUTPUT GenerateReport(INPUT input)
  {
    return Process(input);
  }

  // An abstract method to be implemented by every concrete report
  protected abstract OUTPUT Process(INPUT input);
}

public class ConcreteReport : BaseReport<SomeEntity, SomeDto>
{
  protected override SomeDto Process(SomeEntity input)
  {
    return default(SomeDto);
  }
}

At first I was considering to have every concrete report to specify the logic responsible to determine its own input. I quickly saw that it would make my class less testable. By having the report request an instance of the INPUT generic type I can mock that object and test the report.

So, what I need is some kind of class to tie a report (one of the enum values) to a concrete report class responsible for its generation. I'm trying to use an approach similar to a dependency injection container. This is the class I'm having trouble to write.

I'll write below what I have with comments explainning the problems I've found (it's not supposed to be syntatically correct - it's just a stub since my problem is exactly the implementation of this class):

public class ReportGenerator
{
  // This would be the dictionary responsible for tying an enum value from the Report with one of the concrete reports.
  // My first problem is that I need to make sure that the types associated with the enum values are instances of the BaseReport class.
  private readonly Dictionary<Reports, ?> registeredReports;

  public ReportGenerator()
  {
    // On the constructor the dictionary would be instantiated...
    registeredReports = new Dictionary<Reports, ?>();

    // and the types would be registered as if in a dependency injection container.
    // Register(Reports.Report1, ConcreteReport);
    // Register(Reports.Report2, ConcreteReport2);
  }

  // Below is the most basic version of the registration method I could come up with before arriving at the problems within the method GenerateReport.

  // T repository              - this would be the type of the class responsible for obtainning the input to generate the report
  // Func<T, INPUT> expression - this would be the expression that should be used to obtain the input object

  public void Register<T, INPUT>(Reports report, Type reportConcreteType, T repository, Func<T, INPUT> expression)
  {
    // This would basically add the data into the dictionary, but I'm not sure about the syntax
    // because I'm not sure how to hold that information so that it can be used later to generate the report

    // Also, I should point that I prefer to hold the types and not instances of the report and repository classes.
    // My plan is to use reflection to instantiate them on demand.
  }

  // Based on the registration, I would then need a generic way to obtain a report.
  // This would the method that I imagined at first to be called like this:
  // var response = ReportGenerator.Generate(Reports.Report1);

  public OUTPUT Generate(Reports report)
  {
    // This surely does not work. There is no way to have this method signature to request only the enum value
    // and return a generic type. But how can I do it? How can I tie all these things and make it work?
  }
}

I can see it is not tied with the report interface or abstract class but I can't figure out the implementation.


回答1:


I am not sure that it is possible to achieve such behaviour with enum, so I can propose you the following solution:

  1. Use some identifier generic class(interface) in place of enum values. To use it as key in dictionary you will also have to have some non-generic base for this class.
  2. Have some static class with aforementioned identifier classes as specific static properties.
  3. Use values from static class properties as keys in ReportGenerator class.

Here are required interfaces:

public interface IReportIdentifier
{

}

public interface IReportIdentifier<TInput, TOutput> : IReportIdentifier
{

}

public interface IReport<TInput, TOutput>
{
    TOutput Generate(TInput input);
}

Here is the static "enum" class:

public static class Reports
{
    public static IReportIdentifier<String, Int32> A
    {
        get { return null;}
    }

    public static IReportIdentifier<Object, Guid> B
    {
        get { return null; }
    }
}

And here is the ReportGenerator class:

public class ReportGenerator
{
    IDictionary<IReportIdentifier, Object> reportProducers = new Dictionary<IReportIdentifier, Object>();

    public void Register<TInput, TOutput>(IReportIdentifier<TInput, TOutput> identifier, IReport<TInput, TOutput> reportProducer)
    {
        reportProducers.Add(identifier, reportProducer);
    }

    public TOutput Generate<TInput, TOutput>(IReportIdentifier<TInput, TOutput> identifier, TInput input)
    {
        // Safely cast because it is this class's invariant.
        var producer = (IReport<TInput, TOutput>)reportProducers[identifier];
        return producer.Generate(input);
    }
}

As you see, we use cast but it is hidden inside the Generate method and if our Register method is the only access point to the reportProducers dictionary this cast will not fail.

And also as @CoderDennis pointed:

Then you could always use T4 to generate that static class and its static properties and could even create an extension method that returns the proper IReportIdentifier from your enum.




回答2:


It seems to me that you may want to rethink the design.

You essentially have methods that take objects in and spit objects out. Granted, you use generics, but that doesn't mean much since there are no constraints on input/output and thus no way to commonly process them in calling code.

In fact, I think the use of generics is potentially a hindrance with the given approach, because passing in the wrong combination of generic types will result in a error, and it's not clear to the caller what is valid and what is not.

Given the approach, it's unclear what benefit all of the extra classes give over non-abstractions like:

 int r1Output = Report1StaticClass.GetOutput(string input);
 string r2Output = Report2StaticClass.GetOtherOutput(int input);
 double r3Output = Report3StaticClass.GetWhatever(double input);

A different approach might be to encapsulate input/output something similar to this, but adjusted to your needs. This isn't meant to be an exact approach, but just something to demonstrate what I'm suggesting. Also, I haven't actually tested/compile this. Consider it pseudo-code:

 //something generic that can be easily mocked and processed in a generic way
 //your implementation almost certainly won't look exactly like this...
 //but the point is that you should look for a common pattern with the input
 interface IInput
 {
     ReportTypeEnum EntityType{ get; set; } 
     int EntityId{ get; set; }
 }

 interface IReportTemplate 
 {
      //return something that can be bound to/handled generically.
      //for instance, a DataSet can be easily and dynamically bound to grid controls.
      //I'm not necessarily advocating for DataSet, just saying it's generic
      //NOTE: the guts of this can use a dynamically assigned
      //      data source for unit testing
      DataSet GetData(int entityId); 
 }

 //maybe associate report types with the enum something like this.
 [AttributeUsage (AttributeTargets.Field, AllowMultiple = false)]
 class ReportTypeAttribute : Attribute
 {
    public Type ReportType{ get; set; }
    //maybe throw an exception if it's not an IReportTemplate  
    public ReportTypeAttribute(Type reportType){ ReportType = reportType; }
 }

 //it should be easy for devs to recognize that if they add an enum value,
 //they also need to assign a ReportType, thus your code is less likely to
 //break vs. having a disconnect between enum and the place where an associated
 //concrete type is assigned to each value
 enum ReportTypeEnum
 {
     [ReportType(typeof(ConcreteReportTemplate1))]
     ReportType1,
     [ReportType(typeof(ConcreteReportTemplate2))]
     ReportType2
 }

 static class ReportUtility
 {  
     public static DataSet GetReportData(IInput input)
     {
         var report = GetReportTemplate(input.EntityType);
         return report.GetData(input.EntityId);
     }

     private static IReportTemplate GetReportTemplate(ReportTypeEnum entityType)
     {
         //spin up report by reflecting on ReportTypeEnum and
         //figuring out which concrete class to instantiate
         //based on the associated ReportTypeAttribute
     }
 }


来源:https://stackoverflow.com/questions/30057636/how-can-i-write-this-class-to-be-fully-generic-and-return-different-responses-ac

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!