Search code examples
javascriptsingletonsolid-principlessingle-responsibility-principle

Is it possible to avoid using the singleton pattern in my CartManager class for an online shop by using dependency injection?


How can I avoid using a singleton pattern in this case? I am improving myself on good principles.

I am working on an online shop and I have a CartManager that manages all the items added to the cart.

export default class CartManager {
  _totalItemsAdded = 0;
  addedItems = [];
  constructor() {}

  addItem(itemToAdd) {
    this.totalItemsAdded += 1;
    this.addedItems.push(itemToAdd)
  }

  getAddedItems() {
     return this.addedItems;
  }
}

I want to access the CartManager from the following components: HeaderComponent (prints the counter), AllItemsPage (when a product is clicked, it should be added to the cart), and CheckOutPage (displays all the added items).

I am trying to add this dependency as a parameter in the constructor, but I am don't know if it is the best aproach.

// Create a single instance of CartManager
const cartManager = new CartManager();

// Pass the cartManager instance as a dependency to your components
const headerComponent = new HeaderComponent(cartManager);
const allItemsPage = new AllItemsPage(cartManager);
const checkoutPage = new CheckoutPage(cartManager);

Solution

  • I wouldn't recommend passing the entire model into other objects. In this way, you create a thick interface between your model and these objects, and every time when you need to create an object, you will have to construct and pass an entire model into it. This will complicate your unit-testing and is goes against SOLID principles.

    Additionally, in your example, it is unclear how your Header will know that the cart is updated to update itself. In the case of React, for example, React would take care of state change->re-render relation, but in your case, you will have to take care of this by yourself.

    I would recommend you to use a variation of pub/sub approach where your dependant objects subscribe for events in the model.

    Pros of this approach:

    1. The Header doesn't need a model (Cart) as a strong dependency to be created
    2. It is easy to call onCartChanged method of Header with different values to validate its behavior during unit-testing
    3. The Cart's pub/sub methods can be easily tested
    4. No hidden relations. In contrast, when you pass the entire model, it is not visible how a dependent object is using it.

    Please let me know if this helps.

    class Cart {
      addedItems = [];
      onCartChanged = [];
    
      addItem(itemToAdd) {
        this.addedItems.push(itemToAdd)
        this.onCartChanged.forEach((callback) => callback(this.addedItems));
      }
    
      getAddedItems() {
         return this.addedItems;
      }
      
      subscribeOnCartChanged(callback) {
        this.onCartChanged.push(callback);
      }
    }
    
    class Checkout {
      constructor(items) {
        console.log('checkout with items:', items);
      }
    }
    
    class Header {
      onCartChanged(items) {
        console.log('updating header with items', items);
      }
    }
    
    // Create a single instance of CartManager
    const cart = new Cart();
    
    const header = new Header();
    const updateHeader = () => header.onCartChanged(cart.getAddedItems());
    cart.subscribeOnCartChanged(updateHeader);
    
    // When a user adds an item to the cart
    cart.addItem({ name: 'Book', price: 10 });
    
    // Checkout page is constructed right before it is used
    // and it only needs items, hence we can narrow down
    // the interface here too
    const checkout = new Checkout(cart.addedItems);