Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pattern for Plugins - IoC/DI or not?

Just a very general question, that not only applies to this example.

Let's say you have an Online Shop and you want to implement Vouchers/Gift Certificates, but with Constraints. Let's say you have a voucher for 20% off, but that applies only to products added within the last 3 weeks, but not to ones in a special promotion.

I see two ways to solve it: The first way is to code your shop to "natively" support all crazy types of vouchers. This seems to be the classic way, but it means a lot of work beforehand and very little flexibility (After all, you can't know beforehand what you need, and maybe Sales may come up with some really great new promotion which requires new vouchers - by next Monday).

The second way is a Plug-In way: Vouchers are like Plugins and each Voucher has it's own Code. You pass in the Shopping Basket into the Voucher and then the Voucher itself checks each item if it applies, makes the neccessary changes and returns the changed shopping cart.

I just wonder, what is the Design Pattern for Case 2? It looks a bit like IoC/DI, but then again not really because Vouchers are not replacing any existing functionality. It's more like a set of Object with a Special Interface (i.e. IVoucher), and then a Queue of IVoucher Object that gets iterated over. Is there a standard pattern (and best practice) for these types of "Manipulators"?

Edit: Thanks for the Answers. To clarify that just a bit, the Vouchers (or Manipulators - as said, this is not only a question about online shops but about a similar situations) are "heavy" objects, that is they have Business Logic in them. So I can say that a Voucher only applies if the Customer signed up before January 1 2008, only if the customer ordered at least 100$ in the past 6 months, only applies to articles in the Category X, "stacks" with other Vouchers except for Items marked as Reduced etc. etc. etc. So my concern was more about how to keep a clean structure to make sure the Vouchers get all that they need to check whether they apply and to be able to manipulate the Cart, so I wondered about what the standard for such situations are, which is exactly what the Visitor Pattern seems to do.

like image 582
Michael Stum Avatar asked Dec 10 '22 21:12

Michael Stum


2 Answers

It's a case where you could use the strategy pattern along with the vistor pattern to calculate the value of the basket.

A vistor could visit each item in the basket utilising different strategies (in this case discount vouchers) and using those to calculate the full cost of the basket.

The vouchers used could be retrieved from a database in some way and injected into the visitor quite easily.

The voucher strategy could look something like this:

public interface IVoucher
{
    decimal CostOf(CartItem cartItem);
}

The default would be something like this:

public class FullPriceVoucher : IVoucher
{
    public decimal CostOf(CartItem cartItem)
    {
        return cartItem.Cost;   
    }
}

A 10% discount would be something like:

public class TenPercentOffVoucher : IVoucher
{
    public decimal CostOf(CartItem cartItem)
    {
        return cartItem.Cost * 0.9m;   
    }
}

Then you could have a visitor for calculating cart value like this:

public class CartValueVisitor
{
    private IVoucher voucher;

    public CartValueVisitor(IVoucher voucher)
    {
        this.voucher = voucher;
    }

    public decimal CostOf(Cart cart)
    {
        return cart.Items.Sum(item => voucher.CostOf(item));
    }
}

Which you would use like:

var cart = GetACart();

var fullPriceCartValueVisitor = 
        new CartValueVisitor(new FullPriceVoucher());
var tenPercentOffCartValueVisitor = 
        new CartValueVisitor(new TenPercentOffVoucher());

var fullPrice = fullPriceCartValueVisitor.CostOf(cart);
var tenPercentOffPrice = tenPercentOffCartValueVisitor.CostOf(cart);

This obviously only works with a single voucher at a time but should give you an idea of the general structure.

like image 131
Garry Shutler Avatar answered Dec 17 '22 09:12

Garry Shutler


The previous answers suggesting Visitor and Strategy patterns sound fine to me, although Visitor is overkill in the typical case where each purchase item is an object of the same concrete class. The purpose of Visitor is to allow dynamic dispatch on two (or more) object types -- the visited objects are part of one hierarchy, and the visitors are part of another. But if only one object type (the concrete type of the class implementing IVoucher) varies, then regular old single-type virtual dispatch is all you need.

In fact I personally wouldn't bother with any "pattern" at all -- your own description is exactly what's needed: create an interface, IVoucher, and a bunch of classes that implement that interface. You'll also need a factory method that takes a voucher code and returns an IVoucher object having the appropriate concrete type.

Beware Non-Commutative Vouchers!

The fact that you mention a queue of IVoucher-implementing objects will be run against the purchase items implies that more than one voucher may be used. In this case you need to be careful -- does applying voucher A, then voucher B always have the same effect as applying B then A? Unfortunately many typical "special offers" would seem not to have this property (e.g. if voucher A gives you $10 off and voucher B gives you 5% off, the order definitely matters).

A quick and dirty way out of this is to assign each voucher a distinct numeric "priority" value, and always apply vouchers in priority value order. To reduce the probability of "weird" combinations of vouchers driving you bankrupt, it's probably also a good idea to limit voucher combinations to some set of allowed combinations specified in your code somewhere. (This could be as simple as a list of lists of voucher codes.)

like image 24
j_random_hacker Avatar answered Dec 17 '22 08:12

j_random_hacker