Entity vs. Behavioral thinking in software design

Entity vs. Behavioral thinking in software design
Entity vs. Behavioral thinking in software design Hallo, Guten tag. I am Naren, a software engineer who worked on multiple technology stacks like Python, Node JS(extensively) and Spring Boot (for few features).

If an engineer is asked to build a software project from scratch, or improvise the existing project, how he/she designs the plan, i.e. whether she thinks of entities or behavior? Technically both approaches can solve the given problem. How to pick one, let us see! We at our office have many design debates every day, and this article is a side effect of one of our recent meetings. In this article, I discuss the separation of concerns and all possible ways to do that. We finally see what the benefits/drawbacks of using modules vs. classes are.

Python encourages modules for namespacing:

Modules + FunctionsClasses

moreover, Java encourages interfaces for code level agreements and classes for namespacing

Classes + Interfaces > Modules + Functions

In this article, I am targeting Python developers, who are developing software and want to explore various design concepts.

You can find the code samples here:

software-design-python

Problem Statement

Let us say we have a software project which deals with many subsystems (technical requirements) like:

  1. AWS S3
  2. DB
  3. Artifact (Item) — Some data structure
  4. Cache
  5. AWS Lamda

We have to implement the project in Python. If our project needs to use all of these to achieve some useful purpose, there are two ways of modeling the code.

  • Entity-based
  • Behavior-based

Thinking entities is thinking a system as an entity in software and treating it as a single point of contact for performing operations related to that entity.

Behavioral thinking is first trying to figure out all possible actions of software and grouping them under the type of behavior.

Design 1: Behavior-based (Pythonic but primitive)

After inspecting the above requirements, let us have a few files called get, put, and, delete which has the business logic for our projects. Since Python holds namespacing in modules, we can create modules for all of these functions.

pythonicBehavioral/
        get.py
        put.py
        delete.py
        main.py
        tests.py
        __init__.py

delete.py

# imports ...

def helper_for_delete(*args, **kwargs):
    pass

def delete_from_s3(*args, **kwargs):
    print("deleting key from s3")

def delete_from_db(*args, **kwargs):
    print("deleting a row from db")

get.py

# imports...

def read_from_s3(*args, **kwargs):
	print("reading the file from S3")

def read_from_db(*args, **kwargs):
	print("query the rows from db")

def decode_file(*args, **kwargs):
	print("working on decoding the artifact")

put.py

# imports ...

def write_to_s3(*args, **kwargs):
    print("writing file into s3")

def write_to_db(*args, **kwargs):
    print("inserting rows into db")

def encode_audio_file(*args, **kwargs):
    print("encoding the audio file")

def encode_text_file(*args, **kwargs):
    print("encoding the text file")

We defined functions in modules(namespaces) according to the behavior (outward/inward/some other). Now we can use them in our main program by importing them.

main.py

# some imports
import os

from get import read_from_s3
from delete import delete_from_s3
from put import encode_audio_file

def main(key):
    read_from_s3()
    # some logic
    delete_from_s3()
    # some logic
    encode_audio_file()

if __name__ == '__main__':
    main(os.environ.get("key"))

Here, we imported functions from the modules and used them to build our logic in the main program. This design is pretty good in terms of unit testing because everything is a function. To write tests, we can import all functions into our tests.py and write a TestCase and mocks**.** A simple and straight forward unit testing(skipping tests file in the above code samples for brevity).

Note: You can run the project with

$ python3 pythonicBehavioral/main.py

Using Python ≥ 3.7.2 here

Now let us convert our behavioral design into entity based. A random developer from our meeting saw above design and shouted, “if I need to add a new subsystem, I have to modify three files to add new behaviors.” Yes, that is the drawback of the above system. To add a new subsystem with a set of behaviors you should modify many modules because modules are the namespaces.

Drawbacks:

  • You cannot test a single system without importing tons of modules. For example: To test S3, you have to import functions from modules get, put, and delete. i.e., Subsystem testing is not intuitive.
  • Cannot leverage OOP design patterns (we can work technically but with boilerplate workarounds)

