Home > Software design >  Is there a more effective generic way of achieving this?
Is there a more effective generic way of achieving this?

Time:09-28

Just wondering if there is a more generic way of achieving the following:

public class Payment
{}

public class Paypal : Payment
{
public string PaypalKey{get;set;}
}

public class WorldPay : Payment
{
public int WorldPayID{get;set;}
}

public class PaymentService
{
    public PaymentResponse Process(Payment payment)
    {
        //Checking payment type and casting to access properties as they differ on each payment
        if (payment is WorldPay) //cast to WorldPay
        if (payment is Paypal) //cast to Paypal
        //Perform logic to build a response for given payment and return the response
        return paymentResponse;
    }
}

Each payment type can have own properties and finding that checking and casting is becoming a bit messy. Just wondering if there is a cleaner way.

The idea is the caller of Process can pass in the payment type giving only properties that are required for that payment. Could make it generic but that would mean properties would need to be optional.

The Process method will return the response to the user. The response will be created based on on the input method and will contain various properties based on the input payment.

CodePudding user response:

Check out my answer to a similar question.

What you can do is to use polymorphism. Just declare a virtual or abstract method "Process" in the parent class "Payment" and override that method in each concrete sub-type. And just execute that method in your "PaymentService"

public class Payment
{
   // Common properties & staff

   public abstract/virtual void Process();
}

public class Paypal : Payment
{
   public override void Process() 
   { 
       // Paypal implementation 
   }
}

public class WorldPay : Payment
{
   public override void Process() 
   { 
       // WorldPay implementation 
   }
}

public class PaymentService
{
    public void Process(Payment payment)
    {
        payment.Process();    
    }
}

CodePudding user response:

If you're trying to avoid putting actual payment processing code in the DTOs and you want to make the PaymentService generic, then you could do something like this:

public class PaymentService
{
    private List<(Type type, Delegate process)> _processes = new List<(Type, Delegate)>();

    public void Register<T>(Func<T, PaymentResponse> process) where T : Payment
    {
        _processes.Add((typeof(T), (Func<Payment, PaymentResponse>)(p => process((T)p))));
    }

    public PaymentResponse Process(Payment payment) =>
        _processes
            .Where(p => p.type.IsAssignableFrom(payment.GetType()))
            .Select(p => (Func<Payment, PaymentResponse>)p.process)
            .Concat(new Func<Payment, PaymentResponse>[] { p => null })
            .First()
            .Invoke(payment);
}

Now, to demo, here's my code:

public class PaymentResponse
{
    public string Message { get; private set; }
    public PaymentResponse(string message)
    {
        this.Message = message;
    }
}   

void Main()
{
    var paymentService = new PaymentService();
    paymentService.Register<WorldPay>(p => new PaymentResponse("WorldPay!"));
    paymentService.Register<Paypal>(p => new PaymentResponse("Paypal!"));
    
    var payment = new Paypal();
    
    var response = paymentService.Process(payment);
    
    Console.WriteLine(response.Message);
}

When I run that code I get Paypal! on the console.

This kind of code allows you to inject in the behaviour into PaymentService in a strongly-typed way.

CodePudding user response:

it would be preferable to use an interface.

public interface IPayment
{
   Process();
}

public class Paypal : IPayment
{
   Process() { //do stuff }
}

public class WorldPay : IPayment
{
   Process() { //do stuff }
}

public class PaymentService
{
    public void Process(IPayment payment)
    {
        payment.Process();    
    }
}

That way you can mock that interface if you wanted to write unit tests and you remove the dependency of PaymentService on the base Payment class.

CodePudding user response:

Given the context, given in comments, that these payment types should be DTOs without behaviours, I'd suggest you use dynamic so you don't have to test the different types1:

public class PaymentService
{
    public void Process(Payment payment)
    {
        ProcessInternal(payment as dynamic);
    }

    private void ProcessInternal(WorldPay payment)
    {
        //Do WorldPay stuff
    }

    private void ProcessInternal(Paypal payment)
    {
        //Do paypal stuff
    }
}

Note that (as with your type testing code), you need to consider what happens when a new type is added. Using either my code above, by default, you'd get a runtime error.

If there is a sensible way to handle new types without needing to type test, include a ProcessInternal(Payment payment) method which can then act as the "fallback" when a more specific match isn't made.


1Of course, behind the scenes type checks are happening, but you don't have to write that code yourself.

  •  Tags:  
  • c#
  • Related