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?
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)
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 <https://docs.python.org/3/howto/descriptor.html)>_, but that is a topic for another day)
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
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.
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.