The behavior-based design is “start small.”

Design 2: Entity-based (Not so Pythonic but well structured)

For all who worked on Java/Spring Boot knows how designing a spring boot app enforces you to have three separate responsibilities.

  • Controller (Like Django view which handles requests)
  • Interface which defines behaviors that a controller can expect (No interfaces in Python, something like an abstract class)
  • Service which adheres to an interface and abstracts implementational details of behaviors

This design has the obvious benefit of separation of duties. Controller developers do not know how service is going to do magic behind. It is a perfect example of abstraction, one of the import OOP principles. If we visualize how we can break thoughts into entities instead of behaviors, it looks like below

In this picture, Handler 1 may not know what Artifact service is doing behind the scenes. Above design achieves the separation of concerns based on entities. It is precisely how spring boot MVC encourages. Sadly Python doesn’t have types and interfaces. So we need to use Abstract classes & abstract class methods for writing entity based programs in Python. Let us implement for S3, DB, and Artifact.

entityBased/
        services.py
        enums.py
        main.py
        tests.py
        __init__.py

Let us how the refactored project looks like:
rawenums.py

from enum import Enum # Standard library

class StorageType(Enum):
    S3 = "s3"
    DB = "db"


class ArtifactType(Enum):
    AUDIO = "audio"
    TEXT = "text"

main.py

# some other imports

import os

from services import StorageService, ArtifactService


def main(key):
    s3_service = StorageService.get_storage("s3") # or "db"
    audio_service = ArtifactService.get_artifact("audio")
    # Use methods on those objects as per program logic
    s3_service.read()
    audio_service.encode()
  

if __name__ == '__main__':
    main(os.environ.get("key"))

services.py

# some other imports

from abc import ABC, abstractmethod # Standard library
from enums import StorageType, ArtifactType


class Service(object):
    pass

class StorageService(Service, ABC):
    
    # Methods every child service should implement
    @abstractmethod
    def read(self):
        pass

    @abstractmethod
    def write(self):
        pass
    
    @staticmethod
    def __get(type):
        return {
            StorageType.S3.value: S3Service,
            StorageType.DB.value: DBService
        }.get(type)
    
    # Factory for services
    @classmethod
    def get_storage(cls, type):
        storage_class = cls.__get(type) # Get storage type
        storage_instance = storage_class()
        if not issubclass(storage_class, cls):
            raise Exception(cls, " ", "interface is not satisfied")
        return storage_instance
  
class S3Service(StorageService):
    def read(self):
        print("reading the file from S3")
    
    def write(self):
        print("writing the file into S3")
    
class DBService(StorageService):
    def read(self):
        print("query the rows from DB")
    
    def write(self):
        print("inserting rows into DB")
    
    
class ArtifactService(Service, ABC):
    # Methods every child service should implement
    @abstractmethod
    def decode(self):
        pass
  
    @abstractmethod
    def encode(self):
        pass
  
    @staticmethod
    def __get(type):
        return {
            ArtifactType.AUDIO.value: AudioArtifactService,
            ArtifactType.TEXT.value: TextArtifactService
        }.get(type)

    # Factory for services
    @classmethod
    def get_artifact(cls, type):
        artifact_class = cls.__get(type) # Get storage type
        artifact_instance = artifact_class()
    
        if not issubclass(artifact_class, cls):
            raise Exception(cls, " ", "interface is not satisfied")
        return artifact_instance

class AudioArtifactService(ArtifactService):
  def decode(self):
    print("decoding the audio file")
  
  def encode(self):
    print("encoding the audio file")

class TextArtifactService(ArtifactService):
  def decode(self):
    print("decoding the text file")
  
  def encode(self):
    print("encoding the text file")

some other imports

import os

from services import StorageService, ArtifactService


def main(key):
    s3_service = StorageService.get_storage("s3") # or "db"
    audio_service = ArtifactService.get_artifact("audio")
    # Use methods on those objects as per program logic
    s3_service.read()
    audio_service.encode()
  

