Entity vs. Behavioral thinking in software design (Pythonic vs. other philosophies)

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:

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

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.

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:

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 factory for the instances. It takes the same class as the 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”

Please find the code examples here:

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.

Tschüss!