TransWikia.com

Remove all side-effects from business logic

Software Engineering Asked by Olle Härstedt on November 8, 2021

I’m looking for feedback for a design pattern that aims to remove all side-effects from business logic. I’m using PHP but the pattern can be applied to any OOP language. The point is to enforce pure business logic from the framework, by not injecting any dependencies that have side-effects, e.g. database connections, curl, file, output buffer, etc, and replace them by command objects (see command design pattern) in a pipeline.

The motivation behind removing side-effects is to make testing easier by removing the need for mocking. In general, you can mock pure methods, but you don’t have to mock them the same why you have to mock, say, a database connection.

Here’s an example of a controller action that reverts the admin status of a user:

function updateUser(int $userId, SideEffectFactoryInterface $make)
{
    return [
        $make->query('SELECT * FROM user WHERE id = ' . $userId),
        function ($user) use ($make) {
            $reverted = $user->is_admin ? 0 : 1;
            return [
                null,
                $make->query(
                    sprintf(
                        'UPDATE user SET is_admin = %d WHERE id = %d',
                        $reverted,
                        $user->id
                    )
                )
            ];
        },
        $make->output('Updated user')
    ];
}

As you can see, all side-effects are created as command objects from the SideEffectFactory.

When running the list of callables, there will be a dependency resolver so that query objects get their database connection, file readers their file IO methods, etc.

Of course the question is if mocks will be easier, and if it’s worth it since readability might suffer.

2 Answers

Your aim to avoid having to mock databases and similar external services is good. But the way you approached it is, in my opinion, flawed.

While you have hidden the technicalities of accessing the database behind the SideEffectFactoryInterface, your updateUser function still contains the knowledge that a User object is stored in a database and how it is stored there. This mixes low-level knowledge about storage with high-level knowledge that updating a User involves inverting its admin status.

A more common way to make the business logic testable without mocking is to make the business logic work only with objects in memory and to make it completely unaware of how those objects got there or what happens to them after the business logic has done its thing.

Translating your code to that format would yield a set of functions like this:

function updateUserController(int $userId)
{
  // This function integrates the business logic with the supporting
  // modules for interfacing with external systems.

  // $userRepository knows about databases and how User objects are stored there
  $user = $userRepository->get($userId); 

  updateUser($user);

  $userRepository->save($user);
}

function updateUser(User $user)
{
  // This function contains the business logic
  $user->is_admin = $user->is_admin ? 0 : 1;
}

The updateUser function can be tested completely without mocks. The updateUserController function could be tested with a mock for the $userRepository, but the function should be straightforward enough that that is not needed and that the function is only tested when integrating with a real database.

Answered by Bart van Ingen Schenau on November 8, 2021

if it's worth it

Not in the way you're writing it, IMO.

You're essentially writing a compiler. A compiler converts high level business logic into low level instructions to be run by an interpreter. A compiler can be written as a pure function, but interpreter is necessarily where all the side effect actually happens.

All is good, but the way you currently structure your compiler have two flaws:

  1. You allowed the compiled code to return a function object. This means that you cannot easily test the return value of the compilation process as that function object is a blackbox. If your compiled code only contains only plain old data, then you can write tests that asserts that updateUser(5) == $expectedCompiledCode, but because you are returning a callable, you can't really do that.

  2. Your compiler takes a side effect factory. I don't know what this class does, but this looks like an injection that is probably not necessary as the compilation process itself should be free of side effect.

Also, you can't really remove side effect. At some point, you'll need to have an interpreter actually performs the intended side effect. You're really just moving side effect around, now you have the option between running those side effect in a virtual machine that you design and developed, or why not just rely on the well tested PHP interpreter and just write straight PHP code and accept that side effect need to happen.

Another lighterweight approach you could've taken to avoid needing to developing a full blown interpreter is to just have your compiler generate PL/SQL code (or whatever equivalent for your database system).

Answered by Lie Ryan on November 8, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP