Factory Pattern (Bite Me Burgers v2)

Factory Pattern (Bite Me Burgers v2)

This article is part 2 of the series design patterns. Check previous parts for continuity.

Mr Johnson is happy with v1 of Bite Me Burgers but he wants to add more burgers to his menu. He calls Jack and asks him to add Chicken and Veg Burger to the menu. He also gives the required details to add them to the menu.

The Problem

While adding two new burgers to the menu, Jack realizes that there is something wrong with his code. He finds himself having to go to multiple places and edit his code to add burgers. He understands that he will have to repeat the same process if more burgers are added or removed also in multiple other pages that might come up in the future like orders page. He approaches Emma to check if there are any issues with his code.

As Emma starts going through Jack's code, she observes that the instantiation of Burger objects is directly happening in the List and Details pages of the application. She starts adding comments to Jack's code where ever there is a scope for improvement.

Attaching code for reference

BurgerList.tsx

const BurgersList = () => {
  /**
   * New CheeseBurger, ChickenBurger and VegBurger instances are being created here
   * Whenever burgers are changed, they should be added or removed here 
   */
  const cheeseBurger = new CheeseBurger();
  const chickenBurger = new ChickenBurger();
  const vegBurger = new VegBurger();
  const { Title } = Typography;
  const navigate = useNavigate();
  return (
    <Row className="w-full h-full overflow-y-scroll" justify={"center"}>
      <Col className="space-y-2 p-2" span={22}>
        <Row>
          <Col>
            <Title>Menu</Title>
          </Col>
        </Row>
        <Row justify={"center"}>
          <Col span={18}>
            <BurgerListCard
              burger={cheeseBurger}
              onClick={() =>
                navigate(`/no-patterns/${cheeseBurger.getBurgerType()}`)
              }
            />
            <BurgerListCard
              burger={chickenBurger}
              onClick={() =>
                navigate(`/no-patterns/${chickenBurger.getBurgerType()}`)
              }
            />
            <BurgerListCard
              burger={vegBurger}
              onClick={() =>
                navigate(`/no-patterns/${vegBurger.getBurgerType()}`)
              }
            />
          </Col>
        </Row>
      </Col>
    </Row>
  );
};

export default BurgersList;

Construction code of BurgerDetails.tsx

import React, { useEffect } from "react";
import { useParams } from "react-router";
import Row from "antd/es/row";
import Col from "antd/es/col";
import { CheeseBurger } from "../burgers/CheeseBurger";
import { Burger } from "../burgers/burger";
import { BurgerTypes } from "../withPatterns/Factory/factory";
import { ChickenBurger } from "../burgers/ChickenBurger";
import { VegBurger } from "../burgers/VegBurger";
const BurgerDetails = () => {
  const [burger, setBurger] = React.useState<Burger>();
  const { burgerName } = useParams();


/**
 * getBurger method is creating burger object based on the burgerName 
param fetched from the url. 
 * This method constantly keeps updating whenever burgers change
 */
  const getBurger = (burgerName: BurgerTypes) => {
    if (burgerName === BurgerTypes.cheese) return new CheeseBurger();
    else if (burgerName === BurgerTypes.chicken) return new ChickenBurger();
    else if (burgerName === BurgerTypes.veg) return new VegBurger();
  };
  useEffect(() => {
    setBurger(getBurger(burgerName as BurgerTypes));
  }, [burgerName]);
  return (
    <Row className="w-full h-full" justify={"center"}>
      <Col className="space-y-8 py-8" span={22}>
        <Row gutter={16} justify={"center"}>
          <Col span={20}>
            <Row>
              <Col>
                <b>Name</b>
                <p>{burger?.getName()}</p>
              </Col>
            </Row>
            <Row>
              <Col>
                <b>Description</b>
                <p>{burger?.getDescription()}</p>
              </Col>
            </Row>
            <Row>
              <Col>
                <b>Ingredients</b>
                <p>{burger?.getIngredients().join(", ")}</p>
              </Col>
            </Row>
          </Col>
          <Col span={4}>
            <Row>
              <Col>
                <Row>
                  <Col>
                    {burger?.getImageArray().map((image) => (
                      <Row>
                        <Col>
                          <img
                            src={image}
                            alt="alt"
                            width={100}
                            height={"auto"}
                          />
                        </Col>
                      </Row>
                    ))}
                  </Col>
                </Row>
              </Col>
            </Row>
          </Col>
        </Row>
      </Col>
    </Row>
  );
};

export default BurgerDetails;

Introduction of Factory Method

Our design patterns guru, Emma understands the problem with the code and explains 2 key principles of Object Oriented design to Jack.

  • Identify the aspects that vary and separate them from what stays the same.

  • Dependency Inversion Principle: Depend upon abstractions. Do not depend upon concrete classes.

