Many articles I’ve read about generic classes show a very simple implementation that can sum 2 numbers, output the type passed in or something similar. These are useful articles for basic understanding of how to get started with generic classes, but I am going to go a step further by showing a real world example of them.
We are going to create a generic credit card processing class that accepts request and response types that only implement a specific interface which will be instantiated by a factory method.
First lets start of with the core objects that our solution will require. These are the request and response objects, and they are kept small for prototyping.
1 2 3 4 5 6 7 8 9 |
public class PaymentRequest : IPaymentRequest { public string AccountNumber { get; set; } } public class PaymentResponse : IPaymentResponse { public string Token { get; set; } } |
This is the core factory implementation that will return the appropriate PaymentGateway object. Notice that there are 2 generic parameters(T1 and T2) that are required when instantiating this object. As the syntax states, they must both implement the interfaces entered after the where clause constraints.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
/// <summary> /// Core factory for returning a payment processor /// </summary> public class PaymentProcessorFactory<T1, T2> where T1 : IPaymentRequest where T2 : IPaymentResponse { private readonly string _processor; public PaymentProcessorFactory(string processor) { _processor = processor; } public IPaymentGateway<T1, T2> GetPaymentProcessor() { switch (_processor) { case "mc": return new Payflow<T1, T2>(); case "vs": return new AmazonPay<T1, T2>(); default: throw new Exception("Unknown PaymentGateway type requested"); } } } |
Once the factory above returns a PaymentGateway object, we can simply use the methods since the GetPaymentProcessor factory method already instantiated it using the generic types we provided when initially creating the factory. We are going to reach out to a mock service that returns a hard coded token.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
/// <summary> /// Payflow payment processor concrete implementation /// </summary> public class Payflow<T1, T2> : IPaymentGateway<T1, T2> where T1 : IPaymentRequest where T2 : IPaymentResponse { public async Task<T2> AuthorizeAsync(HttpClient httpClient, T1 paymentRequest) { if (paymentRequest.AccountNumber.Length != 16) { throw new Exception("account number invalid"); } string result; using (HttpResponseMessage res = await httpClient.GetAsync("http://www.mocky.io/v2/59836865100000030ba850e0")) using (HttpContent content = res.Content) { result = await content.ReadAsStringAsync(); } var resObj = JsonConvert.DeserializeObject(result, typeof(T2)); return (T2)(IPaymentResponse)resObj; } public async Task<T2> CaptureAsync(HttpClient httpClient, T1 paymentRequest) { if (paymentRequest.AccountNumber.Length != 16) { throw new Exception("account number invalid"); } string result; using (HttpResponseMessage res = await httpClient.GetAsync("http://www.mocky.io/v2/59836865100000030ba850e0")) using (HttpContent content = res.Content) { result = await content.ReadAsStringAsync(); } var resObj = JsonConvert.DeserializeObject(result, typeof(T2)); return (T2)(IPaymentResponse)resObj; } } |
Here is a test method that demonstrates all of the functionality from above.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
[TestMethod] public async Task Integration_Test_For_Authorize_And_Capture() { // processor can change depending on provider fees per cart type, or processor outages. // This can make it easy to switch providers easily with simple configurations. var factory = new PaymentProcessorFactory<PaymentRequest, PaymentResponse>("mc"); var ccProcessor = factory.GetPaymentProcessor(); var paymentRequest = new PaymentRequest { AccountNumber = "1111222233334444" }; var authResponse = await ccProcessor.AuthorizeAsync(new System.Net.Http.HttpClient(), paymentRequest); var captResponse = await ccProcessor.CaptureAsync(new System.Net.Http.HttpClient(), paymentRequest); Assert.AreEqual(authResponse.Token, "00000000-0000-0000-0000-000000000000"); Assert.AreEqual(captResponse.Token, "00000000-0000-0000-0000-000000000000"); } |
A great benefit to this architecture is that it allows you to support an unlimited amount of PaymentGateways as long as they implement the IPaymentGateway interface and their request and response objects implement the IPaymentRequest and IPaymentResponse interfaces.
I have left out all the interfaces in favor of focusing only on the concrete objects in this article. You can view the entire functional project on Github here https://github.com/RyanOC/PaymentGateway/tree/generic-classes
Happy Coding!
Ryan