Welcome to EMC Consulting Blogs Sign in | Join | Help

Owain Wraggs' Blog

I have now left EMC Consulting, you can subscribe to my new blog here: http://blog.endjin.com/author/owain-wragg/

Handling New Requirements with the Decorator Pattern

Assume we have an existing application which manages customer purchases, below is the code that is responsible for validating purchases.

We have a number of validators which inherit from the PurchaseValidatorBase class, which in turn implements the IPurchaseValidator interface.

    public interface IPurchaseValidator

    {

        bool ValidatePurchases(List<Purchase> purchases);

    }

    public abstract class PurchaseValidatorBase : IPurchaseValidator

    {

        protected Customer Customer { get; private set; }

 

        protected PurchaseValidatorBase(Customer customer)

        {

            Customer = customer;

        }

 

        public abstract bool ValidatePurchases(List<Purchase> purchases);

    } 

 

  public class NumberOfItemsPurchasedPurchaseValidator : PurchaseValidatorBase

    {

        public NumberOfItemsPurchasedPurchaseValidator(Customer customer) : base(customer) { }

 

        public override bool ValidatePurchases(List<Purchase> purchases)

        {

            bool validPurchases = true;

 

            if (purchases.Count > Customer.MaximumNumberOfItemsPerPurchase)

            {

                validPurchases = false;

            }

 

            return validPurchases;

        }

    }

Next we have a factory which is responsible for providing access to the validators.

   public class PurchaseValidatorFactory

    {

        public IPurchaseValidator GetCreditLimitValidator(Customer customer)

        {

            return new CreditLimitPurchaseValidator(customer);

        }

 

        public IPurchaseValidator GetDatePurchasedValidator(Customer customer)

        {

            return new DatePurchaseValidator(customer);

        }

 

        public IPurchaseValidator GetNumberOfItemsPurchasedValidator(Customer customer)

        {

            return new NumberOfItemsPurchasedPurchaseValidator(customer);

        }

    }

The CustomerProcess class is responsible for executing the validation when required.

    public class CustomerProcess

    {

        private readonly PurchaseValidatorFactory validatorFactory;

 

        public CustomerProcess(PurchaseValidatorFactory validatorFactory)

        {

            this.validatorFactory = validatorFactory;

        }

 

        public bool ValidatePurchaseDate(Customer customer, List<Purchase> purchases)

        {

            IPurchaseValidator validator = validatorFactory.GetDatePurchasedValidator(customer);

 

            return validator.ValidatePurchases(purchases);

        }

 

        public bool ValidateNumberOfItemsPurcahses(Customer customer, List<Purchase> purchases)

        {

            IPurchaseValidator validator = validatorFactory.GetNumberOfItemsPurchasedValidator(customer);

 

            return validator.ValidatePurchases(purchases);

        }

 

        public bool ValidateCreditLimit(Customer customer, List<Purchase> purchases)

        {

            IPurchaseValidator validator = validatorFactory.GetCreditLimitValidator(customer);

 

            return validator.ValidatePurchases(purchases);

        }

    }

The current implementation is working as required but then we get the following new requirements

  • Exclude from validation any purchase by a standard customer that has a price less than £1000.
  • Exclude from validation any purchase by a premium customer that has a price less than £5000.
  • No change for validating a purchase by a basic customer

