Clearly write code that increases conditional branching in multiplication

Introduction

Occasionally, when writing code, conditional branching may occur multiplyively (combinatively). For example

--The text of the email sent may or may not be sent depending on the user type such as new user or existing user. There are multiple types of text. --Processing branches depending on the acquisition result of the external service API. Furthermore, the correspondence changes depending on the state of the internal database.

etc.

The theme of this article is how to be resilient to changes without compromising visibility in these situations. The code example is shown in Python, but the idea is not dependent on the programming language, and if the implementation is also an object-oriented language, I think that the same implementation can be done.

Example) Movie pricing

Here, I would like to take a movie pricing as an example. For example, there are general adults, students, and seniors as purchasers, and the price changes according to the time of day (daytime, late show).

Daytime Late show
General 1800 yen 1300 yen
student 1,500 yen 1,500 yen
Senior 1100 yen 1100 yen

Let's consider that there is a discount rate rule for the standard rate (1800 yen), and the application of that rule is expressed in code.

Discount rate rule from the standard amount

Weekdays Weekdays (late show)
General +-0 Yen -500 Yen
student -300 yen -500 yen
Senior -700 yen -700 yen

When written procedurally

In such a case, if you simply write with procedural conditional branching, it will be painful as follows.

from datetime import datetime
import enum


#Viewer classification
class ViewerType(enum.Enum):
  ADULT = "adult"
  STUDENT = "student"
  SENIOR = "senior"


#Return the viewing fee based on the viewer classification, movie start time, and standard fee
def charge(viewer_type: ViewerType, movie_start_at: datetime  base_charge: int) -> int:
  #Late show after 20:00
  if movie_start_at.hour < 20:
    if viewer_type == ViewerType.ADULT:
      return base_charge
    elif viewer_type == ViewerType.STUDENT:
      return base_charge - 300
    else:
      return base_charge - 700

  if viewer_type == ViewerType.ADULT or viewer_type == ViewerType.STUDENT:
    return base_charge - 500
  else:
    return base_charge - 700

Even if you look at it at a glance, the overall outlook is poor, and I think it is difficult to tell if it is properly implemented. Also, when the types of purchasers such as registered members increase or the rules for senior charges change, it is difficult to know where to add processing.

Think of conditional branching as a context

In such a case, you can get a better view by considering ** the event (data that is the source of conditional branching) ** that affects the process you want to do as ** context **.

In this case, the ** show start time **, which is the criterion for whether it is a late show, is expressed in code as the context ** for determining the final ** price.

from datetime import datetime
import dataclasses

@dataclasses.dataclass
class MovieStartAtContext:
  movie_start_at: datetime

  def is_late_show(self) -> bool:
    return self.movie_start_at.hour >= 20

For the time being, I was able to express the context of ** whether the show start time is a late show **, but there is no point in expanding this into the procedural code above.

The important thing is to make this ** decision so that the code can be executed **.

from datetime import datetime
import dataclasses

@dataclasses.dataclass
class MovieStartAtContext:
  movie_start_at: datetime

  def is_late_show(self) -> bool:
    return self.movie_start_at.hour >= 20

  #Price calculation by viewer
  #
  #The method called by the viewer changes depending on whether it is a late show or not.
  #
  # -For late shows: late_show_charge()
  # -During the day:      normal_charge()
  def charge_for(self, viewer: Viewer, base_charge: int) -> int:
    if self.is_late_show():
      return viewer.late_show_charge(base_charge)

    return viewer.normal_charge(base_charge)

By doing this, it is sufficient to define the calculation methods for late show and regular fee for each type of viewer (it is the responsibility of the show time context side to call the appropriate method).

Implementation of fee calculation logic for each viewer

The implementation of the price calculation for each viewer is as follows.

class Viewer:
  def normal_charge(self, base_charge: int) -> int:
    pass

  def late_show_charge(self, base_charge: int) -> int:
    pass


class AdultViewer(Viewer):
  def normal_charge(self, base_charge: int) -> int:
    return base_charge

  def late_show_charge(self, base_charge: int) -> int:
    return base_charge - 500


class StudentViewer(Viewer):
  def normal_charge(self, base_charge: int) -> int:
    return base_charge - 300

  def late_show_charge(self, base_charge: int) -> int:
    return base_charge - 500


class SeniorViewer(Viewer):
  def normal_charge(self, base_charge: int) -> int:
    return base_charge - 700

  def late_show_charge(self, base_charge: int) -> int:
    return base_charge - 700

You can see that the discount rate rule table is expressed in the code almost as it is.

Discount rate rule from the standard amount

Weekdays Weekdays (late show)
General +-0 Yen -500 Yen
student -300 yen -500 yen
Senior -700 yen -700 yen

Code integration

Finally, when you integrate the code you've defined so far, your original charge () function looks like this:

class ViewerFactory:
  viewer_mapping = {
    ViewerType.ADULT: AdultViewer(),
    ViewerType.STUDENT: StudentViewer(),
    ViewerType.SENIOR: SeniorViewer()
  }

  @classmethod
  def create(cls, viewer_type: ViewerType) -> Viewer:
    return cls.viewer_mapping[viewer_type]


def charge(viewer_type: ViewerType, movie_start_at: datetime, base_charge: int) -> int:
  context = MovieStartAtContext(movie_start_at)

  viewer = ViewerFactory.create(viewer_type)

  return context.charge_for(viewer, base_charge)

By doing this, when the number of member registrants mentioned in the example increases, a new viewer class can be defined, and even if a new charge category such as a holiday charge increases, I think that you can implement it without getting lost in the mounting location.

Summary

When conditional branching occurs multiplyively (combinatively)

--Try to grasp the event (data) that is the cause of the branch as a context --Represent the context as an object and aggregate conditional judgments there --Furthermore, make it possible to execute processing based on aggregated condition judgment

Hopefully you can make your code cleaner and easier to extend. I hope it will be helpful for implementation.

Recommended Posts

Clearly write code that increases conditional branching in multiplication
Write standard input in code
Write Spigot in VS Code
One liner that outputs multiplication tables in Python
I struggled with conditional branching in Django's Templates.