I have always advocated in favor of decoupled object models for testability and flexibility, but what does that really mean? In this post I will, through an example, demonstrate what I mean by a decoupled object model and highlight the major benefits of the approach.
The example I will be using is a simple shopping cart.
The first order of business is to look at the general concept of a shopping cart and break it down into domain objects that can be used to describe the cart and its operations. For brevity I have decided to limit the functionality of the cart to adding, removing and calculating the total price of items in the cart.
As you can tell from the code below I have created two domain objects – shoppingCart and shoppingCartItem. The shoppingCart model contains methods for adding, removing and calculating the total price, but notice how it delegates the actual calculations to the individual items. Separating this logic is important since it enables to to support complex logic at the item level without having to modify the cart. As you may have guessed, this sets us up to support polymorphism if we in the future want to go with different types of item models. The beauty of the solution is that any future complexity in price calculations will be handled at the item level without any need for branching on items types in the cart model.
Shopping cart items are totally unaware of the shoppingCart, and how a group of items are organized – making this a one way dependency chain without unnecessary couplings. Ideally a fully decoupled object model should only know how to carry out tasks specific to its domain, and be ignorant of how it's being used in a bigger picture. We are taking advantage of this when writing unit tests. Properly decoupled models should be just as easy to use from a unit test as from a markup view.
Shopping cart code:
As you will see in the unit test, the functionality is completely encapsulated in the object model, but it makes no assumption about how it's being used. Very different markup views or unit tests can use the same exact model without requiring modifications to the model. Benefits of refusing the same model across different views are obvious, but the impact on unit testing is huge here since we can fully test the whole feature without a dependency on the DOM.
The test below shows how we can realistically unit test a full cycle of user interactions of adding and removing items with updated price calculations.
It's of course recommended to include tests for the individual operations in isolation, but for brevity I am only including a test of a full sequence of user interactions working in concert.