Cleaner Code, Better Tests: Leveraging Humble Objects for Better Architecture

Maxim Gorin
9 min readJul 9, 2024

--

Testing is often a thorny challenge in software development, especially when dealing with complex behaviors and user interactions. Imagine if we could make our code not only easier to test but also more maintainable and robust. This is where the Humble Object pattern comes into play.

I don’t know why, but I thought the sheep look like Humble Objects. DALL-E helped bring my idea to life.

In our previous article, “Core Principles of Clean Architecture: From Entities to Frameworks”, we laid the groundwork for building scalable and maintainable software systems. Today, we take a closer look at a specific technique that can significantly enhance the testability and modularity of your code: the Humble Object pattern.

In this article, we’ll delve into the Humble Object pattern and its practical application through presenters and views. We’ll explore how this pattern separates complex, hard-to-test code from simpler, test-friendly logic, and why this is particularly beneficial in mobile development. By the end, you’ll see how adopting these strategies can transform your approach to building and testing software, leading to cleaner, more modular architecture.

Humble Object Pattern

Humble Object | Martin Fowler

Definition and Core Concept

The Humble Object pattern is a design approach aimed at enhancing testability by isolating complex, hard-to-test behaviors from the easier-to-test logic. This pattern helps manage components that are challenging to test due to their reliance on user interactions and visual output. By isolating these behaviors within a “humble” object, and separating the testable logic into a distinct module, developers can achieve a more maintainable and robust codebase.

Instead of embedding complex logic within the components that directly interact with external systems or users, the Humble Object pattern suggests creating a simple object that handles only the essential, hard-to-test behaviors. The remaining logic, which is easier to test, is moved into a separate module. This separation not only enhances testability but also clarifies the responsibilities of different components, leading to a cleaner and more modular architecture.

For example, in a mobile app, the GUI code responsible for displaying data should be kept as simple as possible. The logic for formatting and processing this data should be moved to another class or module. This way, the GUI only handles the minimal essential behaviors, while the bulk of the logic is handled by a more testable component. This approach not only improves testability but also enhances the overall architecture by clearly defining the responsibilities of different components.

Separating Behaviors

Applying the Humble Object pattern involves identifying parts of your code that are difficult to test, such as those interacting directly with the UI or external systems. These parts are simplified and placed in the humble object. The logic that can be tested independently is then moved into a separate module.

This principle can be applied universally across various types of software development. Whether you are developing desktop applications, web applications, or embedded systems, the goal is the same: to keep the complex, hard-to-test code isolated and to move the testable logic into separate, more manageable units. This approach ensures that different parts of your system have well-defined responsibilities and can be tested independently.

Presenters and Views

MVC vs MVP vs MVVM

Role of Presenters and Views

In the context of the Humble Object pattern, presenters and views play distinct roles to maintain a clean separation of concerns. The view acts as the humble object, handling the direct interactions with the user interface or external systems. Its primary responsibility is to display data and capture inputs without processing any business logic.

The presenter, on the other hand, is responsible for processing data and preparing it for presentation. It takes data from the application, applies necessary business rules, formats it, and updates the view model. This model is then used by the view to render the interface. By handling all the business logic, the presenter ensures that the view remains focused on its primary role of data presentation, making the system more modular and easier to test.

This separation ensures that business logic is isolated from the interface, making the code easier to test and maintain. It also promotes a modular architecture, where changes to the interface do not affect the core logic of the application.

Implementing the Presenter Pattern

To implement the presenter pattern in software development, follow these steps:

  1. Define the Interface: Create an interface that includes methods for displaying data and capturing inputs. This interface will be implemented by the actual components interacting with external systems.
  2. Develop the Presenter: Create the presenter class that interacts with the interface through the defined methods. The presenter handles all business logic, data formatting, and updates to the data model.
  3. Link Presenter and Interface: In your application, instantiate the presenter and pass a reference to the interface. This setup allows the presenter to control the interface and update it with processed data.
  4. Ensure One-Way Data Flow: Maintain a one-way data flow from the presenter to the interface. The interface should only display data and relay inputs back to the presenter.

Handling Data

Presenters are tasked with formatting data for the interface. For instance, if the application needs to display a date, the presenter converts the Date object into a formatted string and places it in the data model. Similarly, for monetary values, the presenter formats the currency object appropriately and handles any specific display requirements, such as showing negative values in red.

The data model serves as a simple structure that holds all the formatted data needed by the interface. Populated by the presenter, it ensures the interface remains focused solely on rendering data without any business logic. This keeps the interface humble and straightforward.

Testing and Architecture

Generic Testing Architecture

Importance of Testability

Testability is a key aspect of good architecture. When your code is easy to test, it becomes easier to identify and fix bugs, refactor safely, and ensure that changes do not introduce new issues. A testable codebase allows developers to write automated tests that can quickly validate the correctness of the system, making development more efficient and reliable.

Testable architectures tend to be more modular, with clear boundaries between different components. This modularity not only enhances testability but also improves the overall maintainability and scalability of the system. When components are well-defined and isolated, they can be developed, tested, and maintained independently, reducing the complexity of the system as a whole.

