3

I am working on a class design question - design a simple grocery store with a self-checkout system I am a beginner

After briefly jotting down requirements, I started with the Product class as follows -

class Product{
    String barcode;
    int price;
    String description;
    ...
}

However, some products like vegetables are priced by weight and some others like a chocolate are not. So I created 2 subclasses of Product class - ProductPricedByQuantity, ProductPricedByWeight. And added a "Rate" class attribute for ProductPricedByWeight class.

class Product{
    String barcode;
    String description;
    ...
}

class ProductPricedByQuantity extends Product{ int price; ... }

class ProductPricedByWeight extends Product{ Rate rate; ... }

/example - $1 per 100 grams/ class Rate{ int price; //$1 int measure; //100 grams }

Now at the checkout, when the barcode of a product is scanned, if it were a ProductPricedByQuantity, then its price is simply added to the total bill amount, but if it were a ProductPricedByWeight, the price is calculated follows -

Rate rate = currentProduct.getRate();
totalPrice = (rate.getPrice()/rate.getMeasure()) * weight of the product

This approach tightly couples price (or rate) with the product classes. A simple grocery store would want to change its price often. How do I decouple these classes to ensure changing the price without having to modify product classes?

Also, what is the best design for the above scenario? (different products, priced differently)

Thanks in advance!

2 Answers2

3

Let me start off by saying that as in most cases with software engineering, there are probably many possible solutions. Since you are a beginner, I also want to give you some general advice.

Favor composition over inheritance

A lot of times beginners overuse inheritance, trying to model every noun as some class in a deep inheritance structure.

What does that mean in this specific case? You created two subclasses for behavior that is going to change (price calculation). While inheritance is a useful tool and could be applied in this case, there is a well-known pattern that you can apply: inject the price calculation as a strategy

class Product {
     // attributes etc. here
 // Inject this dependency any way you like, e.g. constructor injection
 // You can even change it at runtime
 PriceCalculation strategy;

 double calculatePrice() {
     return strategy.apply(this);
 }

}

interface PriceCalculation { double apply(Product prod); }

class FixedPriceStrategy implements PriceCalculation { int price; double apply(Product prod) { return price; } }

class RateStrategy implements PriceCalculation { Rate rate; double apply(Product prod) { return (rate.getPrice()/rate.getMeasure()) * prod.getWeight(); } }

Now, this might be overdoing it in your specific case. Some acronyms thrown in:

  • Keep it simple s... (KISS), because you ain't going to need it (YAGNI)

Why did I mention that? Before coming up with a complex design, it's always a good idea to think about whether you need that.

In this case, you mentioned the price will change and the rest will stay fixed. However, this only relates to product instances, not the class design (value of price will change, but the type of price/product will not). So you could just go ahead with the current object hierarchy.

Here, I have one more advice for beginners: Inheritance can be a useful tool for code reuse (in your case basic product attributes), but more generally it is one of several ways to implement polymorphism. Use that!

In this specific problem, put the calculatePrice method on the product in true OOP fashion, where data & behavior should be tightly coupled, and let the subclasses implement the concrete behavior. Users of the product class (i.e. your shopping cart) then don't need to know how the price is calculated and don't need to know the specific subtype, they simply call Product.calculatePrice. (Some more links that I might expand on later: Liskov and Polymporphism, and Anemic data model).

sfiss
  • 955
0

The solution to the problem has nothing to do with decoupling classes. It's about polymorphism - doing the same thing, but differently. There will be different versions of Price.

The goal is to handle all objects as a "product" and call a "price" method. This can be done with different "Price()" method signatures. That is a basic, classic polymorphic solution.

public class Product {
  String barcode;
  double price;   // I don't see how all prices can be whole numbers, i.e. an 'int'
  String description;

public double Price( int quantity ) { return price * quantity; }

public double Price( double weight ) { return price * weight;
} }

Or subclass if you prefer. There is no need for making up interfaces. That's overkill.

public class Product {
  String barcode;
  double price;   // is per unit/each by default
  String description;

public Product ( double price, barCode, description ) { this.price = pricePerUnit; }

public virtual double Price( double quantity ) { return price * quantity; }

public class Veggie : Product // this.price is now per-weight, just because we said so

public Veggie ( double pricePerWeight, barCode, description ) { this.price = pricePerWeight; }

public override double Price( double weight ) { return price * weight;
} }

radarbob
  • 5,833
  • 20
  • 33