6

I want to model (TV)Events and Reminders and I’m wondering what’s ‘the best’ way to model this.

The requirements are roughly

  • When an Event has no Reminder, a Reminder can be created
  • When an Event has a Reminder
    • the Reminder can be deleted
    • an icon is shown on the Event
  • A separate Screen exists presenting all Reminders
  • Reminders are stored in the backend and have multiple properties

Obviously, our real domain is larger and also contains Channels, Recordings, ...

Basically, we need functions to

  • create a Reminder
  • delete a Reminder
  • get the list of all Reminders
  • check if an Event has a Reminder

This seems a relatively simple problem but I'm struggling with it. Below, I’ve collected a number of possible options and I would greatly appreciate your feedback.

Option 1

Event creates Reminders

Event API

Reminder createReminder() //adds the Reminder to the backend 
boolean  hasReminder()

Reminder API

void deleteReminder() //removes the Reminder from the backend
static List<Reminder> getAllReminders()

But

  • deleting and creating a Reminder are symmetric functions and hence I would expect them on a single API (now both Event and Reminder have an interface to the backend)
  • I'm not a big fan of the static function on Reminder. It seems to indicate that a higher-order object should exist

Option 2

Reminder API

Reminder createReminder(Event event) //adds the Reminder to the backend
void deleteReminder() //removes the Reminder from the backend
boolean hasReminder(Event event)
static List<Reminder> getAllReminders()

Advantage is that the Event API doesn't change when adding Reminder functionality (=> good wrt extendibility I guess). But

  • creating a Reminder for a Event (and checking whether an event has a reminder) seems to be more a function on Event so I would expect them on the Event API

Option 3

Event API

Reminder createReminder() //adds the Reminder to the backend
void deleteReminder() //removes the Reminder from the backend
boolean hasReminder()
static List<Reminder> getAllReminders()

But

  • the getAllReminders() seems more a function on Reminder

Option 4

Create a ReminderManager/ReminderService (singleton) with the following API

Reminder createReminder(Event) //adds the Reminder to the backend
void deleteReminder(Reminder) //removes the Reminder from the backend
boolean hasReminder(Event event)
List<Reminder> getAllReminders()

This is my preferred option but

  • This results in anemic Event/Reminder objects (just getters and setters but no logic). There seems to be discussion whether this is an anti-pattern or not
2

Basically if you look at your requirements posted above:

  • create a Reminder
  • delete a Reminder
  • get the list of all Reminders
  • check if an Event has a Reminder

These are classic plain CRUD. Even the keywords match almost exactly Create, Read (/find/get/check), Update, Delete.

As such, you have no complicated domain or business logic where a rich domain model would be an advantage. That's why you are struggling to find the correct places for your API methods.

See here for similar opinions:

Anemic vs rich domain model

I'm all for rich domain models, but only where they make sense. They start making sense when you have to implement many or complicated business rules that operate on your domain, and you find yourself duplicating and scattering business logic all over your services. When a new requirement needs changes in 3 services, and maybe you forgot a change in the fourth, then it's time to start thinking about using a rich domain model and consolidating all the business logic in the domain model objects.

See here a nice talk about how to refactor an anemic domain model app to a rich domain model one. Jimmy Bogart even starts the talk by mentioning that it is often difficult to know from the beginning when a rich domain model would be appropriate (since most applications start out being rather simple). So it makes sense to only start refactoring towards a rich domain model once the complexity becomes apparent.

My recommendation

Just use an EventService, ReminderService and so on for most of the methods, and only put those on the domain objects which clearly belong to one of them. I'll try to give concrete examples below.

It is not fully clear whether you want to model your domain on the front end or back end, but from your text I assume it's front end (correct me if I'm wrong and I'll adapt the answer). In that case you should be separating your concerns cleanly, e.g. via a MVP (or MVC, MVVM, ...) architecture.

Assuming you have that, here is how all the parts would fit together:

View

Has obviously no domain or service logic in it. It handles purely presentational concerns (e.g. which icon to show depending on the state of the model).

Presenter / Controller

Handles communication between View and Model, and the "glue" code to get it all working. Here the controller could call ReminderService.getAllReminders() and then populate the ViewModel with the list.

Similarly you have EventService.getAllEvents().

