In previous posts, I discussed general patterns of message-based systems along with basic guidance on developing components for such systems. While these posts may have provided basic information about their respective topics, there’s a clear difference between theory and implementation. (The former is easier to fake.) Accordingly, it’s time to get our hands dirty and provide concrete guidance on developing packages for Robot Operating System (ROS) using proven messaging-system and other developmental design patterns.
This post is the first in a number of posts which walks the reader through nearly every facet of developing a package for ROS. Primary interests of this series will focus on:
- Layering a ROS package for maintainability and clean separation of concerns,
- Making your package as reusable (as practical) to be used with any messaging framework by introducing a clean separation between package concerns and the underlying ROS messaging framework,
- Using separated interface to easily unit test the elements of your package in isolation of each other and of ROS, and
- Leveraging proven design patterns to develop a solid package which provides a good balance of theoretical best practices and practical development.
A ROS package is a complete application which happens to send and receive messages via messaging middleware (e.g., ROS) to help it complete its job. This isn’t too different from an application which simply retrieves and saves data to a database. Accordingly, the development of the package deserves the same design considerations that a stand-alone application would should receive. This series of blogs will walk the reader through the development of a package, taking into such considerations, appropriately. This will be a six part series include:
- Part I: Planning the package architecture,
- Part II: Creating the package skeleton,
- Part III: Developing and testing the domain layer,
- Part IV: Developing and testing the application service layer,
- Part V: Developing and testing the message endpoint layer, and
- Part VI: Adding a UI to the package.
At the end of this series, the reader should feel comfortable with the concepts behind designing and developing an extensible and maintainable ROS package adhering to proven design patterns and solid coding practices.
A Ladar Reporting Package
Before delving straight into implementation, it’s important to establish the context of the project to assist in guiding architectural and developmental decisions. Accordingly, the package that will be developed will be a ladar reporting package. The requirements of the package are simple enough:
- The package will read laser reports coming from a laser range-finder.
- Reports acquired from the laser range-finder will be published to a ROS topic as LaserScan messages.
- The user may start and pause the ladar reporting process via a simple UI front-end.
Our first stop will be in examining the overall architecture of the package in order to conceptualize how the end product will be layered and developed.
Part I: Planning the Package Architecture
When developing any application, it’s important to strike the appropriate balance between extensibility and maintainability. We’ve all seen as many “over-architected” solutions as those that did not receive a single thought toward design. Such applications usually meet their demise in the middle of the night when a wearied developer throws up their hands in debugging exhaustion before starting The (infamous) Rewrite. Accordingly, when designing a package, “just enough” prefactoring should take place to architect a solution that will smoothly evolve to meet the package’s requirements without over-complicating the solution. For the task at hand, when architecting a ROS package, there are a number of minimal architectural guidelines which should be adhered to which will lead to a extensible messaging component while being maintainable and easy to understand; a few guiding principles are as follows:
- A domain layer should exist to encapsulate the core capabilities of the package. Excellent guidance for developing the domain layer may be found in Eric Evan’s Domain Driven Design. See Domain Driven Design Quickly for just that.
- The domain layer of the package should not have knowledge concerning how to communicate with the messaging middleware directly (e.g., ROS). This implies that the domain layer should have no direct dependencies on the messaging middleware. This allows the domain layer to be more easily reused with another messaging middleware solution. Additionally, keeping this clean separation of concerns facilitates the testing of the domain layer independently from its interactions with the messaging middleware.
- An application services layer should be responsible for managing application “workflow.” An example is accepting data generated by the domain layer and passing it on to the messaging middleware. Even then, the application services should only communicate with the messaging middleware (e.g., ROS) via message endpoint interfaces. Likewise, if the application services need to communicate to a database or other data source, they should do so via data repository interfaces. This use of separated interface provides the same decoupling benefits as those described for the domain layer. Admittedly, the application services layer is the trickiest layer to cleanly define the boundry of – it should not contain anything related to the UI (e.g., it should have no references to wxWidgets) nor should it contain any business logic. A simple way to visualize the responsibilities of the application services layer is to see it as a “task manager.” I.e., it’s responsible for ensuring that all steps of a given task are completed but is unaware of how to do the work itself; kind of like your manager at work, right? The execution of the task steps themselves are then carried out by elements of the domain layer and, as we’ll see, the message endpoint concrete implementations.
- Everything should be independently testable (and tested) from service dependencies. Accordingly, message endpoints, and other external service dependencies, should be injected into the application services layer via dependency injection. Doing so facilitates the testing of the application services and domain layers using test doubles. (To keep things justifiably simpler, we will not be using an IoC container for dependency injection, but manual dependency injection instead.)
At first glance, this may appear to be a lot more than just “minimal prefactoring.” But after more than a dozen years of professional development, and many painful lessons along the way, I can assure you that this is a solid approach to architecting your package. With that said, I certainly encourage you and your team to discuss what architectural approach is most appropriate for the task at hand – and the capabilities of the team – before agreeing upon a particular approach.
The best way to further discuss how the architectural guidelines will be implemented in the package, is to review a UML package diagram expressing all of the layers and the direction of dependencies, accordingly:
Keep in mind that this is not a diagram for the overall message-based system; this represents how each individual ROS package is architected. Accordingly, in the above UML package diagram, each box represents a separate library encapsulating a distinct functional concern. The arrows represent the direction of dependencies; e.g., application_services depends on core. Let us now review each layer in more detail, beginning from the bottom up.
As described previously, the domain layer encapsulates the core capabilities of the package. For instance, if this were a package providing SLAM capabilities, then this layer would contain the associated algorithms and logic. In addition to domain logic, the core library contains interfaces for services which depend on external resources, such as message endpoints and (if applicable) data repositories. Having interfaces in the core library allow both core and the application services layers to communicate with services while remaining loosely coupled to the implementation details of those services.
As described previously, the application services layer acts as the “task manager” of the application, delegating execution responsibilities to the domain layer and services. Note that the application services layer, similar to core, has no direct dependency on the service implementation classes, e.g., the message endpoints. It only has knowledge of the service interfaces for remaining loosely coupled and for facilitating unit testing.
Arguably, in smaller packages and applications, an application services may not add much value and simply complicate an otherwise simpler design. With that said, care should be taken at the beginning of a project to fully consider the pros and cons of including an application services layer before deciding to discard its use, accordingly.
If the package were to retrieve and save data to a database or other external data source, data repositories would encapsulate the details of communicating with the external data source. Note that the data repositories implement the repository interfaces defined in core.
This library contains the concrete implementations of the message endpoints. The message endpoints are the only classes which actually know how to publish, subscribe, and otherwise communicate with the ROS messaging middleware. Note that the message endpoints implement the endpoint interfaces defined in core.
This layer contains the user interface for interacting with the package, if needed.
Finally, this library contains all of the unit tests for testing all layers of the ROS package.
This is our package architecture in a nutshell. In the next post, we’ll begin developing each layer from Core on up to the UI, starting with the overall package structure itself.