When do you need to define your own data types?

All programmers are familiar with the common data types found in many languages like integers, strings, floats and so on. After we learn about them, we go about our work, using those data types in our code. But have you ever thought about defining your own data types?

An Example

Let us say you are building a shopping cart which is going to hold some items. These items can be in multiple quantities, eg: we might have 2 apples and 10 bananas. These quantities can obviously not be negative, so we can never have -5 oranges in the cart. We also cannot have 0 quantity of an item in the cart, so quantity has to start from 1. Additionally, lets say that there is an upper limit of 10 per item, so the quantity cannot be more than 10 either.

How would we code this? The usual way is to use an integer data type here. After all, quantity is a number and the integer type is provided in-built in all languages.

So, our code might look something like this (taking a Python example)

class CartItem:
    def __init__(self, item_name):
        self.item_name = item_name
        self.quantity = 1

    def increment_quantity(self):
        if self.quantity + 1 <= 10:
            self.quantity = self.quantity + 1

    def decrement_quantity(self):
        if self.quantity - 1 >= 1:
            self.quantity = self.quantity - 1

    def set_quantity(self, quantity):
        if 1 <= quantity <= 10:
            self.quantity = quantity

Notice how all the methods need to add checks to ensure the quantity value does not go outside the defined bounds. If we are modifying this quantity anywhere else in the code, we need to add the checks there too. If we forget, or we put the checks wrong, we have a bug. Although the checks are very simple in this example here, in production code you will find hundreds of such checks all over the code making the code very complicated and bug-prone.

How can we do this in a better way?

Defining a BoundedInt data type

The problem here is that an integer data type allows a wide range of numbers, whereas in our app we want the range of numbers to be limited. So there is a mismatch between the type we are using and the type we require.

Unfortunately, most languages do not have in-built type where the value of the number can be restricted. So what? We can always write our own!

In OO languages, a custom type is nothing but a class. Here is the Python version

(PS: In Python specifically, we can also solve this problem using Descriptors, but that is a topic for another day)

class BoundedInt:
    def __init__(self, lower_bound, upper_bound, initial_value):
        self.lower_bound = lower_bound
        self.upper_bound = upper_bound
        if initial_value < self.lower_bound:
            self.value = self.lower_bound
        elif initial_value > self.upper_bound:
            self.value = self.upper_bound
        else:
            self.value = initial_value

    def __add__(self, other):
        final_value = self.value + other
        if final_value > self.upper_bound:
            final_value = self.upper_bound
        return BoundedInt(self.lower_bound, self.upper_bound, final_value)

    def __sub__(self, other):
        final_value = self.value - other
        if final_value < self.lower_bound:
            final_value = self.lower_bound
        return BoundedInt(self.lower_bound, self.upper_bound, final_value)

This data type can store a value with bounds. It supports adding and subtracting (you can implement more operations if you want) and ensures that the value is clipped at the boundaries.

With this class in place, our code now becomes like this

class CartItem:
    def __init__(self, item_name):
        self.item_name = item_name
        self.quantity = BoundedInt(1, 10, 1)

    def increment_quantity(self):
        self.quantity = self.quantity + 1

    def decrement_quantity(self):
        self.quantity = self.quantity - 1

    def set_quantity(self, quantity):
        self.quantity = BoundedInt(1, 10, quantity)

Isn't that simple and beautiful? No more checks anywhere. We just define quantity is a BoundedInt(1, 10, 1) and then we can forget about the boundaries everywhere. The custom data type abstracts away the boundary check. As a bonus, we can use this data type in other places in the code where we may need a bounded number with different bounds.

Summary

Many programmers limit themselves to the core data types provided by a language. The code is then sprinkled with conditions all over the place to enforce various constraints. This leads to ugly, difficult to read, and bug prone code.

While there are good uses for if/else conditional code (eg: in the BoundedInt class), in many situations the conditionals are unnecessary (eg: in CartItem) and excessive use of conditionals is a code smell. Consider creating your own types that enforce constraints on the data and remove those checks from the business logic.