At first appearance the new requirements seem quite simple to implement we could change the implementation of all of the existing validators as shown below.

    public class NumberOfItemsPurchasedPurchaseValidator : PurchaseValidatorBase

    {

        public NumberOfItemsPurchasedPurchaseValidator(Customer customer) : base(customer) { }

 

        public override bool ValidatePurchases(List<Purchase> purchases)

        {

            bool validPurchases = true;

 

            List<Purchase> purchasesToValidate = GetPurchasesToValidate(purchases);

 

            if (purchasesToValidate.Count > Customer.MaximumNumberOfItemsPerPurchase)

            {

                validPurchases = false;

            }

 

            return validPurchases;

        }

 

        private List<Purchase> GetPurchasesToValidate(List<Purchase> purchases)

        {

            List<Purchase> purchasesToValidate = new List<Purchase>(purchases);

 

            if (CustomerStatus.Standard.Equals(Customer.Status))

            {

                purchasesToValidate.RemoveAll(purchase => purchase.Price < 1000);

            }

 

            if (CustomerStatus.Premium.Equals(Customer.Status))

            {

                purchasesToValidate.RemoveAll(purchase => purchase.Price < 5000);

            }

 

            return purchasesToValidate;

        }

    }

This change to the validators would require extensive retesting as all the validators would need to be changed; another issue with this change is what if the validators are used elsewhere in the application where the new requirements are not to be implemented.

By using the decorator pattern we can avoid the problems described above; the decorator pattern enables us to modify the behaviour of a class without changing its implementation and we can decide to modify the behaviour only when required.

Below is the StandCustomerPurchasePriceValidatorDecorator which is responsible for implementing the new requirement “Exclude from validation any purchase by a standard customer that has a price less than £1000”. As you can see the decorator’s constructor takes an instance of the class whose behaviour needs to be modified, in our example that could be any class that implements the IPurchaseValidator interface. The decorator’s ValidatePurchases method simply filters the list of purchases to remove any purchase that has a price less than £1000 and passes the filtered list on to the class being decorated to perform the actual validation, therefore removing any purchases with a price less than £1000 from being included in the validation.

    public class StandardCustomerPurchasePriceValidatorDecorator : IPurchaseValidator

    {

        private readonly IPurchaseValidator validator;

 

        public StandardCustomerPurchasePriceValidatorDecorator(IPurchaseValidator validator)

        {

            this.validator = validator;

        }

 

        public bool ValidatePurchases(List<Purchase> purchases)

        {

            List<Purchase> filteredPurchases = purchases.Where(purchase => purchase.Price >= 1000).ToList();

 

            return validator.ValidatePurchases(filteredPurchases);

        }

    }

Next we need to make sure that the validators are decorated only when required, this can be encapsulated within the PurchaseValidatorFactory when creating an instance of a validator it can inspect the Customer entity and decide if the class should be decorated. As shown below a decorator is only added when the customer is a standard or premium customer therefore leaving the validation logic untouched for basic customers.

    public class PurchaseValidatorFactory

    {

        public IPurchaseValidator GetCreditLimitValidator(Customer customer)

        {

            IPurchaseValidator validator = new CreditLimitPurchaseValidator(customer);

 

            return DecorateValidator(customer, validator);

        }

 

        public IPurchaseValidator GetDatePurchasedValidator(Customer customer)

        {

            IPurchaseValidator validator = new DatePurchaseValidator(customer);

 

            return DecorateValidator(customer, validator);

        }

 

        public IPurchaseValidator GetNumberOfItemsPurchasedValidator(Customer customer)

        {

            IPurchaseValidator validator = new NumberOfItemsPurchasedPurchaseValidator(customer);

 

            return DecorateValidator(customer, validator);

        }

 

        private IPurchaseValidator DecorateValidator(Customer customer, IPurchaseValidator validator)

        {

            switch (customer.Status)

            {

                case CustomerStatus.Standard:

                    validator = new StandardCustomerPurchasePriceValidatorDecorator(validator);

                    break;

                case CustomerStatus.Premium:

                    validator = new PremiumCustomerPurchasePriceValidatorDecorator(validator);

                    break;

            }

 

            return validator;

        }

    }

So now we have successfully implemented the new requirements without changing the existing validators and the code which the uses the validators has been completely isolated from the changes therefore reducing the risk of introducing any bugs.

Comments

No Comments
Anonymous comments are disabled
Powered by Community Server (Personal Edition), by Telligent Systems