Empower Your Python Projects: Harnessing Efficiency with the Balking Pattern

Photo by Tasso Mitsarakis: https://www.pexels.com/photo/blue-wall-with-a-barred-window-7996769/

Introduction

The Balking Pattern might not be widely known, but it plays a crucial role in preventing certain actions on objects based on their state. For instance, attempting to read the contents of an unopened file should not be allowed.

Advantages and disadvantages of the Balking Pattern

The Balking Pattern has several advantages:

  1. Preventing Unnecessary Operations: It stops unnecessary operations, particularly for time- or resource-intensive tasks, leading to improved performance.
  2. Enhanced Readability: Implementing explicit error checking, such as returning a Result struct in Rust, enhances code readability and robustness.
  3. Error Avoidance: Through explicit error checking, potential errors can be avoided.

The disadvantages are:

  1. Increased Complexity: The pattern may introduce extra complexity due to additional state-checking in objects.
  2. Developer Discipline: Proper state checking requires discipline on the part of developers.

Implementation in Python

In this example we will implement a simple MemoryFile, which is basically a string in our implementation. This class can be in two states, either open or closed. Since there are only two states in our example, I will represent them using a boolean. In case there is more than one state, you can use strings (for clarity) or numbers (for brevity) instead. Also, because this is a file-like class, I added some utility methods so it can be used in a contextmanager.

We will start by importing the following:

from typing import Self

This is something we will need in our utility methods.

Next we will start defining the class and the constructor:

class MemoryFile:
    _name: str = None
    _isOpen: bool = None

    def __init__(self, name: str):
        self._name = name
        self._isOpen = False

As you can see, the class has two fields:

  1. The name, which is a string
  2. And the state. Since the file can be either open or closed this is a boolean

Next we implement the open() and close() methods, since they are quite similar:

    def open(self) -> None:
        if not self._isOpen:
            self._isOpen = True
        else:
            raise ValueError('File must be closed before it can be opened')

    def close(self):
        if self._isOpen:
            self._isOpen = False
        else:
            raise ValueError('File must be opened before it can be closed')

In both methods, we check the state, and if the state does not allow us to perform the operation, we raise an exception.

The read() method is very similar:

    def read(self) -> str:
        if self._isOpen:
            return self._name
        else:
            raise ValueError('File must be opened before it can be read')

Next we have the two utility methods, for working with the contextmanager. First there is the __enter__() method, called when we use the with statement. In our case, all that this does, is return the object.

The __exit__() method is a bit more interesting. This method is called at the end of the with block, and is usually used to perform some housekeeping tasks. In our example, the method closes the file if it is open.

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()

The complete code of our MemoryFile class looks like this:

class MemoryFile:
    _name: str = None
    _isOpen: bool = None

    def __init__(self, name: str):
        self._name = name
        self._isOpen = False

    def open(self) -> None:
        if not self._isOpen:
            self._isOpen = True
        else:
            raise ValueError('File must be closed before it can be opened')

    def close(self):
        if self._isOpen:
            self._isOpen = False
        else:
            raise ValueError('File must be opened before it can be closed')

    def read(self) -> str:
        if self._isOpen:
            return self._name
        else:
            raise ValueError('File must be opened before it can be read')

    def __enter__(self) -> Self:
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):

        self.close()

Time to test

Now we can test our setup:

if __name__ == '__main__':
    try:
        with MemoryFile('readme.txt') as f:
            f.open()
            print(f.read())
    except ValueError as err:
        print(f"Error {err}")

Line by line:

  1. We create a MemoryFile in a contextmanager using the with statement, and call it f.
  2. Next we open it, and read from it. There is no need to close it, since that is taken care of by the contextmanager.
  3. In case something goes wrong, we wrap the operation in a try-except block.

Conclusion

As you can see, preventing or allowing actions based on the state of an object is something that requires some planning, and some idea of state-transitions in our domain. In our example we’re only dealing with two states, but you can imagine more complex systems. However, the implementation of this pattern usually is done on small objects, embedded in larger systems, thereby hiding some complexity.

Adequate error handling as you can see in our fastly simplified example remains very important. Also implementing this pattern requires some discipline, on both sides that is on the side of the developer of the API, as well as the user of this API.

Leave a Reply

Your email address will not be published. Required fields are marked *