Note the ViewModel and domain model are usually separated, but in simple cases they could be the same thing, especially if it is a CRUD application where every property of the domain model is directly represented in the View; then it would make sense to just pass the domain model through to the View as its ViewModel.

Model

Once you have your ViewModels (which could delegate to the domain model or be the same thing), you can call the other methods directly on the model objects:

class Event
    Reminder createReminder()
    void deleteReminder()
    boolean hasReminder()

Backend

As long as your business logic remains this simple and CRUD-like, you can just use Services that delegate to the persistence layer.

Summary

So basically my suggestion is like your Option 3, but don't be scared to have Service objects if you really have methods that don't fit into a model object. That does not make your domain model automatically into an anemic one. And even if you at the moment have almost no logic in your domain model, that might be ok if you really just have no complex business logic to take care of.

  • Thanks for your extensive answer and the interesting link with CRUD – Marc Van Daele Jan 14 '16 at 13:02
2
+100

It seems to me you are missing a User object. Obviously there will be millions of reminders, but you're only going to be interested in the ones for a particular User.

Hence

User.AddReminder(Reminder reminder)
User.DeleteReminder(string reminderId)
User.Reminders

Also, as you point out the reminder will want to be able to provide the user with a link to the event

Reminder.Event

or possibly :

Reminder.EventId

depending on whether you have a list of events loaded elsewhere

0

I don't see any OO design in your question so I took the liberty to make some.

--

public interface Event {
    public void setDate(java.util.Date date);
    public java.util.Date getDate();
    public void setDescription();
    public String getDescription();
    public boolean hasReminder();
    public void addReminder(Reminder reminder); 
}

You need to set the Event to create a Reminder, the Reminder will add itself to the Event with the callback addReminder(Reminder reminder); of Reminder.

public interface Reminder {
    public void setEvent(Event e);
    public Event getEvent();
    public void setDescription();
    public void setScheduleInfo(ScheduleInfo ei);
    public boolean existsInBackEnd();
    public void save();
    public void delete();
}

--

Schedule info can be complex so I resorted to a cron-like string to simplify this

public interface ScheduleInfo {
    public void setPriority(Priority p);    
    public void setCronString(String cronstr);
}

--

public enum Priority {
    NONE,LOW,MEDIUM,HIGH,URGENT
}

--

This is a generic ListManager that will work for events and reminders.

import java.util.List;
public interface ListManager<T> {
    public void populateFromBackEnd();
    public void add(T e);
    public void remove(T e);
    public boolean exists(T e);
    public List<T> getAll();
    public void saveAllToBackend();
}

--

public interface EventListManager extends ListManager<Event> {
}

--

public interface ReminderListManager extends ListManager<Reminder>{    
}
  • This seems to be close to Option 4: anemic Event and Reminder objects with a "manager" on top of it handling the list of reminders and the back-end communication. What about the statement that anemic objects are an anti-pattern? – Marc Van Daele Jan 8 '16 at 14:27
  • I don't see how it's anemic. I'm giving you only interfaces that must be implemented. The only anemic part are the managers themselves. Both Event and Reminder should have a lot of business logic implemented in them. For example, ScheduleInfo must get a cron-like string and parse it into dates, repetition patterns etc. Event should raise an exception if a Reminder that doesn't belong to it is added via the callbak, etc. Also Event and Reminder must communicate with a data access leyer for persistence etc. Reminder should find out whether it exists in the backend or only in memory, etc. – Tulains Córdova Jan 8 '16 at 14:45
  • 3
    Don't mistake anemic with separation of concerns and cohesion. An anemic object is one that's basically a DTO, not one that only does what is of its concern. – Tulains Córdova Jan 8 '16 at 14:50
  • 1
    I assumed that the majority of the functions would be simple getters/setters and hence the anemic statement. Two more questions though: 1) Is it correct that both the ListManager and the Reminder class will access the backend? Can't this be avoided? 2) I guess we still need an additional createReminder(Event event) function that a) creates the Reminder and b) adds it to the ReminderListManager. Does it make sense to add this function to the ReminderListManager (as I did in Option 4) or would you create a separate Interface for this? – Marc Van Daele Jan 8 '16 at 15:20
  • 1) Implementors of the interfaces ListManager and Reminder must write the correct implementation to this, for example delegating to a persistence layer or framework. (if by backend you mean database). – Tulains Córdova Jan 8 '16 at 15:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.