Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Design Patterns for a SuperMarket system [closed]

I'm a Software Developer I who is beginning to think like a Software Developer II. I am tasked with a relatively simple Use Case for a coding challenge following as part of an interview:build a Supermarket Pricing System.

Rules and Requirements: Each item at Super Foods is identified by a unique four-digit code. Today, pricing schemes at Super Foods use the following pricing categories, but beware: prices are constantly changing, and the sales department is always creating new incentives and deals, such as buy one-get-one free.

EG: Chips and salsa (items #6732 and #4900) cost $4.99 together, but they cost $2.49 and $3.49 alone, respectively.

EG2: Buy two toothbrushes $1.99 each, get one free.

EG3: A bottle of wine (item #0923) costs $15.49 and is taxed an additional 9.25%

Having read through Design Patterns, this looks like a natural place for some form of Decorator Pattern for totaling the sales of objects. A SQLite database, with schema <ID, ObjectName, Price> will also be useful somehow, though I'm rusty on how we go about making the data access objects in all this.

I'm trying to wrap my mind around this in a full stack MVC mindset, I feel like I might be rusty on something. Is this what the Spring Framework is renowned for? Maybe an even better API for this use case can be recommended?

Thank you for anyone helping me to brainstorm the design of this system out.

like image 688
TheHolyDarkness Avatar asked Mar 30 '17 14:03

TheHolyDarkness


2 Answers

I allowed myself to add a little more than design patterns. I treated it as an exercise for me as well :) Let's break it down, and go thru my thought process:

  1. Numbers and names representation in system as types
  • ID - four digits number - unsigned short int - we only need numbers from 0 - 9999 which means we need just 14 bits (2^14 = 16384) and unsigned short int uses 16 bits. That gives us space in case we would like to increase the number of items to 65536 (2^16)
  • Name - string UTF-16, usually is good enough. However we have to remember that some products might come from far countries and use various characters. UTF-16 has only 16 bits per a character (again 65536), so we have to remember in our implementation, that if we need more bits we need to use different encoding (like UTF-32)
  • Price or Discount - is float good enough? I think it's not. In many languages performing various arithmetic operations lead to not fully correct answers (try in JavaScript 0.2 * 0.4 - you can use your browser's console). When it comes to money you don't want to increase prices or loose money. There are special libraries and types implementations to be safe, when it comes to dealing with money
  1. Data structures and classes
  • Item - class/structure with following fields: ID, Name, Original price (types described above)
  • Item Collection - array - As we use short unsigned int for IDs, we can also use it to index the array. We can even initialize the whole array and load all items into memory when the app starts up.
  • PriceModifier - ID - short unsigned int, Type - ENUM to represent type of discount (or tax) and to know what do to in the case, it is applied, Name - String UTF-16 (just to inform an user), Value - short signed int percentage modifier
  • PairPriceModifier - class - extends PriceModifier and adds pair of item IDs.
  • PriceModifier Collection - here we need to use a structure with quick search, as we use ID as a short usigned int, we can use an array of array. That list won't change frequently. When we need to add/remove a modifier, we can do that in a linear time O(1)*O(m), where m is Modifier length of an Item. We won't copy physically Modifiers as we can use references to PriceModifier objects.
  • Shopping Cart Collection - the key functionality of each shop is a shopping cart, it has to be optimized to: add/remove items and to list saved items. We need to frequently add/remove elements and check for already added items to apply various discounts, but also we want to list all saved items (for instance in the cart view). We can use hash table with coupled array [to have O(1) when searching/adding/removing items and O(n) when listing] We update the final price when we add/remove an item. In the array we save a reference to object of a class, which represents an item from shopping cart to know which PriceModifier was applied
  • CartItem - class with fields: ItemID - short unsigned int, Quantity - unsigned int , AppliedPriceModifier Collection - hash table to easy access applied PriceModifiers with coupled array to list them in Cart view.
  • AppliedPriceModifier - class with fields: PriceModifierID, Quantity

Interesting case: In case we add some element into Shopping Cart Collection we need to check the PriceModifier Collection. In O(1) we can access the right list of PriceModifiers we have to apply. Then in case we spot PairPriceModifier we need to check Shopping Cart Collection againm, if we don't have an paired item there. The whole operation takes:

  • O(1) - to obtain array of PriceModifier of length m
  • O(m) - to go thru the array and apply modifiers
  • O(1) - in case we've found PairPriceModifier we access existing item from Shopping Cart Collection
  • O(k)+O(m) - to update collections of AppliedPriceModifier for both items in the Shopping Cart Collection
  • O(1) - to update final price (explained below)
  1. Design patterns
  • For the whole APP structure I think it's good to use some framework with Inversion of Control. To keep our code modulerized and easier to unit test we can use a dependecy injection pattern.
  • For presentation layer we can use either MVC or its mutation - MVVM. It will give us dual binding ability to quickly update UI. Information about final price, deals and applied discounts or taxes is essential.
  • For updating final price, we can use Observer pattern. We observe the Shopping Cart Collection and when something goes in or goes out, we can update final price in constant time
  • To keep our code compact we can use chaining(Cascade)
  • You can use Decorator pattern to add extra functionality to PriceModifiers without changes in classes (for instnace when one situation prevents from applying an another PriceModifier)
  • To keep the CartItems Collection safe from changes outside of the cart, we can use Mediator design pattern. That mediator service would be responsible for applying PriceModifiers, only when an Item is being added or removed from the Cart Collection
  1. Another questions

