0

Note This is a bit lengthy to have give a better understanding of the situation and to get some context. You might spot other architectural flaws (it's from an ancient application). I appreciate any feedback on this, but to stay on topic, I will mark the core question at the end in bold!


Let's assume that I have an application that uses a factory to create isolated database transactions (in my case an SQLAlchemy "Session"), but also uses other "application-global" structures like an event-bus and a metric-collector.

My current architecture feels wrong and I feel like there's a better way to do it. My architecture is a result from trying to avoiding the "Singleton" pattern because it feels to me like "hidden global state". But in a sense I want a certain "globalness". So maybe I should go for a singleton? What I don't like about the singleton is that the resulting code becomes a bit more "magical". For example, consider these two functions:

def store_in_db_no_singleton(dbconnection, item):
    dbconnection.add(item)

def store_in_db_singleton(item):
    db_connection = singleton.get_instance()
    dbconnection.add(item)

The first example makes code which is calling that function more explicit. And is makes it easier in unit-tests to inject alternative db-connection implementations.

But I digress.

For the above reason, my application now looks like the following:

class Application:
    def __init__(self, sessionmaker, eventbus, metrics):
        self.sessionmaker = sessionmaker
        self.eventbus = eventbus
        self.metrics = metrics
        self.component_1 = Component1(sessionmaker, eventbus, metrics)
        self.component_2 = Component2(sessionmaker, eventbus, metrics)

sessionmaker is responsible to generate new isolated DB transactions, Component1 and Component2 are two distinct parts of the application, each with their own problem-domain.

The way DB-transactions are handled here makes this hard to unit-test. Because the transactions are scoped to the methods inside component_1 and component_2. For example, assume component_1 has an add_user method, the implementation would look something like this:

def add_user(username):
    with self.sessionmaker() as session:
        entity = User(username)
        session.add(entity)
        session.commit()

In the unit-tests I can provide a session-maker which does a flush and rollback whenever a "commit" would be issued. This has the advantage that the SQL queries are actually sent to the test-db (which is intentional in this case) but the "commit" would never be sent.

However, because the session is scoped to the low-level add_user method, a unit test would never "see" the stored data and cannot confirm data has successfully been written. For example:

from sqlalchemy.orm.session import Session, sessionmaker
from sqlalchemy import create_engine

class RollbackSession(Session):

    def commit(self) -> None:
        super().flush()
        return super().rollback()

def test_add_user():
    engine = create_engine(test_db_dsn)
    my_session_maker = sessionmaker(bind=engine, class_=RollbackSession)
    app = Application(my_session_maker, None, None)
    app.component_1.add_user("john.doe")
    # Session has now been rolled back and it's impossible to check the DB-data

While trying to improve this I came back to the Application object (duplicated here for reference):

class Application:
    def __init__(self, sessionmaker, eventbus, metrics):
        self.sessionmaker = sessionmaker
        self.eventbus = eventbus
        self.metrics = metrics
        self.component_1 = Component1(sessionmaker, eventbus, metrics)
        self.component_2 = Component2(sessionmaker, eventbus, metrics)

I was about to change it into the following:

class Application:
    def __init__(self, sessionmaker, eventbus, metrics):
        self.sessionmaker = sessionmaker
        self.eventbus = eventbus
        self.metrics = metrics
        self.component_1 = Component1(self)
        self.component_2 = Component2(self)

But that triggered my code-smell trunk. This would mean that the two componented "reach up" to the "Application" object. And I don't know if this is acceptable practice. Maybe I am unaware of a well-known design-pattern for this?

So the core question is: Is there a cleaner way to make sessionmaker, eventbus and metrics available to the underlying components? Or is it okay like it is?

1 Answer 1

1

There are some problems with your current design.

First of all, the RollbackSession class violates the Liskov Substitution Principle because it breaks the behaviour of its parent class (the Session) class. As a consequence, your tests fail because the program does not work as expected when replacing Session objects with their subtype instances.

Then, you should put all database related operations into a separate layer in your application, which I usually call the persistence layer. I am not a Python expert. From my knowledge with other programming languages, if you want to test the persistence layer implementation with sessions, there are at least two approaches:

  • Have an interface for the session then two implementations: one for the real database engine and one for in-memory database for testing
  • Use a separated database for testing this layer and then reset the whole database after/before every run of the test suite.

For testing the rest of the application, there should be no interaction with the database as we will mock the persistence layer in those tests.

Last but not least, using singleton is absolutely acceptable. The importance here is to limit the places you use singletons. In many frameworks, the only place that singleton can be used is service factories, where we do the dependency injection. In your classes, it's better to have no singleton nor static method calls.

5
  • Isn't the LSP only about the function signature though? If it would forbid any changes in the implementation of the overridden method, then overriding wouldn't make any sense, no? Because you normally override the method for the exact purpose of altered implementation. In my example I keep the same signature. So I do not expect this to be a violation of the LSP.
    – exhuma
    Aug 24, 2021 at 16:14
  • Method signature requirement is just one of condition that LSP imposes. This principle says "if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program", the important part is desirable properties. In your example, the client of Session objects want the changes to be persisted when the session is commited, which is broken with the RollbackSession subtype. We can change the behaviour of the parent type as long as we don't break the expectation.
    – Hieu Le
    Aug 24, 2021 at 16:31
  • That makes perfect sense. And rephrasing this like you did, made me realise the crux of the problem: If I want to test data-persistence, but then change exactly that behaviour in my unit-test, then what I am doing right now is very wrong on a fundamental level. Thank you!
    – exhuma
    Aug 24, 2021 at 18:32
  • You're welcome. I've edited the answer, replacing change the behaviour with break the behaviour to make less confusion.
    – Hieu Le
    Aug 25, 2021 at 1:50
  • 1
    Let’s say you have a button class and a coloured button subclass. If the spec for the draw method of the button class is “draws a black and white button” then the coloured button breaks this. If the spec says “draws a button using the correct colours”, then a coloured button drawing a green button looks different but does exactly what it is supposed to do.
    – gnasher729
    Aug 25, 2021 at 18:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.