if __name__ == '__main__':
    main(os.environ.get("key"))

In the above program:

  • We defined a class called Service which acts as Base class for all services
  • Then we have a StorageService which can be used by the main program to perform some business logic. However, we should maintain the abstraction by delegating implementation details to a particular storage type.
  • We turned StorageService into an abstract class(by inheriting ABC) to make sure everyone who inherits that class should implement read and write methods. We defined methods as abstract so that children must implement them.
  • We defined S3Service and DBService for S3 and DB logic respectively. In those classes, methods can make API calls to either AWS or Database server. We printed some statements to simulate work.
  • **@classmethod**acts as a factoryfor the instances. It takes the same class asthe first argument. We are returning the actual storage service based on type supplied. Besides, we are checking whether the requested instance is the right child of the parent abstract class. If we don’t then, the interface can break by passing an invalid instance of some random class.
  • We defined @staticmethod as a helper function to get the right storage type based on a request.
  • In the main program, we import StorageType and ArtifactType and request services for “s3” storage and “audio” artifact and do some processing. The good thing is we are not directly importing S3Service or AtifactService so that in the future, we can replace the functionality without affecting the main program or can switch to a new storage type/artifact on the fly.

$ python3 entityBased/main.py

Using Python ≥ 3.7.2 here

The obvious main benefits of this design are:

  • Separation of concerns by entities (Storage, Artifact, S3, DB, Audio, Text, etc). Only talk to them by a set of methods.
  • Try to implement interfaces in Python
  • Create code level services instead of functions
  • Able to test subsystems alone (TestStorage, TestS3, TestDB etc)
  • All OOP design patterns make sense

Drawbacks:

  • Testing now has an overhead of instantiating the objects instead of unit testing. If classes are big, you need to carry whole baggage of properties plus methods for a single unit test.
  • A stateless class(no properties only methods) like the above example is just a namespace bucket. Python already provides modules for that. Languages like Java push a developer to think namespaces via classes by enforcing the creation of atleast a single class in the program.
  • According to Fabian Ihl, head of technology at TradeByte, testing a stateful class(properties and methods that can alter a property) is always unpredictable in contrast to testing functions. More details like thread safety of state need to be tested which is an overkill.

I personally like the above design as it is neat and can even be broken into a single module per service class.

The entity-based design is “start big”

Design 3: Hybrid (Pythonic with improved modularity)

Now let us try to find a middle ground solution. After seeing previous designs, why can’t we have modules based on entities?

It means let us have a file for each entity(instead of class)

entityBasedBehavior/
        s3.py
        db.py
        audio.py
        text.py
        main.py
        tests.py
        __init__.py

This design is exactly a behavioral pythonic approach with some minor fixes. It only uses modules and functions and brings the benefits of behavioral approach(unit testing friendly) and eliminates some of its drawbacks like the testing subsystems.

Achtung! Exercise: The code for this approach is an exercise to the reader.

The hybrid design is “start small”

conclusion:

It all boils down to personal preferences and an extra thought while choosing an entity-based or behavior-based design for implementing Python software. Both the approaches are decent and do the job and, it is the developer’s option to go for either to “start small” or “start big.”

I hope you enjoyed my article. This article is an output of many of my recent learnings. Use the above design principles even though you are a Node.js developer or a Java Developer.

30s ad

The Python Bible™ | Everything You Need to Program in Python

The Ultimate Python Programming Tutorial

Python for Data Analysis and Visualization - 32 HD Hours !

Python Fundamentals

Hello Python - Python Programming for Beginners

Suggest:

Python Tutorials for Beginners - Learn Python Online

Learn Python in 12 Hours | Python Tutorial For Beginners

Complete Python Tutorial for Beginners (2019)

Python Tutorial for Beginners [Full Course] 2019

Python Programming Tutorial | Full Python Course for Beginners 2019

Introduction to Functional Programming in Python