-Buy X, get Y free
-Amount A off when buying quantity Q
What we know:
-List of products
-List of discounts
-Quantity and type of product the customer wants to buy
What we are interested in finding out:
-Which discount provides the biggest savings taking all of the products into account, in aggregate?
-What is the total saving with that discount?
-What is the total price of the customers' chosen products (taking quantities into account)?
-What is the discounted total price?
I have a feeling my code smells, but I can't pinpoint why exactly. I seem to be using a lot of static methods. Also, I don't like how the Buy X, get Y free discount needs the price of the product applied because that couples it to the specific product, while Amount A off when buying Quantity Q can be more generic.
You're right, your code smells from top to bottom. The first problem I see is that it's not object-oriented. That's why you're using a lot of static methods, because the perspective you have when you wrote the code is that you, the programmer, are in control of everything that's happening. I like to call it the "Wizard of Oz" perspective where you're the man behind the curtain, pulling all the levers and controlling every little thing that happens. Good object-oriented programs written from the proper perspective read like a story, where each object has its role and responsibilities and all you need to do is create the objects and connect them to each other, then start a chain reaction by telling one object to do something. That object then works with other objects to do more things until finally, the collection of objects have done each of their "things" to contribute to a larger goal.
Here's an example of code that's not object-oriented:
Here's the first step of refactoring (not the last step, mind you) that can be done to make the perspective more object-oriented:
The next step would be to ask where lines 25-34 above belong, which object/class should be responsible for doing this. The answer is NOT the ProductsHolder class. That will probably violate the Single Responsibility Principle (SRP).
There are many more problems in your code but again, the main problem is that the code is written with a more procedural mindset rather than an object-oriented one.
Here's another problem. Your Product class has a quantity field. Why? It may seem convenient to have the quantity field in Product but the name suggests a more general and long-term nature whereas a quantity is a temporal attribute, that is, it will change over time. Additionally, the quantity of a product is more closely related to a purchase, specifically, a line item in a specific purchase or transaction. I would look to refactor product by separating attributes that are more permanent/long-term (like name, description) from the ones that are temporal and short-term (price, quantity).
AmountPerQuantityDiscount and QuantityPerQuantityDiscount are strategies or rules that can be applied in calculating a discount. Read up on the Strategy pattern to see how you can refactor those two classes so that they follow the Strategy pattern.
Junilu Lacar wrote:
The next step would be to ask where lines 25-34 above belong, which object/class should be responsible for doing this. The answer is NOT the ProductsHolder class. That will probably violate the Single Responsibility Principle (SRP).
That answer is NOT the Products class either. That whole class is smelly and needs to go away. My first inclination would be to explore the concept of a "purchase" or "order" which might have a list products to be purchased, the quantities of each product, and the kind of discount to be applied.
You're right. There's definitely value in introducing a Cart/Order/Transaction type object.
Here's what I've done so far:
-Realized that almost no code should remain in Controller. It should simply trigger a calculation, then return the info we need, no other responsibilities.
-Introduce an Order class. This class receives the list of products and quantities and maintains a map of line items with Product <-> Quantity.
-Moved all calculations from Discounts into Order, as Order has good visibility of the products and quantities.
-Realized that this gives Order too much responsibility. Introduced a DiscountCalculator object and gave it those discount-related calculations. An Order has-a DiscountCalculator.
-Renamed Info to OrderInfo. An Order has-an OrderInfo. (To simplify further, I could just get rid of this altogether and return the processed Order.)
-Attempted to introduce the Strategy pattern. Discount is now more flexible, but in my opinion somewhat kludgy. Look in DiscountsHolder where we create new Discounts to see what I mean. I did not see the value in keeping the two discount type classes around _and_ introduce strategies. But I've never used this pattern before.
-Created Strategies for calculating the two types of discounts we have so far.
Let's start with some of the names you use. Forget what you know about the design and think about your code from the perspective of someone coming upon it for the very first time. If all they read was the code above, they'd probably ask, "What's the difference between an Order and an OrderInfo object? How do we know what product we're associating with each of those quantities? Why does the calculate() command give me back an OrderInfo object?"
This part of the design just does not make sense at all. The names you use and the behaviors that the objects exhibit just don't match and they don't tell a coherent, cohesive story. It gets even worse as you dig deeper.
From that perspective, I would be very confused indeed.
Since products are not mentioned anywhere in that class, I would not necessarily even know that the quantities are for products.
It seemed like a good idea to put the three things we are interested in in a class, but it wasn't.
The calculate method is too generic. It also doesn't say anything about what it's calculating - discounts? A price perhaps, but what kind? Frequent buyer points?
One thing I did not reveal initially - sorry about that - is that the quantities arrive through a servlet.
I have made it so that an id arrives along with the quantity and we can populate our list items collection based on that. I hope the servlet/controller code makes more sense now.
I'm struggling with the DiscountCalculator class. Specifically, the assign() method is somewhat silly. There is a requirement that says a product can only have an 'amount per quantity' type discount applied if the product itself is 'that kind of product'. I am using the product name to check for that. Perhaps we would be better off with a field on Product to set that. Not sure. The rest of the methods are not very elegant either.
Still a lot of non-object-oriented code. I'll just point out one glaring example. In the DiscountCalculator.assign() method:
Nobody reading this (except maybe you) will understand this code. What is the significance of "-6000"? So what if the name starts with that? Why would a name start with a number anyway? This code is all about implementation detail and you showing intimate knowledge of it and manifesting that knowledge in code. However, the intent of the code is lost in the implementation details. The perspective is still from your point of view, of you taking a reference to objects (discount and product), looking at their names (calling getName() method), then checking it against a specific value ("-6000") with no indication of what that value's significance is to the logic around it.
Then, in two cases, you will add the discount to the assignedDiscounts list. If you draw up a truth table for this logic involving the discount and product objects, there are two other cases that are not accounted for (I'll let you figure out what those cases are). While this may be what you intended, the way you wrote the logic is misleading because the if and the else parts of the statement lead to the same result. It's like saying "Heads I win, tails you lose" when calling a coin toss. it makes no sense.
A more object-oriented approach would be to flip your perspective 180 degrees so that instead of asking these objects for information and doing the logic evaluation outside of the object, you'd simple ask the object to evaluate the logic internally. Again, I don't know what your intent is for checking startsWith("-6000") but let's just suppose that in the given context, the intent was to determine if a discount should be assigned. Then, a more object-oriented design of the interaction with these objects might look something like this:
EDIT: I commented out the original suggestion since the call doesn't match the List.add() method signature. Since your design isn't very clear to me, I'm not really sure what kind of object would be passed. Logically, it would be some kind of object that establishes a relationship between the given discount and product objects.
The details of evaluating whether "should assign discount" is true or false occurs inside the object, where the "knowledge" of its name and whatever logic is involved related to name is contained and encapsulated. Anything external to the product and discount objects don't have to care what those details are. They just need to ask the product object the question via the shouldAssign() method, "Should this discount be applied to you?"
If that becomes unwieldy or awkward, then another shift in perspective might lead you to a better design:
The question is more or less the same but the perspective is switched from asking the product if a discount is applicable to it to asking a discount if it can be applied to a particular product. I like to use refactoring as a way to experiment with different perspectives to see whether assigning responsibilities one way or another results in a more coherent and cohesive design.
Here's another point about refactoring. My main goals for refactoring are aligned with the Four Rules for Simple Design, in short, one or all of clarity, eliminating duplication, keeping things small, and making code testable and verifiable.
In my experience, Refactor-Rename is one of the simplest yet most neglected refactoring techniques available to programmers. This is why when I mentor other developers, Refactor-Rename is what I focus on first, accompanied with teaching them how to choose good names in code. Poorly named classes, interfaces, methods, fields, variables, etc. probably contribute to the more than 50% of the problems with code.
Take my first suggestion for example:
After reading this again, the name shouldAssign() is not the best name to communicate the intent. To find a better name, apply Refactor - Rename several times, each time reading the code out loud to discern whether you're truly communicating the correct intent. Here are some alternatives:
There could be other names you can use for that method that will convey the intent more clearly and concisely. Or perhaps you like the other perspective better:
The whole point of this Refactor-Rename exercise is to increase the clarity of intent of the code and thus make it more readable and understandable by others.
If you can master the application of Refactor - Rename to clarify the intent of code, then add to your toolbox Refactor - Extract (to eliminate duplication, to make things smaller, to move behavior/responsibility), and Refactor - Compose Method to simplify and clarify, to get a Single Level of Abstraction, then you will have enough to clean up 80% or more of the problems in any code that you will write or encounter.
For me, the main keys to mastering these three refactoring techniques are:
1. Knowing how to properly assign responsibilities - this needs diligent study of design principles like SOLID, KISS, SLAP, and GRASP 2. Focusing on clarity of intent
3. Always thinking about the Four Rules of Simple Design 4. Practicing Test-Driven Development (with a strong focus on design thinking)