The OCP is about allowing "team 1" to provide a black-box framework containing classes like
ShoppingCart, and "team 2" to change the list of products and the discount strategy without asking team 1 for a change in their code.
There are usually three major situations to consider:
"Team 2" is a second development team. "Team 1" has to provide "injection points" for team 2 where they can provide new discount strategies along with new products.
"Team 2" are the business people. They don't want to ask the programmers, but simply add new products and discount calculation rules at run time through some nice GUI or configuration files.
Team 1 and 2 are identical (there is no "team 2"). So whenever the business people add a new product or product code which does not fit to the current discount logic, they ask the devs of team 1 to implement this, for each and every minor change.
For case 1, there are several options. The goal is to allow team 2 to provide new discount rules without changing the original
Product code. Team 1 may provide an abstract interface
PriceCalculator, let the the checkout method take an object of that type and let team 2 pass a concrete
PriceCalculator object as a parameter. Team 1 may let team 2 add new product codes into some list or database table. Alternatively, one could also decide to let team 2 provide a list of
DiscountStrategy objects as a parameter to
checkout. Each of those
DiscountStrategy objects could have a method
Discount calcDiscount(Product), which returns
null in case the strategy does not apply, or a discount object (with the discount and loyalty points) in case it applies. Then, the
checkout method can simply iterate over the
DiscountStrategy objects and stop when the first call to
calcDiscount(Product) returns something different from null.
For case 2, one needs to parametrize all kinds of discount calculation rules, store that parameters in some database or file and and implement a generic
PriceCalculator module which can evaluate that parameters (along with some UI for the business people to change them). In your contrived example, this looks pretty simple: introduce a parameter object with three attributes ProductCodePattern, DiscountFactor and LoyaltyPointsFactor. However, reality, discount strategies will often be more complex, requiring different formulas for different product codes.
In case there is more flexibility required, the
PriceCalculator could be some small "DSL" (domain specific language)" interpreter, and the business people specify their discount rules using that DSL.
For case 3, applying the OCP is probably overdesign and not necessary. Nethertheless it may be a good idea to extract the price and discount calculation from the shopping card into some
PriceCalculator class, for increased testability. Since the discount calculation depends mainly on the product, it probably fits better into the
Product class itself. But beware, inheriting from the Product class and providing a different discount calculation rule for each subclass is most probably the wrong approach here.
So in short, for applying the OCP most sensibly, one needs to know the organizational context. The OCP is not an end in itself, you need to know which parts of your code should be open for changes to whom, and which should be closed to whom, and for what purpose.
ShoppingCart(something I deem insufficient) or can be extended to