I don't spend a lot of time writing code, but I've been trying to steadily improve my ability to build functional prototypes in ROR and I'm looking for suggestions on how to improve my model design skills.
The entire MVC approach is new to me - I first learned to write code before many of you were born, but I haven't had to use it in any work-related responsibilities for 20 years. Any suggested resources would be appreciated.
I feel like I learn best by going through examples with explanations. The Michael Hartl tutorial was a helpful beginning, but I'm having difficulty finding other examples where I can see the functioning app (or a description of the functionality), and a list of all the models - especially with some reasoning behind their decisions.
As an example, my current side project is to make an app I can use with my children to assign and track their chores, and how much allowance I should pay them based on what they've done. Here's a few details on how the app would work...
Chores are assigned to people, and they have a recurring frequency (daily, weekly, monthly,...). Sometimes the person that did the chore is not the person assigned to do it. I want to track all the dates a chore was done, who was assigned to do it, and who actually did it.
In my mind I can see tracking that information as attributes of a chore, or I could have a separate model for chores performed.
How do I make that decision? While I'd like advice on this specific example, I'm really trying to understand more generally. I'm finding that this is the most time consuming and difficult part of each new project I undertake.
Any suggestions on how to learn model design "best practices" would be appreciated.
To which I responded:
Remember that this is less about Ruby and Rails and more about software design as a whole. I recommend Agile Software Development, Principles, Patterns, and Practices for a better understanding of the design process. (That's a shameless affiliate link by the way) There's a great example of building a bowling score tracker that demonstrates how to incrementally design. I'm going to walk you through a similar process, but at a higher level than code. This description is unedited (don't worry, no foul language) and is meant to be a stream of consciousness so you can get a feel for how I design things. Everyone is going to have a different process though, and I expect many here to disagree with my decisions. Here goes nothing:
So in your example, we definitely have people or users. This would be a normal resource endpoint
and we have a list of all chores. This would be another resource endpoint.
we probably have an endpoint that returns all chores assigned to a user
and we may even scope those down by frequency (this is a a little off the beaten REST path, but don't let that deter you. Naming things is hard, and as long as there is consistency and a degree of intuition shown, it's fine)
and a user has a list of chores completed by them
Now that I have some endpoints, I start thinking about how to implement them.
For /users , this is going to be pretty straightforward.
For /chores, I see some ambiguity. Is a chore the action being performed? or a reusable description of a chore? in other words, is it "Bobby clean up his room" or "Clean bedroom. Pick up toys. Put away clothes." The former is simpler, but it means you are going to repeat yourself when you create the "Sally clean up her room" task. The latter is going to be more complex, but you will be able to reuse the Chore description multiple times. The latter is probably the more correct way, but it feels potentially over-engineered for me. When it comes to avoiding repeating myself, I tend to follow a Rule of 3s. If I repeat myself at least 3 times, I do something about it. Until then, I try to go the simple route for the sake of getting things done. (Usually also weigh in on the cost of changing my mind later. In this case, a chore will have a description field. If we decide to go with the reusable descriptions, we could drop the description field and replace it with a task_id, where a Task contains the reusable description)
Anyway, let's assume we don't mind repeating ourself and stick with a chore representing "Clean Bobby's Room" and we'll just create another chore for "Clean Sally's Room". With that assumption, Let's look at types of actions we want to perform:
CREATE: We create a chore (description and frequency) and assign a user to it
READ: We return a chore (description and frequency) along with the assigned_user, the date of completion, the user who completed it.
At this point I start to wonder: How does date of completion and user who completed it work with a recurring chore? If we only care about the last person to complete the chore, then this would be fine. I suspect though, that paying allowance once a week means we want to see each time a daily chore was completed and who completed it. So let's try again:
READ: We return a chore (description and frequency) along with the assigned_user and a list of dates of completion tied to who completed it at that point in time.
This introduces a new model requirement. we need to be able to do something like @chore.completion_events (as I said, naming is hard... there's probably a better name than completion_events) so now we have a new endpoint:
with actions CRUD, where we have a user and a completion date assigned to it. For index, we will probably want to be able to query on a date range, giving us the answer to the question "Who took out the trash in the last 7 days?" and other similar questions./chores/1/completion_events
UPDATE: Since the act of completing a chore is now accomplished by creating a CompletionEvent, we only use update on chores to update the description and the assignee.
DELETE: Pretty straightforward.
As we continue to our other endpoints, it looks like we've answered most questions.
/users/1/assigned_chores will be a list of chores with user_id = 1. I would make this just an Index action, and leave CRUD up to the /chores endpoint.
daily,weekly,monthly} are going to be extra routes pointing to /users/1/assigned_chores and setting a param for "frequency" or something that can be used to filter the query. This will then also be an Index only action. Now that I think about this, these routes seem superfluous. Probably aren't needed.
/users/1/completed_chores is going to be a little complex. We will want to do a lookup on CompletionEvents by user_id, and then for each of those we will want to fetch the associated Chore. We will also want a date range for this most likely so we can answer the question "What chores did Bobby do this week?"
So now, without even really touching Ruby or the underlying implementation, we see that we need 3 models:
with an optional 4th model (depending on a design decision):
Task (a Chore has a task_id so multiple chores can share a description)
And the relationships (ActiveRecord associations):
User has many Chores
User has many CompletionEvents
Chore belongs to Task
Chore belongs to User
Chore has many CompletionEvents
CompletionEvent belongs to User
CompletionEvent belongs to Chore
Task has many Chores
And there you have it. That should be fairly complete (I'm sure you can attach other attributes, like how much to pay them in allowance and stuff like that). From there, implementation is going to mostly be trivial. You'll encounter some tricky implementation details like what @user.completed_chores actually looks like, but often it's just because there are so many good ways to implement it that sometimes we get hung up on finding the great way to implement it. I would focus on just getting things done at that point. Don't worry about whether you should be using a has_many :through with :conditions or whatever. Just get the data that you need. If you notice something is clumsy, look for a way to clean it up. But if it works then don't worry too much about the right way. It is far easier to learn a shortcut if you've done it the long way a couple of times.
I don't claim that this is the best solution, but I do feel it demonstrates a good balance between GSD (getting stuff done) and a well thought-out design. I'm sure there are better designs and I'm sure there are worse designs. In any case, I felt that it was worthy of a post and will be a helpful resource to someone.