Separating Testable and Non-Testable Code

Separating testable code from non-testable code is essential for improving testability. This separation involves isolating complex, hard-to-test behaviors within humble objects and moving the easily testable logic into distinct, test-friendly modules. By doing this, you create clear boundaries within your codebase, making it easier to test individual components without having to deal with the complexities of the entire system.

For example, UI components that interact directly with users can be challenging to test. By using the Humble Object pattern, you can move the business logic and data formatting into a separate presenter class, leaving the UI component as a simple, humble object. This approach allows you to test the business logic and data formatting independently of the UI, ensuring that your tests are more focused and easier to manage.

Mobile Development Context

In mobile development, testability is particularly important due to the frequent updates and variations in devices and operating systems. Mobile applications often need to be tested on multiple devices and platforms to ensure a consistent user experience. By adopting a testable architecture, you can streamline this process and make it more efficient.

Using patterns like MVP (Model-View-Presenter) or MVVM (Model-View-ViewModel) in mobile development can greatly enhance testability. These patterns promote the separation of concerns, where the view handles the UI, the model manages the data, and the presenter or view model handles the business logic and data presentation. This separation allows you to test each component independently, ensuring that the application behaves correctly under different conditions.

Automated testing in mobile development to quickly identify and fix issues. Unit tests can verify the correctness of the business logic, while UI tests can ensure that the user interface behaves as expected. By keeping the UI code simple and moving the business logic to testable components, you can write more effective and reliable tests.

Database Gateways

Role of Gateways

Database gateways serve as interfaces between the application logic and the database. They define methods for create, read, update, and delete (CRUD) operations that can be performed on the database. By using gateways, you ensure that the application logic does not contain any database-specific code, making the system more modular and easier to maintain.

Gateways abstract the details of database interactions, providing a simple interface for the application logic to use. This abstraction allows you to swap out the underlying database implementation without affecting the core business logic, enhancing the flexibility and adaptability of your system.

Interfaces and Implementations

To use database gateways effectively, you should define interfaces that specify the methods required for interacting with the database. These interfaces are then implemented by classes in the database layer, which handle the actual database interactions.

For example, you might define a UserGateway interface with methods like getUserById, createUser, updateUser, and deleteUser. The implementation of this interface would contain the actual SQL queries or calls to the database API. By keeping the SQL or database-specific code in the implementation class, you ensure that the business logic remains clean and focused on its core responsibilities.

This approach also makes it easier to test the application logic. By using mock implementations of the gateway interfaces, you can write unit tests for the business logic without needing to interact with the actual database. This not only speeds up testing but also makes the tests more reliable and easier to maintain.

Data Mappers

Mastering Data Mapping: Techniques and Best Practices for Optimal Integration

Role of ORMs and Data Mappers

Object-Relational Mappers (ORMs) simplify the process of working with databases by mapping data between relational tables and object-oriented programming languages. In the context of clean architecture, ORMs act as data mappers, transforming database rows into objects that the application can use.

Separating Data and Objects

It is essential to keep a clear separation between data structures and business objects. ORMs should reside in the database layer, acting as intermediaries that convert raw data into structured objects. This separation ensures that business logic remains decoupled from database implementation details, making the system more modular and testable.

Services

Interacting with External Services

Applications often need to communicate with external services, such as APIs or other applications. Managing these interactions efficiently is crucial for maintaining a clean architecture. The Humble Object pattern is useful here as well, creating a boundary that separates the core logic from the external service integration.

Using Humble Objects

When dealing with external services, use humble objects to manage the interaction. These objects handle the communication and data transformation, ensuring that the core application logic remains focused and testable. By keeping the integration details within humble objects, you can simplify testing and maintenance, allowing the core logic to remain clean and independent of external changes.

Conclusion

Navigating the complexities of software architecture can often feel like walking a tightrope, balancing between functionality, maintainability, and testability. In this article, we delved into the Humble Object pattern and its application through presenters and views, demonstrating how this approach can transform the way we handle complex, hard-to-test code.

By isolating difficult behaviors and focusing on clear, modular responsibilities, we pave the way for cleaner, more manageable codebases. This practice not only simplifies testing but also enhances the overall robustness and adaptability of our applications, particularly in the dynamic landscape of mobile development.

The Humble Object pattern is more than just a design strategy — it’s a mindset that encourages thoughtful separation of concerns and a commitment to quality. As you implement these principles in your projects, you’ll find that your code becomes not only more reliable but also easier to extend and maintain.

Thank you for joining us on this exploration of clean architecture. If you found these insights valuable, don’t forget to subscribe for more in-depth articles on software development. We invite you to share your thoughts and experiences in the comments below — let’s continue this journey towards cleaner, more effective code together.

--

--

Maxim Gorin
Maxim Gorin

Written by Maxim Gorin

Team lead in mobile development with a passion for Fintech and Flutter. Sharing insights and stories from the tech and dev world on this blog.

No responses yet