DEV Community

Joe Petrakovich
Joe Petrakovich

Posted on • Originally published at purple.pizza on

Refactoring service class configuration using fluent interfaces

To many of us, interfaces are old news.

We use it here and there, you know, whenever we have a Duck and an Eagle that both need to Fly().

Or, using a more practical example, when we want to support both GooglePay and ApplePay by way of a single IPaymentService.ProcessPayment() method.

That’s all well and good, but a skillful developer can extract far more power from interfaces beyond simple polymorphism.

“A well-placed interface acts as a firewall between the dirty implementation details of service code and a clean well-organized client.” - Gary McLean Hall

In this article, we continue our trek through the book Adaptive Code via C# by Gary Mclean Hall by briefly summarizing Chapter 3. In case you missed them, see Part 1, Part 2, and Part 3.

Chapter 3 covers several design patterns that help show us an interface’s full range of functionality.

At the end, we will look a bit deeper at the concept of a fluent interface and how it can improve our code’s readability and ease of use with some refactoring of actual production code.

Getting Explicit

Chapter 3 opens by giving us the standard definition of an interface: the mechanism of describing the behavior of a class, without describing the implementation.

Like I said, old news.

In C#, we of course use the interface declaration when creating what are essentially implementationless contracts that keep our client and service code separate and changeable.

Something in the book I didn’t know was that you can explicitly implement interface methods. Instead of the usual way of writing matching method signatures to implement the inteface, you can use dot notation for scenarios where you have method signature naming collisions. For example, if you have an interface for a boat with a method for steering the boat and car:

    public interface IBoat
    {
        void Steer(int degrees);
    }

    public interface ICar
    {
        void Steer(int degrees);
    }

And then by some luck, you have a business requirement to support those odd-looking touring boats that can also drive on the road. Since your ICar interface also defines a Steer() method, how will the code know which method implementation to use in the below example?

    public class LandShark : ICar, IBoat
    {
        public void Steer(int degrees) {
            //Steer with the car rack-and-pinion or with the boat rudder?
        }
    }

To get around this naming clash, you can explicitly define it like so:

    public class LandShark : ICar, IBoat
    {
        public void ICar.Steer(int degrees) {
            //Steer with rack-and-pinion...
        }

        public void IBoat.Steer(int degrees) {
            //Steer with rudder...
        }
    }

Three Patterns

Next, the author looks at three design patterns making use of interfaces: The Null Object pattern, the Strategy pattern, and the Adapter pattern. I won’t be covering them in this article, but you can catch them in future articles by getting on my list below, from which I will shoot you an email when they are ready to be feasted upon.

I highly recommend it if you are someone with a thirst for getting better as a developer. I don’t spam, I just send you easy-to-consume dev kool-aid once every week or so that is only geared at helping you become better at your craft.

DEV.TO Readers click HERE!

Mixins and Duck Typing, What The Duck…

Continuing on, the author then takes us into a strange territory, duck typing and mixins.

I’ll spare you the details (and encourage you to research it on your own), but the main idea is that if you have classes that implement certain method signatures, you can retroactively program to them as if they are implementing an interface with those same method signatures.

Because I’ve never had a need to use something like this, it all sounded very kludgy to me.

If I had some kind of third-party code that I wanted to fit into my own interfaces, I’d probably just write an adapter around it, but that’s just me.

I imagine there was a real need for these techniques, so take my opinion as just that… an opinion.

Fluent Interfaces

The author closes this chapter touching briefly on the topic of fluent interfaces. An interface can be defined as fluent if it returns an instance of itself, allowing calls to be chained together.

This simple technique can do wonders for your code’s readability and easy of use, and in order to demonstrate that, I’d like to close the article with a quick refactor of some actual production code.

For some background, an application I currently work on makes use of electronic signatures, allowing users to both sign documents and send them on to other users for additional signing. To implement this feature, we are making use of the third-party vendor known as DocuSign.

In the DocuSign world, the fundamental top-level model used is an envelope which simulates a real world mailing envelope, complete with recipients, subjects, messages, and of course, documents that need to be signed.

In our current implementation, our DocuSign API service implements an IESignatureService interface that provides a set of methods for the creation and sending of envelopes. You can see a shortened version of that here:

    public interface IESignatureService
    {
        public string SendSignatureRequest(IESignatureEnvelope envelope);

        public void VoidRequest(string envelopeID, string voidReason);

        public EnvelopeStatusInfo GetEnvelopeStatus(string envelopeID);

        public IEnumerable<RecipientStatusInfo> GetRecipientStatuses(string envelopeID);
    }

