Home Architecture Understanding Single Responsibility Pattern – Problem and It’s Solution

Understanding Single Responsibility Pattern – Problem and It’s Solution

by Dhanik Lal Sahni

SRP states, “An object should have only one reason to change”. If an object has more than one reason to change then it has more than one responsibility and is in violation of SRP. An object should have one and only one reason to change.

Let us take example of below code, where XML file with product information is parsed and displayed on screen.

    FileDialogReader.Filter = "XML Document (*.xml)|*.xml|All Files (*.*)|*.*";
    var result = FileDialogReader.ShowDialog();
    if (result == DialogResult.OK)
    {
        txtFileName.Text = FileDialogReader.FileName;
        lstProduct.Items.Clear();
        var fileName = txtFileName.Text;
        using (var fs = new FileStream(fileName, FileMode.Open))
        {
            var reader = XmlReader.Create(fs);
            while (reader.Read())
            {
                if (reader.Name != "product") continue;
                var id = reader.GetAttribute("id");
                var name = reader.GetAttribute("name");
                var unitPrice = reader.GetAttribute("unitPrice");
                var discontinued = reader.GetAttribute("discontinued");
                var item = new ListViewItem(
                new string[] { id, name, unitPrice, discontinued });
                lstProduct.Items.Add(item);
            }
        }
    }

Let us refactor this code step by step.

Step 1. Defining a Model

Since we are importing product from XML File, we should introdue a entity model Product. By carefully analyzing above code we can create below model class.

    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal UnitPrice { get; set; }
        public bool Discontinued { get; set; }
    }

Step 2: Extracting the Parsing into a repository

We also can recognize a distinct concern of loading a list of products from a data source. In this case the data source is a XML document. It will make sense to have a specific component concerned with loading a list of products from a data source and return a list of instances of type Product.

    public interface IProductRepository
    {
        IEnumerable GetByFileName(string fileName);
    }

The repository is responsible to load, parse and map the XML document and return a list of Product items to the caller. Based on above steps our code will be like below

    private IProductRepository repository=null;
    private void btnLoadFile_Click(object sender, EventArgs e)
    {
        FileDialogReader.Filter = "XML Document (*.xml)|*.xml|All Files (*.*)|*.*";
        var result = FileDialogReader.ShowDialog();
        if (result == DialogResult.OK)
        {
            txtFileName.Text = FileDialogReader.FileName;
            lstProduct.Items.Clear();
            var fileName = txtFileName.Text;
            lstProduct.Items.Clear();
            var products = repository.GetByFileName(fileName);
            foreach (Product product in products)
            {
                var item = new ListViewItem(new[]
                {
                     product.Id.ToString(),
                     product.Name,
                     product.UnitPrice.ToString(),
                     product.Discontinued.ToString()
                 });
                lstProduct.Items.Add(item);
            }
        }
    }

Above code is better than before, but still it does not look complete refactored.

Above code is written in View and view should not have logic. View should only focused on presenting data and delegating requests to controller or presenter. So we have to seprate each responsibility or concern. we introduce a pattern which separates the concerns of a) visualization, b) orchestration and c) (data-) model. A pattern that perfectly fits our needs is the Model-View-Presenter pattern (MVP). The presenter is the component that orchestrates the interactions between model, view and (external) services. In this pattern the presenter is in command.

Based on this, view should

  • delegate the user’s request to choose an XML document to the presenter
  • delegate the user’s request to load the data from the selected XML document to the presenter
  • provide the name of the selected XML document to the presenter
  • accept a file name (of a selected XML document) from the presenter
  • display a given list of products provided by the presenter (in a ListView control)

Let us create each concern classes.

ProductRepository class is responsible for loading file and converting each record into entity list. This class uses IFileLoader and IProductMapper to load and map record into entity.

  public class ProductRepository : IProductRepository
    {
        private readonly IFileLoader loader;
        private readonly IProductMapper mapper;
        public ProductRepository()
        {
            loader = new FileLoader();
            mapper = new ProductMapper();
        }
        public IEnumerable GetByFileName(string fileName)
        {
            var products = new List();
            using (Stream input = loader.Load(fileName))
            {
                var reader = XmlReader.Create(input);
                while (reader.Read())
                {
                    if (reader.Name != "product") continue;
                    var product = mapper.Map(reader);
                    products.Add(product);
                }
            }
            return products;
        }
    }