We can expand the topic and ask:

  • How to represent your data in DB
  • What if we would like to increase number of items and represent them as 10.000 of alpha-numeral characters
  • What to do, if we would like to keep our shopping app 99.9% stable and running
  • How can we handle many requests
  • What happens if a client has a discount applied in his cart, but system administrator removes that discount from the system and many more.

Please add some comments, as it's an interesting topic for me as well. What could I improve/change? What I didn't think through?

like image 82
Oskar Avatar answered Nov 15 '22 09:11

Oskar


Decorator pattern is used to add/modify existing class's behavior without changing class itself. So that acts as a wrapper for existing class, and may be therefore you thought of it. Point is your don't have system that you're extending, you are building it from scratch!

S/w design is tough and cannot be finalized in one go. Also I'm sure your prospect employer is more interested in how you design it, than what tech stack you use. So I'll not comment on that. It's your call.

Per your requirements, these are my initial thoughts. There is room for improvement (Yes!) but at the least this should work for the scope of your assignment. This is C#. That shouldn't stop you from understanding it though.

namespace Entities {
    public class StoreItem 
    {
        // Code
        // Name
        // Cost
        // Tax -> for specific items
        // MfgDate
        // ExpDate
    }

    public class StoreDeal 
    {
        // Code
        // Name
        // Cost
        // Validity
        // Items (type: IList<StoreItem>) -> items participating in a deal
    }
}

namespace Domain {
    public class Cart 
    {
        // Items (type: IList<CartItem>)
        // TotalAmount
        // TotalDiscount
        // FinalAmount
    }

    public class CartItem
    {
        public CartItem(string code) {
            Code = code; // assume "6732" -> Chips
        }

        public CartItem(StoreItem item) {
            MapStoreItem(item);
        }

        // declare props: Code, Name, Quantity, Cost

        public void Prepare() {
            if(Quantity > 0) {
                // Invalid operation alert and return
                // This is one time call per item type
            }
            // Sample. Retrieve item from database.
            var item = new StoreItem { Code = code, Name = "Chips", Cost = 2.49, Tax = 0 /* etc */ }
            MapStoreItem(item);
            Quantity = 1;
        }

        public void UpdateQuantity(int quantity) {
            Quantity = quantity;
            Cost = Cost * Quantity;
        }

        private void MapStoreItem(StoreItem item) {
            Code = item.Code;
            Name = item.Name;
            Cost = CalculateCost(item.Cost, item.Tax);
        }

        private static double CalculateCost(double cost, double tax) {
            // If tax > 0, apply it to cost
            // else return cost as is
        }
    }
}

public class DealService
{
    public StoreDeal GetDeal(string itemCode) {
        // Assume item to be Chips. Retrieve current deal that involve Chips.
        // Sample data. You should delegate this stuff to data access layer.
        return 
            new StoreDeal { 
                Code = "CS4.99", 
                Name = "Chips and salsa @ $4.99",
                Cost = 4.99,
                Items = new List<StoreItem> {  
                    new StoreItem { Code = "6732", Name = "Chips" },
                    new StoreItem { Code = "4900", Name = "Salsa" }
                }
            }
    }
}

public class CartService 
{
    private Cart cart;
    private DealService dealService;

    // ctor - inject dependencies
    public CartService(Cart cart, DealService dealService) {
        this.cart = cart;
        this.dealService = dealService;
    }

    public void AddItem(CartItem item) {
        var found = cart.Items.Find(i => i.Code == item.Code);
        if (found != null) { // Update quantity
            found.UpdateQuantity(found.Quantity + 1);
        }
        else { // Add new item
            item.Prepare();
            cart.Items.Add(item);
        }
    }

    public void RemoveItem(string code) {
        var found = cart.Items.Find(i => i.Code)
        if (found != null) {
            cart.Items.Remove(found);
        }
    }

    public void CalculateTotal() {
        // Used for checking which items in cart have got deal applied on.
        // We don't want "CS4.99" deal applied to both Chips and Salsa, for ex. Only for one of them.
        // So this collection simply holds deal codes already applied.
        var dealsApplied = new List<string>();

        foreach(var item in cart.Items) {
            // Check deal
            StoreDeal deal = dealService.GetDeal(item.Code);
            // Apply the logic for TotalAmount, TotalDiscount, FinalAmount
        }
    }
}

Note that, if you were to design such system for real, then there would be much more classes than above. For example, in real case "Chips" is not an item, it's a type of item and hence cannot have Code. However, "Lays Potato Chips" would be an individual item of type "Chips" with it's own Code. Also StoreItem would become abstract entity which is derived by types like EdibleItem, PersonalCareItem, HealthCareItem, CosmeticItem and anything that exists in real store where EdibleItem will specifically have Nutrition info which does not apply to other in this list.

Lastly, I just wrote this (incomplete) code, didn't test it! Code is incomplete where you see comments, and did this purposely because I don't want you to cheat interview blindly. :)

like image 44
Nikhil Vartak Avatar answered Nov 15 '22 08:11

Nikhil Vartak