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!

darklion
  • 39
  • 3
  • Welcome to StackExchange. You are asking good and interesting questions. It is good practice on this site to make one post per question, even if the questions are related. In your case the two questions make sense independently and you should be able to easily split this post in two. – Helena Mar 18 '23 at 10:00
  • Another product type is priced by having a base barcode with the last four digits being the price in cents. This is commonly used for pre-packaged items like meat. – kiwiron Mar 18 '23 at 19:09
  • what language are you writing in? – radarbob Mar 19 '23 at 00:11
  • @Helena, edited the post, thank you! – darklion Mar 20 '23 at 21:42
  • @radarbob I'm using Java – darklion Mar 20 '23 at 21:42
  • note that being measured by weight or quantity, in most supermarkets I've ever seen, **is** tightly-coupled to the product and not to the price tag. – user253751 Mar 20 '23 at 22:01
  • 1
    Many of them also treat weight and quantity the same - if you think about it, what's actually the difference? – user253751 Mar 20 '23 at 23:21

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
  • 667
  • 4
  • 7
  • Even though your remarks are false, I have updated the code a little bit to account for potential misunderstandings. However, let me point out that 1) this is absolutely composition (Product composes PriceCalculation). How you construct your object graph does not change what composition is. 2) I even injected an interface, not sure how much more clear it can be that different implementations can be injected? 3) I stated that many solutions are possible. In fact, the whole last paragraph describes how to use subtyping to achieve the solution (It is equivalent to your proposed solution). – sfiss Mar 19 '23 at 08:20
  • Interface implementation is defining methods or properties that a class must implement. Composition, on the other hand, refers to the process of building a new object by combining two or more existing objects. In composition, an object is composed of one or more other objects, which are typically created and managed by the object being composed. Composition allows objects to be built from smaller, reusable components, which can simplify code and improve code reuse. – radarbob Mar 20 '23 at 00:40
  • In C# a `delegate` is an object that serves as a container for a method. It is a technical feature of the language to allow a method to be treated like just another variable. Now we can instantiate various `priceDelegate` objects each with different price calculations. Passing a "priceByWeight" object into the `Product` constructor is composition per se. So we can instantiate `Product` objects each with a different `Price()` method. Note there is no overriding, no subclassing, and no `interface` implementation. Just composition. – radarbob Mar 20 '23 at 01:18
  • Absent code or explanation, I did not see the composition of it. So my observation, now deleted, was wrong; but not false. – radarbob Mar 20 '23 at 20:43
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,808
  • 18
  • 31