September 27, 2015

RESTful software requirements specification (part 4 of 4)


Pages: 1 2 3 4


State diagram: Defining the REST service interface

Specification

In this stage we design the server-side service contracts, baed on the relations between our business models, as discovered in the class diagram. We do so by defining the state transition which happens to any business model after one of the supported CRUD methods is executed. Remember that in REST wording, the association is:
Create: PUT
Read:   GET
Update: POST
Delete: DELETE
According to the microformats standard which we apapt here, there’s typically a GET with an id to get the one entity with the id provided and a GET without id to get all the entities.

There is a 1 : 1 relation between business model and service: One service per business model. Hence we also have to define a unique service URL for each business model service: Can it be reached directly or only via a relation on another business model? The entity relations modeled in the class diagram can help us discover this.

Here is a service definition for the example use cases:

/books:
 /users:
/users/:id/orders:
We do purposefully use a pseudo-RESTful syntax in these diagrams: We use HTTP verbs with RESTful URL patterns, response status codes, response content and metadata. This will greatly facilitate implementation. This also proves here the high degree of self-documentation that REST offers. Also note that even though we limit ourselves here to the few state transitions implied by the use cases, we can already see similarities between all the resources. It’s of course best practice to search for similarities and unify service contracts wherever possible; this is yet another thing we can directly translate into the actual implementation in order to stay DRY.

As you can see above, we covered the “get a book” functionality by the book resource itself. There is actually no need for an artificial “catalogue” pseudo-resource, as there is only one catalogue. Always try to think in independent “resources” when modeling state transfer, and ask yourself: “In which resource’s responsibility lies this state transfer?” You will then find out any state machine inconsistency.

Also, there is no “independent” /orders resource as an order is always bound to a user. Our class diagram actually revealed this when we saw that the multiplicity of the “user to order” relation in the “user” side is at least 1; an order without a user cannot exist. In the diagram, we illustrated this intention by using a composition relation between these two entities. Thinking of relations as potential “navigation paths” helps you prevent implementing illogical or inconsistent service interfaces later on.

Again note that we try to unify the service interface for every resource whenever possible, using the same “syntax” for every resource to handle these CRUD methods.

Implementation

We can translate this state diagram directly into a Java EE JAX-RS RESTful web service just by applying the appropriate Java annotations to a resource POJO.

Because we created a unified service contract for every resource, we can stay DRY (don’t repeat yourself) compliant in the code as well by defining the service contracts in one base resource only:
public abstract class BaseResource<T extends BaseHalModel> {
    @GET
    @Path("/")
    @Produces(MediaType.APPLICATION_JSON)
    public List<T> findAll(@Context UriInfo uri) {
        return new ArrayList<>(getService().findAll());
    }
    
    @GET
    @Path("/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public T findById(@PathParam("id") Long id) {
        return getService().findById(id);
    }
    
    @PUT
    @Path("/")
    @Consumes(MediaType.APPLICATION_JSON)
    public Response save(@Context UriInfo uri, T entity) {
        entity.removeLinks(); // cleanup HAL metadata
        try {
            entity = getService().save(entity);
            entity.addLink("edit", uri.getPath() + entity.getId()); // add HAL metadata
            return Response.status(Response.Status.OK).entity(entity).build();
        } catch (ConstraintViolationException ex) {
            return ex.getResponse();
        }
    }
    
    @DELETE
    @Path("/{id}")
    public Response delete(@PathParam("id") Long id) {
        try {
            getService().delete(id);
            return Response.status(Response.Status.NO_CONTENT).build();
        } catch (ConstraintViolationException ex) {
            return ex.getResponse();
        }
    }
    
    protected abstract BaseService<T> getService();
}
The actual implementation of the CRUD operations is not of interest from our REST service perspective. It would typically be implemented using an EntityManager which connects to a DB. For the demo implementation, I have mocked DB access with an application-scoped Map.

For the @PUT operation, I use the HAL format to add “link” metadata to the HTTP response. A validation constraint violation will trigger a 400 response with the validation constraint messages in the JSON body. Note that in a real world application, you would rather send a message bundle key than a localized validation error String so that the client side can localize the message.

Apart for the individual root paths of each concrete resource, their implementation is literally empty:
@Path("users/{userId}/orders")
@Stateless
public class OrderResource extends BaseResource<Order> {
    // implement getService() by dependency injection
}

Feel free to take a look at all of the resource implementation classes at the project’s GitHub repository.

Finally, we can now implement the REST consumer on the client side, i.e. we implement the actions triggered by button / link clicks or other user interactions using AngularJS with Restangular for REST interface consumption.

This, for instance, is the implementation of OrdersEditCtrl which backs the 3. users/:user.id/orders/new?bookId=:book.id view:
app.controller("OrdersEditCtrl", function ($scope, $route, $routeParams, $location, Restangular) {
    $scope.initEntity = function() {
        if ($routeParams.id === "new") {
            $scope.entity = Restangular.one("users/0/orders/");
            if (typeof $routeParams.bookId !== 'undefined') {
                $scope.entity.deliveryDate = moment().add(1, 'days').toDate();
                $scope.entity.book = Restangular.one("books", $routeParams.bookId).get().$object;
            }
        }
    };
    
    $scope.back = function() {
        $location.path("/books/" + $scope.entity.book.id);
    }
    
    $scope.execute = function() {
        $scope.entity.put().then(function(response) {
            $scope.errors = null;
            $location.path("/users/" + $routeParams.userId + "/orders/").search({created: response.id});
        }, function (response) {
            $scope.errors = response.data;
        });
    }
    
    $scope.initEntity();
});
In initEntity(), the view is initialized with the information from the RESTful URL.

back() simply navigates back according to the information from the activity diagram and the UI mock-ups.

execute() actually does a PUT request to the server, triggering a navigation on success or rendering the error received from a non-2xx response. In the example use case for instance, this would be triggered by a Bean Validation constraint violation, e.g. an order delivery date which is not in the future.

Note that because retrieving a user is not actually a use case covered by this example application, I always work with a statically defined user with id 0.

I’ve created a blog post about AngularJS + Restangular interplay for REST service consumption, in case you’re interested in the technical details. Of course, the source code for all the other controllers is also part of the accompanying GitHub repository of this article.

That’s it. We have now inspected every important part of the specification and implementation of the example project.

Conclusion

With common enterprise software engineering best practices and architecture patterns such as RESTful contracts, object orientation, I18N and DRY built in right from the earliest specification stage, the resulting software product is clearly more likely to adhere to those principles as well. Rather than expecting the programmer to disentangle complex business requirements into a well-thought through implementation under the pressure of everyday software development, he and the business analyst work as a team to find the best possible technical solution for any business requirement in an early project phase.

Not only is overall software and documentation quality increased, but collaboration within the team will also tremendously profit. This of course is especially true in an agile development environment.

I never understood why requirements engineering and implementation should work separately as is often the case in “traditional” project structures. In this article, I wanted to show a way to incorporate some techniques which in my experience work well in software architecture and design into the software specification process as well. Of course, there may be many variations of this process other than the one I’ve illustrated here.

On a more technical note, I especially enjoyed the AngularJS / Restangular / JAX-RS interplay when working on the example project. With the basic architecture and specification laid out so clearly before implementation, it really felt like rapid application development with the UI building and styling, not technical or architectural problems, as the main time-consuming factor.

Please feel free to comment what you think of this article below and to share any experience in applying software development best practices to the requirements specification process.



Pages: 1 2 3 4

No comments:

Post a Comment