In the above code in List and Details page, burger construction code keeps changing every time a new burger is added or existing burger is removed. As the first principle says, encapsulate things that change, we create a method called factory that takes care of object instantiation and returns us concrete burger object.

factory.ts

import { CheeseBurger } from "../../burgers/CheeseBurger";
import { ChickenBurger } from "../../burgers/ChickenBurger";
import { VegBurger } from "../../burgers/VegBurger";

export enum BurgerTypes {
  cheese = "cheese",
  chicken = "chicken",
  veg = "veg",
}
/**
*A creator method is added which takes the type of burger to create 
and returns an instantiated object to the client code.
*/
export function creator(type: BurgerTypes) {
  switch (type) {
    case BurgerTypes.cheese:
      return new CheeseBurger();
    case BurgerTypes.chicken:
      return new ChickenBurger();
    case BurgerTypes.veg:
      return new VegBurger();
  }
}

After adding factory.ts, our List and Details pages look like below

BurgerList.tsx

import React from "react";
import Row from "antd/es/row";
import Col from "antd/es/col";
import { useNavigate } from "react-router";
import { Burger } from "../../burgers/burger";
import { BurgerTypes, creator } from "./factory";
import { enumToArray } from "../../utils/enumToArray";

const BurgerListCard: React.FC<{
  burger: Burger;
  onClick: () => void;
}> = ({ burger, onClick }) => {
  return (
    <Row justify={"center"} className="my-2 cursor-pointer" onClick={onClick}>
      <Col className="border-b border-b-gray-300 p-2" span={24}>
        <Row>
          <Col span={24}>
            <b>{burger.getName()}</b>
          </Col>
        </Row>
        <Row>
          <Col span={24}>
            <p className="text-gray-500">{burger.getDescription()}</p>
          </Col>
        </Row>
      </Col>
    </Row>
  );
};
const BurgersList = () => {
  const navigate = useNavigate();
  return (
    <Row className="w-full h-full overflow-y-scroll" justify={"center"}>
      <Col span={12}>
        {enumToArray(BurgerTypes).map((burgerType) => {
        /**
           * Creator method is called while mapping through all the burgers 
           * from the enum 
           * This code is indendent of burgers being added or removed, 
           * provided each burger is implemented using Burger inteface
           */
          const burger = creator(burgerType as BurgerTypes);
          return (
            <BurgerListCard
              burger={burger}
              onClick={() => navigate(`/patterns/${burger.getBurgerType()}`)}
            />
          );
        })}
      </Col>
    </Row>
  );
};

export default BurgersList;

BurgerDetails.tsx

import React, { useEffect } from "react";
import Row from "antd/es/row";
import Col from "antd/es/col";
import { BurgerTypes, creator } from "./factory";
import { useParams } from "react-router";
import { Burger } from "../../burgers/burger";
const BurgerDetails = () => {
  const { burgerName } = useParams();
  const [burger, setBurger] = React.useState<Burger>(
    creator(BurgerTypes.chicken)
  );

  useEffect(() => {
        /**
           * Creator method is called to create a burger object based 
           * on BurgerType received in param
           * This code is independent of burgers being added or removed, 
           * provided each burger is implemented using Burger inteface
           */
    setBurger(creator(burgerName as BurgerTypes));
  }, [burgerName]);
  return (
    <Row className="w-full h-full p-8" justify={"center"} align={"middle"}>
      <Col className="space-y-8" span={22}>
        <Row gutter={16} justify={"center"} align={"middle"}>
          <Col span={20}>
            <Row>
              <Col>
                <b>Name</b>
                <p>{burger.getName()}</p>
              </Col>
            </Row>
            <Row>
              <Col>
                <b>Description</b>
                <p>{burger.getDescription()}</p>
              </Col>
            </Row>
            <Row>
              <Col>
                <b>Ingredients</b>
                <p>{burger.getIngredients().join(", ")}</p>
              </Col>
            </Row>
          </Col>
          <Col span={4}>
            <Row>
              <Col>
                <Row>
                  <Col>
                    {burger.getImageArray().map((image) => (
                      <Row>
                        <Col>
                          <img
                            src={image}
                            alt="alt"
                            width={100}
                            height={"auto"}
                          />
                        </Col>
                      </Row>
                    ))}
                  </Col>
                </Row>
              </Col>
            </Row>
          </Col>
        </Row>
      </Col>
    </Row>
  );
};

export default BurgerDetails;

As you can observe in the above code, both BurgerList and BurgerDetails page is implemented according to burger interface. So, as long as these components get objects that implement burger interface, they work. Also, with the introduction of factory method, for every change, we add or remove burger types from factory.ts but we don't have to make any changes to list and details page. Version 2 of Bite Me Burgers can be seen below

Bite Me Burgers