Consent Preferences

Classes: What Were They Made For?

An introduction to classes in computer programming.

Classes: What Were They Made For?
This image was created using OpenAI's GPT-4.

I was 14 years old when I made my first attempts to learn computer programming (in C++ at that time). Sitting in front of the screen of my Windows XP PC, the hard part of programming seemed to be getting the computer to do what I wanted it to do. I simply assumed that all elements of a programming language's toolset are equally fundamental. Later I learned that programming languages like Python or C++ provide a Turing complete set of tools with only numbers, assignments, comparisons, loops, and input/output, meaning that any problem that can be solved computationally can be solved with this subset. Of course, it would be hair-raising to write larger programs using only this subset, but this ultimately implies that all the other tools are not needed by the machine, but are there for the human who is writing and reading the code.

There are almost infinite ways to write a computer program in any programming language, but we follow paradigms (such as object-oriented programming) or structure code according to certain rules because this allows us to build programs in a way that is easier for our human minds to grasp and understand. Thus, programming is often the art of structuring the abstract machinery of computer software as intuitively as possible for humans. We want to reduce the complexity of the program as much as possible, but complexity is meant from a human perspective. The computer doesn't care about this complexity as long as the code is correct and not algorithmically inefficient. In code obfuscation, this is even used when the code is reformulated in such a redundant way that it is intentionally difficult to read.

