avatarMohamed Amine HAINE

Summary

The article advocates replacing switch-case statements with JavaScript objects to adhere to the Open/Closed Principle, making code more maintainable and scalable.

Abstract

The article titled "Stop Using Switch-Case and Use This Instead to Make Your Code Cleaner" argues that while switch-case statements may initially seem clean, they violate the Open/Closed Principle of SOLID design by requiring code modifications to extend functionality. The author suggests using JavaScript objects to map functions to types, which allows for the addition of new types without altering existing code logic. This approach is demonstrated through a refactoring example where new shapes can be added to a shape area calculation program by extending the object map without changing the core function. The article concludes by emphasizing the benefits of this method in reducing the risk of introducing bugs due to fewer modifications in the codebase.

Opinions

  • The author believes that switch-case statements lead to code that is not easily maintainable or scalable due to the need for modifications when extending functionality.
  • Adhering to the SOLID principles, particularly the Open/Closed Principle, is emphasized as crucial for writing robust and maintainable code.
  • The author promotes the use of JavaScript objects over switch-case as a means to facilitate code extension without modifying existing logic, thus minimizing the potential for new bugs.
  • The article suggests that the proposed object-based approach is cleaner, more extensible, and aligns better with good software engineering practices.
  • By following the advice in the article, the author implies that developers can create more reliable and adaptable software with less effort in the long term.
  • The author invites readers to explore further techniques for improving code quality and encourages the use of AI services like ZAI.chat for cost-effective coding assistance.

Stop Using Switch-Case and Use This Instead to Make Your Code Cleaner

Photo by Jonathan Farber on Unsplash

As coders, our job is to write a code that works. It’s fine, but it’s not enough.

As software engineers, our job is to write a code that works, maintainable and scalable.

For who knows, in SOLID principles, “O” means open to extend but closed to modification.

Sometimes, modifying code can be really tricky and demands a little quiet attention to not break it.

Extending/adding code, it’s easier than modifying code. You don’t really have to deal with the old code. If you don’t modify the code, you probably not alter its work. That means, less or no bugs.

When you use Switch-Case, generally you make your code open to modification, and it violates the “O” principle of SOLID.

Why ? Let’s see.

Using Switch-Case

Imagine we have an application that calculate an area of shape. The shape can be either a rectangle or a circle.

enum ShapeType {
  Circle = "circle",
  Rectangle = "rectangle"
}

type ShapeBase<T extends ShapeType> = { type: T };

type Circle = ShapeBase<ShapeType.Circle> & { radius: number };
type Rectangle = ShapeBase<ShapeType.Rectangle> & {
  width: number;
  height: number;
};

type Shape = Circle | Rectangle;

const getCircleShapeArea = (circle: Circle) => Math.PI * circle.radius ** 2;

const getRectangleShapeArea = (rectangle: Rectangle) =>
  rectangle.width * rectangle.height;

const getShapeArea = (shape: Shape): number => {
  switch (shape.type) {
    case ShapeType.Circle:
      return getCircleShapeArea(shape);
    case ShapeType.Rectangle:
      return getRectangleShapeArea(shape);
    default:
      return shape as never;
  }
};

const circle: Circle = { type: ShapeType.Circle, radius: 10 };
const rectangle: Rectangle = { type: ShapeType.Rectangle, width: 10, height: 5 };

const shapes: Shape[] = [circle, rectangle];

console.log(shapes.map(getShapeArea)); 

In the code above,

  • We’ve made sure that each shape is created by the ShapeBase type, has the good ShapeType affected to it and has the specific attributes.
  • We’ve created getCircleShapeArea and getRectangleShapeArea functions which calculate each shape area.
  • We’ve created getShapeArea function which don’t care about the shape type and calculate the area.
  • We’ve created a list of shapes.
  • We’ve calculated the area of each shape in the list.

Here our code works well, and it seems very clean. Of course, it is. But it’s not very easy to maintain. Why ? Because, when we want to add another shape such the triangle, we have to change the code inside the getShapeArea function. This will force us to modify this function logic. The “O” principle is broken.

How can we make the code better, to respect the “O” principle ? I mean, how can we add the triangle shape without modifying the getShapeArea function, but only adding code outside ?

Let’s see this.

Using JavaScript Object

Let’s make some refactoring to use an object instead of the Switch-Case.

enum ShapeType {
  Circle = "circle",
  Rectangle = "rectangle",
}

type ShapeBase<T extends ShapeType> = { type: T };