In this example, the primary task of sending the e-signature request happens by calling SendSignatureRequest(IESignatureEnvelope envelope). It requires clients to pass it a fully constructed envelope object, and returns a string, which in our case is used to return the ID of the envelope since that is what DocuSign returns to us upon successful request.

The devil is in the details here, because it is the creation of the envelope that becomes the heavy and complex burden of the client. First, take a look at the current IESignatureEnvelope interface:

   public interface IESignatureEnvelope
    {
        string EmailMessage { get; set; }
        string EmailSubject { get; set; }
        IEnumerable<IESignatureEnvelopeRecipient> Recipients { get; }
        byte[] Document { get; set; }
        string DocumentName { get; set; }
    }

There isn’t much to it, but you’ll notice that the Recipients property has no setter, leaving it entirely up to the concrete implementation to come up with a way to populate it.

At a level lower, there are additional complexities around the creation of the objects implementing IESignatureEnvelopeRecipient. Below is an example of how you would currently need to create and send an e-signature request from start to finish:

   public class ESignatureClient
    {
        private readonly IESignatureService eSignatureService;

        public ESignatureClient(IESignatureService eSignatureService) 
        {
            this.eSignatureService = eSignatureService
        }

        public void SendESignatureRequest(byte[] documentToSign)
        {
            var envelope = new ESignatureEnvelope();

            envelope.EmailSubject = "Your signature is needed";
            envelope.EmailMessage = "Hi there, please review this document and sign it.";

            envelope.DocumentName = "Quote #405";
            envelope.Document = documentToSign;

            var firstRecipient = new ESignatureEnvelopeRecipient();
            firstRecipient.Name = "Joe";
            firstRecipient.Email = "joe@example.com";
            firstRecipient.Type = RecipientType.Signer;

            var secondRecipient = new ESignatureEnvelopeRecipient();
            secondRecipient.Name = "Abby";
            secondRecipient.Email = "Abby@example.com";
            secondRecipient.Type = RecipientType.Signer;

            var thirdRecipient = new ESignatureEnvelopeRecipient();
            thirdRecipient.Name = "Susan";
            thirdRecipient.Email = "Susan@example.com";
            thirdRecipient.Type = RecipientType.CarbonCopy;

            envelope.Recipients.Add(firstRecipient);
            envelope.Recipients.Add(secondRecipient);
            envelope.Recipients.Add(thirdRecipient);

            this.eSignatureService.SendSignatureRequest(envelope);
        }
    }

As you can see, we’ve used quite a bit of code to configure a basic e-signature request with two signers, a carbon copy, and a single document to sign.

Now, we could improve this a lot simply by using constructor parameters to set the needed properties of each object, but let’s take it a step further by adding some fluent interface methods on our envelope interface.

   public interface IESignatureEnvelope
    {
        //fluent!

        IESignatureEnvelope AddSubjectAndMessage(string subject, string message);
        IESignatureEnvelope AddSigner(string name, string email);
        IESignatureEnvelope AddCarbonCopy(string name, string email);
        IESignatureEnvelope AddDocument(string documentName, byte[] document);

        string EmailMessage { get; set; }
        string EmailSubject { get; set; }
        IEnumerable<IESignatureEnvelopeRecipient> Recipients { get; }
        byte[] Document { get; set; }
        string DocumentName { get; set; }
    }

Now take a look at our client class:


   public class ESignatureClient
    {
        private readonly IESignatureService eSignatureService;

        public ESignatureClient(IESignatureService eSignatureService) 
        {
            this.eSignatureService = eSignatureService
        }

        public void SendESignatureRequest(byte[] documentToSign)
        {
            var envelope = new ESignatureEnvelope();

            //such clean, wow!

            envelope.AddSubjectAndMessage("Your signature is needed", "Hi there, please review this document and sign it.")
                    .AddSigner("Joe", "joe@example.com")
                    .AddSigner("Abby", "abby@example.com")
                    .AddCarbonCopy("Susan", "susan@example.com")
                    .AddDocument("Quote #405", documentToSign);

            this.eSignatureService.SendSignatureRequest(envelope);
        }
    }

By adding a handful of fluent methods to configure the envelope, we’ve greatly reduced the amount of code needed to get a request out the door.

We are also abstracting away details like the RecipientType and making life easier for future developers.

Fluent interfaces are a strategy I’ve seen in practice (and enjoyed using) but have never implemented one until now. Take some time next time you’re in your codebase to do a refactoring like this. Your team and future developers will thank you.

Top comments (0)