Blogger news

Blogger templates

Wednesday, February 22, 2017

Strive for low coupling and high cohesion- Decouple web service call from consumer and make it testable

The main focus of this article is to explain how you can make the class you are invoking soap web service testable or how you can decouple the class you are invoking a soap service. I have created a very simple service contract to GetCustomerName like below
[ServiceContract]
    public interface ICustomerService
    {
        [OperationContract]
        string GetCustomerName();
    }

And service implementation,
public class CustomerService : ICustomerService
    {
        public string GetCustomerName()
        {
            return "Vijayanath Viswanathan";
        }
    }

I have hosted this service and now I am creating a client application to consume the same service which usually looks like below


Hmm, all good isn't it? I am sure you the service call will give the expected output, "Vijayanath Viswanathan". 
But there a serious problem of tight coupling here. Have you noticed how I have created an instance of service client? That instantiation made a complete coupling between "CustomerInformation" class and service client. In fact "CustomerInformation" class doesn't want to know how or where it is getting data from. The only job of "CustomerInformation" should be to get data to its client. This class is not unit testable because there is no way you can write test double or mock the service call. If you write a test method it will always and always hit the real web service, which is not a good design at all. If you want to replace soap service say REST then you have to modify the entire logic of creating a service instance. This design is exactly same as you are using a copper wire directly connected to an electric bulb instead of a socket. If you want to change the bulb then you have to remove the hard connection remove the bulb then do another connection to the new bulb. Awful isn't it? Always remember the slogan of Object Oriented Programer, "Strive for low coupling and high cohesion"
Hmm, now let's see step by step how we can refactor this to make a loosely coupled design. As always Interface is your friend. I am sure you might be aware of the proxy class generated while you adding a service reference to your project and that will be the life saver here. Let see what's there in the generated proxy code. We are interested in two parts in the proxy class. First the base interface in our example it is ICustomerService
public interface ICustomerService {
        
        [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/ICustomerService/GetCustomerName", ReplyAction="http://tempuri.org/ICustomerService/GetCustomerNameResponse")]
        string GetCustomerName();
        
        [System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/ICustomerService/GetCustomerName", ReplyAction="http://tempuri.org/ICustomerService/GetCustomerNameResponse")]
        System.Threading.Tasks.Task<string> GetCustomerNameAsync();
    }

Next, the proxy class which implements ICustomerService interface
System.Diagnostics.DebuggerStepThroughAttribute()]
    [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
    public partial class CustomerServiceClient :
System.ServiceModel.ClientBase<CustomerClient.CustomerService.ICustomerService>,
 CustomerClient.CustomerService.ICustomerService {
        
        public CustomerServiceClient() {
        }
        
        public CustomerServiceClient(string endpointConfigurationName) : 
                base(endpointConfigurationName) {
        }
        
        public CustomerServiceClient(string endpointConfigurationName, string remoteAddress) : 
                base(endpointConfigurationName, remoteAddress) {
        }
        
        public CustomerServiceClient(string endpointConfigurationName, System.ServiceModel.EndpointAddress remoteAddress) : 
                base(endpointConfigurationName, remoteAddress) {
        }
        
        public CustomerServiceClient(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) : 
                base(binding, remoteAddress) {
        }
        
        public string GetCustomerName() {
            return base.Channel.GetCustomerName();
        }
        
        public System.Threading.Tasks.Task<string> GetCustomerNameAsync()   {
            return base.Channel.GetCustomerNameAsync();
        }
    }



Have you noticed the function "GetCustomerName()" in above code snippet? One thing is clear now it is an instance of Channel which is calling the real web service method "GetCustomerName". What if we are adding an adaptor to that channel? As web service is a 3rd party component to adapt in we need an adapter. Let's see how does it look. I am creating an interface which looks like below,
public interface ICustomerServiceAdaptor<TChannel> 
        where TChannel : ICustomerService
    {

       TChannel CreateChannel();

        void Open();

        void Close();
       
    }

As you can see I have added a generic constraint, TChannel should be of type ICustomerService. Now let's add the class implementing ICustomerServiceAdaptor<TChannel>
public class CustomerServiceAdaptor<TChannel> 
        : ICustomerServiceAdaptor<TChannel> where TChannel : ICustomerService
    {
        private readonly IChannelFactory<TChannel> _serviceChannel;
        private readonly EndpointAddress _endPointAddress;

        public CustomerServiceAdaptor(IChannelFactory<TChannel> serviceChannel, EndpointAddress endpointAddress)
                    {
            _serviceChannel = serviceChannel;
            _endPointAddress = endpointAddress;
        }
       

        public TChannel CreateChannel()
                    {
            return _serviceChannel.CreateChannel(_endPointAddress);
        }

        public void Open()
                    {
            _serviceChannel.Open();
        }

        public void Close()
                    {
            _serviceChannel.Close();
        }

        
    }

As you can see in the code snippet above I am creating a channel with CreateChannel which needs the endpoint address to connect to. As the name represents Open and close is to Open and close the connection to the endpoint. I am actually injecting IChannelFactory<TChannel> which is the base for ChannelFactory. As I have mentioned before TChannel should be of type ICustomerService.
public class CustomerInformation
    {
        private readonly ICustomerServiceAdaptor<ICustomerService> _customerServiceAdaptor;
        public CustomerInformation(ICustomerServiceAdaptor<ICustomerService> customerServiceAdaptor)
        {
            _customerServiceAdaptor = customerServiceAdaptor;
        }
        public string GetCustomerName()
        {
            _customerServiceAdaptor.Open();
            var customerServiceChanel = _customerServiceAdaptor.CreateChannel();

            string customerName = customerServiceChanel.GetCustomerName();

            _customerServiceAdaptor.Close();

            return customerName;

        }
    }

I have injected "ICustomerServiceAdaptor<ICustomerService>" to "CustomerInformation" class and I have created the channel with the injected instance, _customerServiceAdaptor.CreateChannel(). Channel can be any channel base on the instance you injected. "CustomerInformation" class have no idea what channel it is. It means now "CustomerInformation" class relies on abstraction or in other words we have decoupled "CustomerInformation" class from service proxy. Now if you want to mock "customerServiceChanel.GetCustomerName()" then definitely it is doable as you can mock "ICustomerServiceAdaptor<ICustomerService>" and "GetCustomerName" will return the mocked string instead of the real service call.
Let's see how can you compose and inject your objects. In real time you can go with IoC container like Ninject, Unity IoC etc. instead of injecting it manually.
CustomerInformation customerInformation =
                new CustomerInformation(
                    new CustomerServiceAdaptor<ICustomerService>(
                        new ChannelFactory<ICustomerService>(new BasicHttpBinding()),
                        new EndpointAddress("http://localhost:1136/CustomerService.svc")));

            string customerName = customerInformation.GetCustomerName();



No comments:

Post a Comment