I'm currently in the early stages of designing an e-commerce application. I'm working out some designs for my classes which have so far been fairly straightforward, but I've run into something of a problem which I suspect is related to the Circle/Square problem. The following (simplified) class diagram outlines what I've designed so far
- There are two classes of sellable products in the system
- Physical products representing an object that exists through the real world (Bikes, Lego, CDs, etc)
- Downloadable products representing something you can purchase over the web (MP3s, eBooks, etc)
- A physical product has attributes such as shipping weight
- A downloadable product has attributes such as file type and size
- Products can themselves be made up of products (components). An album is composed of tracks, a bike is composed of frame, wheels, handlebars, gears, etc, each of which is a product in its own right
- A third type of product, a product bundle, is for grouping otherwise unrelated products together for selling as a single unit (A bike could be bundled with a pump, helmet, etc, a collection of eBooks could be bundled together, and so on).
The Product Bundle is where the difficulties are springing up. A bundle of physical products would have things such as the shipping weight being the total shipping weights of all the products that make it up, a stock level equal to the level of the composing product with the least stock, etc. A bundle of downloadable products would have a total file size of all the files that make up the bundle, a file name list composed of all the files in the bundle and so on.
But what about a bundle that comprises both physical and digital products? Suppose for example you want to provide a digital download of a movie when someone buys the same movie on DVD, or you want to give an eBook about the Tour De France away with a bicycle, now there's a mixture of properties that put the bundle in neither the physical or digital product grouping.
I've thought of the following solutions but none seem ideal:
Put all the properties applicable to both in the superclass
- The Bundle class can easily override methods defined on the superclass to be able to compute the composite values (sum all product weights together, etc)
- This seems like a really poor encapsulation
- You'll have methods on physical products that are only really applicable to digital ones and vice versa
- The superclass starts to become a "god object"
- The storage requirements for objects mean that you always need to store attributes that don't apply to the actual type of object (you're always storing a shipping weight of 0 for downloads, a filesize of 0 for physical products, etc)
Implement abstract methods in the Product class, override them in the subclasses to do something where appropriate, throw an exception where not
- Doesn't really solve the encapsulation problem
- Code that deals with products now has to be prepared to determine actual class of object being processed, deal with exceptions, etc
- Possible violation of the liskov substitution principle?
As above but return default values where the method is not appropriate (0 for weight on a digital product, 0 for filesize on physical products, etc)
- Code no longer has to deal with exceptions or class
- Could lead to unforeseen logical errors when trying to process products in inappropriate ways
Implement physical properties/methods in physical product class, implement file properties/methods in digital product class, both in bundle class with interfaces to tell them all apart (physical implements one, digital implements another, bundle implements both)
- Better encapsulation
- Adds a fair chunk of boilerplate code (interfaces to be implemented, etc)
- The Bundle class will still need to be aware of what type its component classes are
- The problem of what the Bundle class should do when methods that aren't appropriate for a given circumstance remains. Should getShippingWeight return 0 or throw an exception if the bundle's products are all digital?
- Some other solution that hasn't occurred to me yet
Most of the above approaches would still require the Bundle class to do additional work to determine the types of its constituent parts.
Basically, is there an approach of the ones highlighted above that would be preferable to address these issues, or is there some other approach from these that could be better applied to get a good design that doesn't overload a base class with too much functionality and doesn't leave considerable special cases that need to be dealt with elsewhere? Any input is appreciated.