Web MVC With Spring and Business Objects
Once you recognize the business objects in your system and their functionality, it’s really easy to move those into a framework like Spring MVC.
Join the DZone community and get the full member experience.
Join For FreeIn my previous posts, we explored the MVC Pattern Language by Trygve Reenskaug and made an attempt to implement MVC in the console, while focusing on MVC’s overarching idea – supporting users’ mental models. In this post, we’ll take a quick look on how to move the (poor) console example to the web.
A Short Reminder
For those of you who haven’t read the previous post or don’t (want to) remember, we were creating a simple “Pet Clinic” application. We visited an old vet lady who does not own a computer to gather requirements and get a basic idea of what her work is about. For the most part, she’s using two interesting items at work: a visit calendar and pet files. These are perfect candidates to become business objects in our system. Since the lady is so used to them in the physical world, we assume she’ll have no problems using their computerized versions. So far, we created a console representation of the visit calendar. Now, we want to move it to the web so that it’s usable for non-nerd human beings.
View
In the console example, the view was a regular Java object, which talked to the VisitCalendar model object and displayed it on the screen:
public class VisitCalendarView implements View {
// stuff
private void show(DayOfWeek day) {
System.out.println(day + ":");
show(visitCalendar.visitsOn(day));
}
private void show(List<Visit> visitsOnDay) {
if (visitsOnDay.isEmpty()) {
System.out.println("No visits!");
} else {
visitsOnDay.forEach(this::show);
}
}
private void show(Visit visit) {
System.out.println(visit.getTime() + ": " + visit.getOwnerName());
System.out.println("Pets: " + visit.getPetNames());
}
// stuff
}
In our Spring MVC version of the view, we want to replace the console printing stuff with HTML. Now, we have two choices:
- The HTML template is the view and talks to the model object directly.
- The view is a Java object responsible for filling the HTML template and rendering it (effectively, it could simply return Spring’s ModelAndView)
In my code, I started with the first option but one could easily find reasons to go with the second. The interesting part of my view template looks like this (please, don’t learn HTML from me and don’t judge me, I know I suck at this):
<div class="container">
<div class="row navigation">
<h1 class="col-3">
<a href="/previous">Previous</a>
</h1>
<h1 class="col-6 text-center">
${visitCalendar.currentWeek.start} - ${visitCalendar.currentWeek.end}
</h1>
<h1 class="col-3 text-right">
<a href="/next">Next</a>
</h1>
</div>
<div class="row">
<table class="table table-sm table-striped table-bordered">
<thead class="thead-inverse">
<tr>
<th></th>
<#list visitCalendar.openDays as day>
<th class="text-center">${day}</th>
</#list>
</tr>
</thead>
<tbody>
<#list visitCalendar.visitTimes as visitTime>
<tr>
<td>${visitTime}</td>
<#list visitCalendar.openDays as day>
<td>
<#if visitCalendar.visitOn(day, visitTime)??>
<#assign visit = visitCalendar.visitOn(day, visitTime)/>
<div>${visit.ownerName}</div>
<div>${visit.petNames?join(", ")}</div>
<#else>
<div class="visit-form">
<div class="click-to-add text-center">
Click to add
</div>
<form method="post">
<div class="form-group">
<input type="text" class="form-control" name="owner" placeholder="Owner">
</div>
<div class="form-group">
<input type="text" class="form-control" name="pet" placeholder="Pet">
</div>
<input type="hidden" name="day" value="${day}">
<input type="hidden" name="time" value="${visitTime}">
<input type="submit" class="btn btn-primary" value="Submit">
</form>
</div>
</#if>
</td>
</#list>
</tr>
</#list>
</tbody>
</table>
</div>
</div>
As you can see (actually, I hope you just skimmed it and didn’t bother to understand), we’re showing our vet lady a page from her calendar i.e. one week of visits. She can insert visits into the calendar the same way she does with her physical one — if there is no visit on given time, you can “type in” a visit directly there. Overall, it renders something like this (shame rises):
Model
Since the HTML template talks to the model directly, it had to change a little to accommodate template’s idiosyncrasies i.e. public methods and retrieving visits one-by-one. Regarding using it with Spring MVC, I had to make it a session scoped bean so that users don’t mess with each others calendar i.e. the currentWeek field:
@Service
@SessionScope
public class VisitCalendar {
private static final List<DayOfWeek> OPEN_DAYS = asList(MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY);
private static final LocalTime OPEN_TIME = LocalTime.of(8, 0);
private static final LocalTime CLOSE_TIME = LocalTime.of(18, 0);
private static final int VISIT_DURATION = 30;
private Week currentWeek;
private Visits visits;
public VisitCalendar(Visits visits) {
this.currentWeek = Week.since(LocalDate.now().with(MONDAY));
this.visits = visits;
}
public Week getCurrentWeek() {
return currentWeek;
}
public List<DayOfWeek> getOpenDays() {
return OPEN_DAYS;
}
public List<LocalTime> getVisitTimes() {
List<LocalTime> visitTimes = new ArrayList<>();
for (LocalTime time = OPEN_TIME; time.isBefore(CLOSE_TIME); time = time.plusMinutes(VISIT_DURATION)) {
visitTimes.add(time);
}
return visitTimes;
}
void nextWeek() {
this.currentWeek = currentWeek.next();
}
void previousWeek() {
this.currentWeek = currentWeek.previous();
}
public Visit visitOn(DayOfWeek day, LocalTime time) {
return visits.on(currentWeek.get(day).atTime(time)).orElse(null);
}
void addVisit(DayOfWeek dayOfWeek, LocalTime time, String ownerName, String petName) {
LocalDateTime dateTime = currentWeek.get(dayOfWeek).atTime(time);
Owner owner = new Owner(ownerName);
List<Pet> pets = asList(new Pet(petName, owner));
visits.add(new Visit(dateTime, pets));
}
}
When used with a real database, the visitOn method might quickly become too slow for the application to work smoothly. In such case, we could, for example, keep current week’s visits in model‘s or view‘s memory (the latter calls for using an object as a view instead of an HTML template).
Controller
The Spring MVC controller obviously looks very different from the console one, but the general idea and functionality remained:
@Controller
public class VisitCalendarController {
private VisitCalendar visitCalendar;
public VisitCalendarController(VisitCalendar visitCalendar) {
this.visitCalendar = visitCalendar;
}
@GetMapping
public String show(Model model) {
model.addAttribute("visitCalendar", visitCalendar);
return "visitCalendar";
}
@GetMapping("/next")
public String nextWeek() {
visitCalendar.nextWeek();
return "redirect:/";
}
@GetMapping("/previous")
public String previousWeek() {
visitCalendar.previousWeek();
return "redirect:/";
}
@PostMapping
public String addVisit(@ModelAttribute VisitForm visitForm) {
visitCalendar.addVisit(
visitForm.getDay(),
visitForm.getTime(),
visitForm.getOwner(),
visitForm.getPet());
return "redirect:/";
}
}
This solution keeps the “next” and “previous” commands I used in the console solution. One could as well replace the /next and /previous endpoints with some sort of paging. The user experience would remain just as good, but we could avoid redirecting to the root path all the time and give users the possibility to share URLs to certain weeks.
Summary
Once you recognize the business objects in your system and their functionality, it’s really easy to move those into a framework like Spring MVC. The model that you implement is not very different from the one you’d implement for any other kind of application. The controller and the view will depend a lot on the underlying technology that you’re using. If it’s Spring MVC, implementing the former is pretty straightforward, while there are at least two good options for the latter. Now, the real idea behind this post is the same as in the previous one – the model, view, controller triplet is supposed to represent business objects inspired by the users’ mental models, instead of being a generic presentation framework.
Published at DZone with permission of Grzegorz Ziemoński, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments