|
Jun
19
2010
Developing Well-Designed Packages for Robot Operating System (ROS), Part IVPart IV: Developing and Testing the Application Services [Author's note, July 28, 2010: Introduced Boost::shared_ptr to manage reference to message endpoint so as to enable postponed construction, as will be required in Part VI.] Ah, we’ve made it to the application services layer. After defining the architecture, setting up the package, and implementing the core layer which contains the domain logic of the application, we’re ready to take on developing the application services layer. In short, the service layer provides the external API to the underlying domain layer. Ideally, one should be able to develop the entirety of the “non-UI” portions of an application and have it exposed via the application services layer. In other words, one should be able to swap out the front end – say from a web front end to a Flash front end – without having the application services layer effected. If you’d like to go ahead and download the source for this article, click here. The application services layer is analogous to a task or coordination manager; i.e., it doesn’t know how to carry out the low level details of a particular action but it does know who is responsible for carrying out particular tasks. Accordingly, the application services layer of the package (or any application for that matter) is mostly made up of a number of publicly accessible methods which, in turn, pass responsibilities on to external service dependencies (e.g., a message endpoint) and the domain layer for execution. With that said, the services layer should still eschew direct dependencies on external services, such as communications with a messaging framework (e.g., ROS). Accordingly, in this post, we’ll materialize the application services layer of our package and give it two responsibilities:
But (there’s always a “but” isn’t there), while the application services layer should be responsible for passing messages received from the domain layer on to the messaging middleware, it should not have a direct dependency on that messaging middleware itself. This decoupling facilitates testing of the application services layer with test doubles, keeps a clean separation of concerns between task coordination and messaging, and provides greater flexibility with being able to modify/upgrade the messaging layer without affecting the application services layer. A moment needs to be taken to clarify the differences among application services, domain services, and “external resource” services.
Before we delve in, let’s briefly review what we plan to accomplish:
Enough with the chatter, let’s see some code! Target Class Diagram The following diagram shows what the package will look like after completing the steps in this post. While the individual elements will be discussed in more detail; the class diagram should serve as a good bird’s eye view of the current objectives. The elements with green check marks were completed in previous posts.
1. Setup the Package Skeleton and Domain Layer If not done already, follow the steps in Part II and Part III to create the package and develop/test the domain layer. 2. Create an interface for the message endpoint service which the application service layer will leverage to communicate with ROS. // ILaserScanEndpoint.hpp #ifndef GUARD_ILaserScanEndpoint #define GUARD_ILaserScanEndpoint #include "sensor_msgs/LaserScan.h" namespace ladar_reporter_core { class ILaserScanEndpoint { public: // Virtual destructor to pass pointer ownership without exposing base class [Meyers, 2005, Item 7] virtual ~ILaserScanEndpoint() {} virtual void publish(const sensor_msgs::LaserScan& laserScan) const = 0; }; } #endif /* GUARD_ILaserScanEndpoint */ There shouldn’t be anything too surprising in this interface. It simply exposes a method to publish a laser scan to the underlying messaging middleware. Why not put the interface in the application services layer, which intends to use it? A couple of good reasons come to mind: 1) since it’s a pure interface, having elements within core aware of it (or even directly dependent upon it) does not introduce any further coupling to the underlying external resource, the messaging middleware, and 2) in very simply packages, an application services layer might be overkill, so keeping the “external resource service interface” (there’s a mouthful) in the core layer facilitates either approach without having to move anything around if the selected approach changes during development. 3. Create the application service header class. Create src/application_services/LaserScanReportingService.hpp containing the following code: // LaserScanReportingService.hpp #ifndef GUARD_LaserScanReportingService #define GUARD_LaserScanReportingService #include <boost/shared_ptr.hpp> #include "ILaserScanEndpoint.hpp" namespace ladar_reporter_application_services { class LaserScanReportingService { public: explicit LaserScanReportingService(boost::shared_ptr<ladar_reporter_core::ILaserScanEndpoint> laserScanEndpoint); void beginReporting() const; void stopReporting() const; private: // Forward declare the implementation class class LaserScanReportingServiceImpl; boost::shared_ptr<LaserScanReportingServiceImpl> _pImpl; }; } #endif /* GUARD_LaserScanReportingService */ A few notes:
4. Create the application service implementation class. Create src/application_services/LaserScanReportingService.cpp containing the following code: // LaserScanReportingService.cpp #include "sensor_msgs/LaserScan.h" #include "ILaserScanListener.hpp" #include "LaserScanReader.hpp" #include "LaserScanReportingService.hpp" using namespace ladar_reporter_core; namespace ladar_reporter_application_services { // Private implementation of LaserScanReportingService class LaserScanReportingService::LaserScanReportingServiceImpl : public ladar_reporter_core::ILaserScanListener { public: explicit LaserScanReportingServiceImpl(boost::shared_ptr<ILaserScanEndpoint> laserScanEndpoint); void onLaserScanAvailableEvent(const sensor_msgs::LaserScan& laserScan) const; LaserScanReader laserScanReader; private: boost::shared_ptr<ILaserScanEndpoint> _laserScanEndpoint; }; LaserScanReportingService::LaserScanReportingService(boost::shared_ptr<ILaserScanEndpoint> laserScanEndpoint) : _pImpl(new LaserScanReportingServiceImpl(laserScanEndpoint)) { // Wire up the reader to the handler of laser scan reports _pImpl->laserScanReader.attach(*_pImpl); } LaserScanReportingService::LaserScanReportingServiceImpl::LaserScanReportingServiceImpl(boost::shared_ptr<ILaserScanEndpoint> laserScanEndpoint) : _laserScanEndpoint(laserScanEndpoint) { } void LaserScanReportingService::beginReporting() const { _pImpl->laserScanReader.beginReading(); } void LaserScanReportingService::stopReporting() const { _pImpl->laserScanReader.stopReading(); } void LaserScanReportingService::LaserScanReportingServiceImpl::onLaserScanAvailableEvent(const sensor_msgs::LaserScan& laserScan) const { // Send laserScan to the message end point _laserScanEndpoint->publish(laserScan); }; } A few notes:
As mentioned previously, the above implementation could be done without a private implementation design pattern, but this serves to illustrate how such a pattern may be leveraged when warranted. 5. Configure CMake to Include the Header and Implementation With the header and implementation classes completed, we need to make a couple of minor modifications to CMake for their inclusion in the build.
6. Build the application services Class Library In a terminal window, cd to /ladar_reporter and run Like before, we’re not done yet…it’s now time to test our new functionality. 7. Unit Test the LaserScanReportingService Functionality When we go to test the functionality of the laser scan reporting application service, it will likely be quickly noticed that there’s a missing dependency which will be needed to test the functionality of this class: a concrete implementation of ILaserScanEndpoint.hpp. The job of the message endpoint class will be to take laser scans and publish them to the appropriate topic on ROS. But we’re just not there yet…what we’d really like to do is to be able to test the functionality of the application service layer – LaserScanReportingService.cpp to be specific – without necessitating the presence of the message endpoint and ROS itself. While we’ll have to cross that bridge eventually (in Part V to be exact), we’re not currently interested in doing integration tests. Instead, we’re only interested in testing the behavior of the application service regardless of its integration with ROS. Accordingly, a “stub” object will be employed to stand in for an actual message endpoint. In this case, the stub object is nothing more than a concrete implementation of ILaserScanEndpoint.hpp; but instead of publishing the laser scan to ROS, it’ll do something testable, such as keep a tally, or possibly a temporary queue, of laser scans “published” which can then be verified with testing asserts. If you’re new to unit testing, you’ll want to read about test doubles, Martin Fowler’s Mocks Aren’t Stubs, and XUnit Test Patterns for a more comprehensive treatment of the subject. Onward with testing…
While running the tests, you should see a few messages published to the message endpoint stub. This demonstrates that all of the interactions among our core and application service layers are occurring exactly as expected. Now for the fun part… In Part V, we’ll finally take a look at using all of these pieces together in order to publish messages to a ROS topic via a message endpoint. Enjoy! |
© 2011-2012 Codai, Inc. All Rights Reserved