Classes are a powerful tool to reduce the complexity of computer programs. I recently came across some of my backups of Python projects on a 12-year-old external hard drive (I didn't use Git back then). I must have been studying object-oriented programming at the time, because the code of these projects is stuffed with all kinds of classes. I cannot explain what I had in mind when I implemented most of those classes. They were certainly not wrong in principle, but when I looked at those classes, I felt like I was listening to the Billie Eilish song "What Was I Made For". But what are classes made for?

One of my favorite books on computer programming, "C++ Primer" by Stanley B. Lippman, Josée Lajoie and Barbara E. Moo, an evergreen since 1986, introduces classes conceptually using the example of book sales data in a bookstore. What may sound boring is actually a very helpful introduction to understanding classes. Let's say we are writing inventory software for a bookstore. The essence of this software is, of course, to manage the inventory of books. A key concept in computer science is translating real-world concepts, such as a book, into abstract data types. An abstract data type is a set of objects and their own functions/methods: If we want to define an addition method for 2 books in our inventory software, it does not necessarily make sense to simply add them. We know this kind of different operations on different data types from the basics of Python: For example, when we "add" two strings in Python, we do not add the (binary) numeric values of the Unicode characters of the strings, but we concatenate the two strings. So why not define a separate abstract data type with special operations for books? It would only make sense for a bookstore manager to add books if they are the same books, i.e., books with the same ISBN. So the isbn is information that belongs to the abstract data type Book.

When we design the abstract data type book as a class Book, we should separate all properties and operations as much as possible to create well-organized code that is as understandable and maintainable as possible. This principle is known as decomposition. Decomposition involves breaking down a complex system or problem into smaller, more manageable pieces. In the context of the Book class, decomposition is demonstrated by breaking down the broad concept of a "book" into discrete attributes and behaviors. Attributes are the basic parts that define what a book is in this context: isbn, title, authors, selling_price, and stock_quantity (and maybe later we could add year of publication, publisher, edition, etc.). Each attribute represents a piece of data that is critical to defining the identity and state of a book. The behaviors of the Book class are its methods, such as add_to_stock, sell, update_selling_price,has_same_authors, and has_same_title. Each method addresses a specific aspect or action related to a book, such as selling copies or updating the sale price. By decomposing these actions into methods, the class becomes more organized, and each part of the code has a clear, focused responsibility.

class Book:
    def __init__(self, isbn: str, title: str, authors: list, selling_price: float, stock_quantity: int = 1):
        self.isbn = isbn
        self.title = title
        self.authors = authors
        self.selling_price = selling_price
        self.stock_quantity = stock_quantity

    def __eq__(self, other_book):
        print(f"\nChecking if two editions of '{self.title}' are the same book...")
        if not isinstance(other_book, Book):
            return NotImplemented
        return self.isbn == other_book.isbn

    def __str__(self):
        authors_str = ', '.join(self.authors)
        return f"\n{self.title} by {authors_str}, ISBN: {self.isbn}, Price: ${self.selling_price}, Quantity: {self.stock_quantity}"

    def __add__(self, other_book):
        if not isinstance(other_book, Book) or self.isbn != other_book.isbn:
            print("\nCan't add stocks of different books!")
            return NotImplemented
        total_stock = self.stock_quantity + other_book.stock_quantity
        return Book(self.isbn, self.title, self.authors, self.selling_price, total_stock)

    def add_to_stock(self, additional_quantity):
        if additional_quantity > 0:
            print(f"\nAdding stock for '{self.title}'...")
            self.stock_quantity += additional_quantity
        else:
            print("\nPlease enter a positive quantity to add.")

    def sell(self, sell_quantity):
        if sell_quantity <= self.stock_quantity:
            print(f"\nSelling {sell_quantity} copies of '{self.title}'...")
            self.stock_quantity -= sell_quantity
        else:
            print("\nNot enough stock to sell the requested quantity.")

    def update_selling_price(self, new_selling_price):
        if new_selling_price > 0:
            print(f"\nUpdating the selling price of '{self.title}'...")
            self.selling_price = new_selling_price
        else:
            print("\nPlease enter a valid price.")

    def has_same_authors(self, other_book):
        print(f"\nChecking if '{self.title}' and '{other_book.title}' have the same authors...")
        return set(self.authors) == set(other_book.authors)  # Compare authors regardless of order

    def has_same_title(self, other_book):
        return self.title == other_book.title

We can now apply our Book class by creating objects like book1 and book2.

book1 = Book(isbn='978-0321714114', title='C++ Primer (5th Edition)',
             authors=['Stanley Lippman', 'Josée Lajoie', 'Barbara Moo'], selling_price=35.99,
             stock_quantity=50)

book2 = Book(isbn='978-1732102217', title='A Philosophy of Software Design, 2nd Edition',
             authors=['John Ousterhout'],
             selling_price=22.95, stock_quantity=35)

print(book1)
print(book2)

book1.add_to_stock(10)
print(book1)

book2.sell(5)
print(book2)

book1.update_selling_price(44.99)
print(book1)

print(f"Same authors: {book1.has_same_authors(book2)}")

print(book1 + book1)

When we run this code, we get:

C++ Primer (5th Edition) by Stanley Lippman, Josée Lajoie, Barbara Moo, ISBN: 978-0321714114, Price: $35.99, Quantity: 50

A Philosophy of Software Design, 2nd Edition by John Ousterhout, ISBN: 978-1732102217, Price: $22.95, Quantity: 35

Adding stock for 'C++ Primer (5th Edition)'...

C++ Primer (5th Edition) by Stanley Lippman, Josée Lajoie, Barbara Moo, ISBN: 978-0321714114, Price: $35.99, Quantity: 60

Selling 5 copies of 'A Philosophy of Software Design, 2nd Edition'...

A Philosophy of Software Design, 2nd Edition by John Ousterhout, ISBN: 978-1732102217, Price: $22.95, Quantity: 30

Updating the selling price of 'C++ Primer (5th Edition)'...

C++ Primer (5th Edition) by Stanley Lippman, Josée Lajoie, Barbara Moo, ISBN: 978-0321714114, Price: $44.99, Quantity: 60

Checking if 'C++ Primer (5th Edition)' and 'A Philosophy of Software Design, 2nd Edition' have the same authors...
Same authors: False

C++ Primer (5th Edition) by Stanley Lippman, Josée Lajoie, Barbara Moo, ISBN: 978-0321714114, Price: $44.99, Quantity: 120

We have now created a class for our abstract data type Book. Classes are an elegant tool for encapsulating information and functionality in smaller subprograms, which can ultimately reduce the complexity of the overall program. Ideally, we don't need to worry later about the exact implementation of the operations inside the Book class. The class hides all unnecessary information from the rest of the program, which in turn reduces complexity - this is known as information hiding. The goal is to abstract the Bookclass as much as possible to make it easy to use in a larger program.

But what does abstraction actually mean? The Latin abstractus means "subtracted" (past participle passive of abs-trahere: "to subtract", "to take away"), and this is also what abstraction means in computer programming: we want to hide the exact details of the implementation and reveal only the necessary information. When we invoke an abstract data type, we interact with it through its Application Programming Interface (API), which is the part of the class that can be accessed from the outside. This should be kept as simple as possible to reduce the complexity of the overall computer program.

Finally, we want to keep our class as general as possible, so that we can use it to create more specialized inherited subclasses. We can design subclasses that are derived from our Book class. For example, a class for paperback or hardcover books, or e-books. According to the Liskov substitution principle, it must still be possible to use these classes instead of the parent Book class without changing the functionality of the program.

class eBook(Book):
    def __init__(self, isbn: str, title: str, authors: list, selling_price: float, file_format: str,
                 stock_quantity: int = 1):
        super().__init__(isbn, title, authors, selling_price, stock_quantity)
        self.file_format = file_format

    def __str__(self):
        book_str = super().__str__()
        return f"{book_str}, Format: {self.file_format}, ebook"

    def update_file_format(self, new_format):
        print(f"\nUpdating the file format of '{self.title}' to {new_format}...")
        self.file_format = new_format

In his 2018 book "A Philosophy of Software Design", Stanford computer scientist John Ousterhout proposes the idea of "deep classes". A deep class provides as much functionality as possible (and reasonable) through the simplest possible API, without unnecessary internal details to reduce complexity. In contrast, "shallow classes" provide access to less complex classes through an overloaded API. So when we implement classes, we should never forget what they are for: to organize our programs by hiding information, simplifying interaction, and generalizing in a way that is easy for us humans to understand. There may be no perfect implementation, but when we design and revise a class, perhaps we should always remember to ask ourselves for what our class was made for:

"Something I'm not, but something I can be
Something I wait for
Something I'm made for
Something I'm made for"

(Billie Eilish, What Was I Made For?, 2023)

Do Not Sell or Share My Personal information