Separation According to Testability
A simple criteria for splitting up logic
In Radical Object-Orientation, logic is encapsulated in objects primarily represented by functions. This approach enhances both understandability and testability.
But how and when should logic be divided into different functions? Which logic should be wrapped in an object? The guiding principle is the Single Responsibility Principle (SRP). For details see:
Rather than exploring this deeply here, though, I'll offer a rough heuristic for implementing Radical Object-Orientation by zooming out and taking a complete software system into view:
Such a system comprises (at least) three distinct concerns:
UI/Frontend logic that interacts with users, triggering the system into action. Resources used include command line, console, GUI API, HTTP communication infrastructure, etc.
Service logic that utilizes resources the software system depends on, such as databases, file systems, time functions, random number generators, HTTP communication APIs, etc.
Domain/Core logic that operates independently of external resources. It may require in-memory state but doesn't access the environment or infrastructure.
Resource access logic is encapsulated in adapters. For user interactions, I call these "portals"; for service usage, I call them "providers."
Logic across these concerns varies in testability:
Domain logic: easy to test
Provider logic: harder to test
Portal logic: hardest to test
This difference in testability is the primary reason for dividing logic into separate functions.
Most importantly, separate UI logic from everything else.
With clever isolation, you might not even need to test it frequently.
Then separate service logic from core logic. Implement automated tests for both, but don't contaminate domain logic tests by involving service access. (The solution isn't DIP but IOSP — more on that in a future post.)
Here's an extended palindrome scenario as an example: This time palindromes are not only detected but also logged.
All three basic aspects exist in the code. Different APIs are separated into different functions. API calling logic isn't intermixed with core logic.
The location of these aspects in the message flow is easily visualized:
This represents the minimum separation you should implement.
Not all objects are equal — while they share the same basic form, they differ in resource usage and testability.
Ensure you separate your logic accordingly and focus each object's responsibility!