2

Assume we are in the world of car rental application :)

Let's say that I have 3 types of cars with 2 categories of price:

Car type         Price category
------------------------------
Sport            High
Luxury           High
Economy          Low

Values of High and Low can change over time, so they should be a separate entity with its own representation in DB.

Every car type has its own price calculation strategy, e.g. renting a Sport car costs (High x time_rented) + High_constant_factor.

Below is my current approach:

  • one calculation strategy/policy per car type. It encapsulates know-how required to calculate the price, with the following responsibilities: determines constant factor to be used, fetches appropriate price from a repo and finally calculates the total price by applying the right calculation formula. Example: SportCarPriceCalculationPolicy uses a hardcoded HIGH_CONSTANT_FACTOR and fetches findPrice(Low).
  • a Car knows nothing about its price or constant_factor.

Is that a good approach? Or should Car be aware of its price and pass High/Low as an argument to a policy and avoid that policy has any dependency on repository?

2

You have chosen the right approach.

Strategy and parameter passing

The intent of the strategy design pattern is according to GoG to:

define a family of algorithms, encapsulate each of one and make them interchangeable. Strategy lets the algorithm vary independently from the client that uses it.

The parameter passing between the strategy and its context requires some tradeoffs:

  • The interface for the strategy shall be common to all the supported algorithms. So, passing only a few values will constrain future evolution.
  • Passing the context (will the Car invoke the strategy or will a renting contract invoke it ?) is known to create a tighter coupling. But it increases flexibility if the parameters have to evolve
  • Letting the strategy find itself elements that it needs seems a reasonable option, and lets freedom for making the encapsulated algorithms more complex.

Your choice of responsibilities

In your model, the strategy is responsible for finding additional parameters. It's a valid choice with the following advantages:

  • the details of the algorithm and finding any additional calculation parameters remain in the internal kitchen of the strategy. That's separation of concerns.

  • evolution of the algorithms is not limited to a minimalistic set of parameters. In particular, the interface will remain stable (i.e. it doesn't have to change every time you create a new fancy pricing algorithm).

By the way, the car cannot know its price. You explained why: the base price changes frequently. So, a car has several prices. The right price can only be determined in combination with a rental date. So the price is something associated with the car. The only question that remains is whether it's an entity or a value object ? (I'd opt for the second: we don't care about the identity of a price: we just take care of its value)

Evolution of pricing strategies

Ensuring that the pricing strategy can easily evolve is critical for your design. You start with a very simple pricing model. But this could quickly have to evolve into something more complex.

Pricing depends more generally on 3 different categories of informations:

  • Product: you only look for the price category. But you could also want to look at other product attributes, such as the millage, the power of the engine, the type of energy, the brand.

  • Customer: you ignore this completely. But you may want one day to grant some good customers some advantages.

  • Transactional conditions: you look only at the quantity, and the date of the pricing. But you could in future desire to take into account also some special holiday promotions, payment conditions (e.g. discount for advance payment), or geographical criteria.

In addition, the pricing algorithm could evolve:

  • you currently use the same calculation method, with the strategy having only limited a limited impact by influencing the parameters of the formula.
  • but tomorrow you may consider the milage and the geographical place for luxury cars, and the engine power for sports car.
  • You could also chose different formulas. For example degressive for economy, constant for luxury, progressive for sports.
2

I'm going to offer a different, slightly more DDD, approach to this situation. Rather than focusing on a cohesive mechanism like PriceCalculationPolicy, let's let that be an implementation detail and focus on your model instead.

What we need here, in order to make the implicit explicit, is a new domain object that captures the behavior/concept we are dancing around. What we need is a Rental. A Rental encapsulates all of the information needed to both fulfill/track/manage the car rental process and also calculate the price.

Currently, it seems you only use information like CarType, PriceTier, and ExpectedDuration, but conceivably any sort of transactional details/ancillary information could be added as well: Date, CustomerAge, CouponCode, etc.

So new when a RentCar command comes in, your application can use the information sent with the command (CarType, CustomerId, etc.), to explicitly create a new Rental and persist the information back into your data store. Utilizing an "Accumulating Snapshot Table" (or equivalent) in your backing store allows you to incorporate methods like Rental.SelectCar(lotId, carId) and Rental.ReturnCar(lotId) that track the progress of a Rental through it's life-cycle.

As far as pricing is concerned, if you need differing algorithms rather than just differing parameters, you are free to create different kinds of Rentals. The important bit here is, rather than have all of this data transiently exists as part of a command script, to instead model the behavior so it can be captured. Let the rules exist with the data, NOT around the data.

The issue with domain objects like Rule and Policy is that they separate data and behavior. That is, your Rental simply becomes a bag of data that gets inspected/crunched according to some rules. Separating data from behavior is a fundamentally procedural paradigm, not OOP, and certainly not DDD. Creating a separate cohesive mechanism to encapsulate algorithmic functionality is best suited for situations where the logic is so verbose that your model starts to lose focus ((x * y) + z does not count).

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.