type Circle = ShapeBase<ShapeType.Circle> & { radius: number };
type Rectangle = ShapeBase<ShapeType.Rectangle> & {
  width: number;
  height: number;
};

type Shape = Circle | Rectangle;

type GetShapeArea<T extends Shape> = (shape: T) => number;

const getCircleShapeArea: GetShapeArea<Circle> = (circle: Circle) =>
  Math.PI * circle.radius ** 2;

const getRectangleShapeArea: GetShapeArea<Rectangle> = (rectangle: Rectangle) =>
  rectangle.width * rectangle.height;

const getShapeAreaByShapeType: {
  [K in ShapeType]: GetShapeArea<Extract<Shape, { type: K }>>;
} = {
  [ShapeType.Circle]: getCircleShapeArea,
  [ShapeType.Rectangle]: getRectangleShapeArea,
};

const getShapeArea: GetShapeArea<Shape> = (shape: Shape): number => {
  const _getShapeArea = getShapeAreaByShapeType[shape.type] as GetShapeArea<
    Shape
  >;
  return _getShapeArea(shape);
};

const circle: Circle = { type: ShapeType.Circle, radius: 10 };
const rectangle: Rectangle = {
  type: ShapeType.Rectangle,
  width: 10,
  height: 5
};

const shapes: Shape[] = [circle, rectangle];

console.log(shapes.map(getShapeArea)); // Output: [314.1592653589793, 50]

In the code above, we replace the switch case by an object that map the good getShapeArea function with the good ShapeType.

Imagine we want to add another shape like ellipse. We only had to do is :

  • Create a new ellipse shape type.
  • Create the ellipse shape.
  • Created the getEllipseShapeArea function.
  • Add the function to getShapeAreaByShapeType const.

And that all. No logic code has been modified, only type and constants.

enum ShapeType {
  Circle = "circle",
  Rectangle = "rectangle",
  Ellipse = "ellipse"
}

type ShapeBase<T extends ShapeType> = { type: T };

type Circle = ShapeBase<ShapeType.Circle> & { radius: number };
type Rectangle = ShapeBase<ShapeType.Rectangle> & {
  width: number;
  height: number;
};
type Ellipse = ShapeBase<ShapeType.Ellipse> & {
  semiMajorAxis: number;
  semiMinorAxis: number;
};

type Shape = Circle | Rectangle | Ellipse;

type GetShapeArea<T extends Shape> = (shape: T) => number;

const getCircleShapeArea: GetShapeArea<Circle> = (circle: Circle) =>
  Math.PI * circle.radius ** 2;

const getRectangleShapeArea: GetShapeArea<Rectangle> = (rectangle: Rectangle) =>
  rectangle.width * rectangle.height;

const getEllipseShapeArea: GetShapeArea<Ellipse> = (ellipse: Ellipse) =>
  Math.PI * ellipse.semiMajorAxis * ellipse.semiMinorAxis;

const getShapeAreaByShapeType: {
  [K in ShapeType]: GetShapeArea<Extract<Shape, { type: K }>>;
} = {
  [ShapeType.Circle]: getCircleShapeArea,
  [ShapeType.Rectangle]: getRectangleShapeArea,
  [ShapeType.Ellipse]: getEllipseShapeArea
};

const getShapeArea: GetShapeArea<Shape> = (shape: Shape): number => {
  const _getShapeArea = getShapeAreaByShapeType[shape.type] as GetShapeArea<
    Shape
  >;
  return _getShapeArea(shape);
};

const circle: Circle = { type: ShapeType.Circle, radius: 10 };
const rectangle: Rectangle = {
  type: ShapeType.Rectangle,
  width: 10,
  height: 5
};
const ellipse: Ellipse = {
  type: ShapeType.Ellipse,
  semiMajorAxis: 10,
  semiMinorAxis: 5
};

const shapes: Shape[] = [circle, rectangle, ellipse];

console.log(shapes.map(getShapeArea)); // Output: [314.1592653589793, 50, 157.07963267948966]

Conclusion

If you avoid to use switch case, you avoid having to modify the code logic later, you avoid the risk to have the logic bugs.

If you are interested in making the code cleaner, feel free to check my Stop Writing Multi-Branch Code with Nested if-else’s and Try This Instead.

And you! What is your technique to avoid Switch-Case code?

Thank you for reading until the end. Please consider following the writer and this publication. Visit Stackademic to find out more about how we are democratizing free programming education around the world.

Typescript
JavaScript
Clean Code
Software Engineering
Software Development
Recommended from ReadMedium