2

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).

Class Diagram

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.

2

What's your use-case?

Domain-Driven Design is (partly) about implementing specific models rather than general ones: a model which solves your particular business problem really well is much more valuable than a model which sort-of-solves lots of problems.

To me it looks like the most general part of your current design (by which I mean the part which is trying to be the most things at once) is the ProductBundle. So let's try to cut its responsibilities down by examining use cases.

If, for example, you're designing a shopping cart system, then you may be better served by a model which accumulates a flat list of items in the cart, and then performs all the calculations (bundles, discounts, and so on) at the checkout, perhaps using some sort of rules engine. ProductBundle may still appear in this design, but only as an implementation detail of the discount engine.

On the other hand, if you are implementing an inventory/warehouse control system which tells you when to order more items, then you might model products as individual SKUs with, say, shipping times, quantities, and so on. ProductBundle may still appear in this design, but in a purely advisory role ("X is in a bundle with Y; you should order them together").

If your system has to fill both use-cases then you need to evaluate the likelihood of friction as the application evolves. (It sounds like you're already experiencing such friction.) You can alleviate this pain by splitting the application up into bounded contexts.

  • I haven't had time to look at this in a while but to be brief a ProductBundle is basically a collection of products that are to be treated as a single unit addressed by a distinct SKU. Its price is the sum of all the prices of its bundled products, its weight is the sum of those products, its stock is equivalent to the stock of the bundled product with the least stock, etc. This is a bit different from a component because while a component can be purchased separately, a component is implicitly already assembled into its product, which is a single unit. – GordonM Nov 27 '14 at 9:22
0

I would expect the bundle class to have 2 lists, I am assuming that the downloadable and physical products are in 2 different tables with 2 different sets of keys. So you can have you 2 separate lists.

I would have a method in the product bundle that returns an object with total digital items, total digital size, total physical items and total physical weight, 0 being perfectly legitimate answers to return and can be filtered out for different display purposes. Though if you prefer you can have one method for each value.

0

There are several ways to approach this depending on what tradeoffs you want to make.

One such approach would be to have a set of classes that accept a Product and provide different information from it. For example, a ShippingWeightViewAdapter which knows how to get the shipping weight from any Product or subclass, providing an aggregate value where appropriate.

This is a similar approach to some MVC frameworks, e.g. Eclipse. You can have a window that provides information on some object, and another object that adapts it for the view. For example, by filtering compiler errors based on what is selected. If you select an object that is not compiled (e.g. a plain text file) it simply shows nothing.

The same principle applies here. A view adapter for the shipping weight could see that a product has no weight, and assume zero. Maybe that product is bundled with another product that does have a shipping weight, and the view can handle that by adding up the shipping weight only for physical products.

This approach has several advantages:

  • It is extensible. Add a new property? Add a new view adapter to go along with it. If this is plugging into some web framework, you might not even need much code.
  • Products can stick to the single responsibility principle. They contain data, and do not need to know how to aggregate data as well.
  • View adapters also have a single responsibility: aggregate a single piece of data.
  • Different types of products do not need to know about each other. There is no need to keep default method implementations in the superclass, nor do subclasses need to "return 0" for properties they do not have. Each class has its own and only its own properties.
  • The view adapter classes can be plugged into arbitrary other classes: they have interface segregation (the I in SOLID) which helps keep them loosely coupled.
  • The adaptor idea sounds interesting, but I'm not sure it's right as weight would be part of the business logic rather than just a presentation thing. For example the total shipping weight could determine how much delivery is going to cost. – GordonM Oct 28 '14 at 21:31
  • @GordonM weight is a property of an object. Unless you are shipping from a planet with different gravity, it is constant. Shipping cost could definitely change based on the weight, distance to ship, domestic v. international, 2nd day vs. ground, etc. Granted it is not my system, but I would expect weight not to be part of the business logic because it is a natural property of the items being shipped. – user22815 Oct 28 '14 at 21:43
0

There's also a Key-Value approach. Each type of product would have its own set of properties stored as individual key-value items in your database. So you would not have a shippingWeight property as part of a class. Rather, you'd request the 'shippingWeight' value as needed. Some ecommerce systems use this (Magento, I believe) for it's flexibility. The downside is it can be a PITA to work with. But I thought the option worth mentioning to add to the list you presented.

Personally, barring any really good reasons to do otherwise, I'd put the fields into the parent class to keep things simple. The increased storage is unlikely to be a practical problem. But I would move much of the functionality, such as shipping calculations, into other classes like @snowman was describing.

  • I'm glad you mentioned Magento and that EAV tables can be a pain to work with. :) That's the kind of design I'm trying to avoid if at all possible (assuming that this project sits purely on an SQL database and not entirely or partially on another storage mechanism, a decision I've not made yet). It's not just a matter of storing inapplicable data though, it's also a case of what constitutes a "clean" design, and putting stuff in the superclass because some subclasses might need it feels like a very wrong approach. – GordonM Oct 27 '14 at 21:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.