FileLoader is resposible to load XML file.

    public interface IFileLoader
    {
        Stream Load(string fileName);
    }
    public class FileLoader : IFileLoader
    {
        public Stream Load(string fileName)
        {
            return new FileStream(fileName, FileMode.Open);
        }
    }

IProductMapper is resposible to map XML element value into entity field. This will return list of products after parsing loaded XML File.

    public interface IProductMapper
    {
        Product Map(XmlReader reader);
    }
    public class ProductMapper : IProductMapper
    {
        public Product Map(XmlReader reader)
        {
            if (reader == null)
                throw new ArgumentNullException("XML reader used when mapping cannot be null.");
            if (reader.Name != "product")
                throw new InvalidOperationException("XML reader is not on a product fragment.");
            var product = new Product();
            product.Id = int.Parse(reader.GetAttribute("id"));
            product.Name = reader.GetAttribute("name");
            product.UnitPrice = decimal.Parse(reader.GetAttribute("unitPrice"));
            product.Discontinued = bool.Parse(reader.GetAttribute("discontinued"));
            return product;
        }
    }

ViewForm is view component.

    public interface IProductView
    {
        void Initialize(ProductPresenter presenter);
        string GetFileName();
        void ShowProducts(IEnumerable products);
        void SetFileName(string fileName);
    }
    public partial class ViewForm : Form, IProductView
    {
        public ViewForm()
        {
            InitializeComponent();
        }
        private ProductPresenter presenter;
        private void btnBrowse_Click(object sender, EventArgs e)
        {
            presenter.BrowseForFileName();
        }
        private void btnLoad_Click(object sender, EventArgs e)
        {
            presenter.GetProducts();
        }
        public void ShowProducts(IEnumerable products)
        {
            lstView.Items.Clear();
            foreach (Product product in products)
            {
                var item = new ListViewItem(new[]
                {
                     product.Id.ToString(),
                     product.Name,
                     product.UnitPrice.ToString(),
                     product.Discontinued.ToString()
                 });
                lstView.Items.Add(item);
            }
        }
        public string GetFileName()
        {
            return txtFileName.Text;
        }
        public void SetFileName(string fileName)
        {
            txtFileName.Text = fileName;
            btnLoad.Enabled = true;
        }
        public void Initialize(ProductPresenter _presenter)
        {
            presenter = _presenter;
        }
    }

ProductPresenter is presenter and responsible to manage above component.

    public class ProductPresenter
    {
        private readonly OpenFileDialog openFileDialog;
        private readonly IProductRepository repository;
        private readonly IProductView view;
        public ProductPresenter()
        {
            view = new ViewForm();
            view.Initialize(this);
            repository = new ProductRepository();
            openFileDialog = new OpenFileDialog();
        }
        public IProductView View
        {
            get { return view; }
        }
        public void BrowseForFileName()
        {
            openFileDialog.Filter = "XML Document (*.xml)|*.xml|All Files (*.*)|*.*";
            var result = openFileDialog.ShowDialog();
            if (result == DialogResult.OK)
                view.SetFileName(openFileDialog.FileName);
        }
        public void GetProducts()
        {
            BrowseForFileName();
            var products = repository.GetByFileName(view.GetFileName());
            view.ShowProducts(products);
        }
    }

Below code need to put in Main method to run application.

    var presenter = new ProductPresenter();
    Application.Run((Form)presenter.View);

These above classes are having very less code and each are responsible for one responsibility. In real application, there are complex structure and for that SRP is very important. With the aid of the SRP, a complex problem can be reduced to many small sub-problems which are easy to solve in isolation. Just remember the sample of the Roman empire I gave you in the introduction of this post.

Complete Source code can be downloaded from here.

You may also like

Leave a Comment