8

Consider typical gym trainings tracker app.

User has account related attributes:

User {
  id
  login
  password
  email
  fname, lname
  isBlocked
}

However, the requirements are that an application's user manages his trainings, trainings history, achievements, profile, etc. All of those entities should be somehow linked with user account.

How do I link it with an account? What is the common way to do it and its pros/cons?


I can imagine two scenarios:

Possibility 1: Making User a large 'god' object:

User {
  id
  login
  password
  email
  fname, lname
  isBlocked

  trainings        # one to many
  training_history # one to one
  achievements     # one to many
  /** possibly many more relations */
}

Possiblity 2: Link User with UserProfile, and then UserProfile holds all the relations.

User {
  id
  login
  password
  email
  fname, lname
  isBlocked

  user_profile     # one to one
}

UserProfile {
  user_id          # one to one

  trainings        # one to many
  training_history # one to one
  achievements     # one to many
  /** possibly many more relations */
}

Is the second option really better than the first one? Can I do better?

5 Answers 5

4

The second approach is the way to go. It has the significant advantage of separation of concerns:

  • User is related to the identity of the user. One step more would be to separate identity (name, address) from the authentication (password), which are related but different concerns.
  • UserProfile is related to the activity of the user. I'd, by the way rename it UserActivity because "profile" is colloquially used to refer to the account, or to authorisations, which is confusing here.

It has also the advantage of being in line with the principle of single responsibility, which says that a class should have only on reason to change:

  • How to manage identity would be the only reason to change User.
  • How to access user activity would be the only reason to change UserActivity
2
  • 1
    If we follow SRP, why did not we split UserProfile further?
    – Basilevs
    Jan 11, 2021 at 3:49
  • @Basilevs Because, as explained by Uncle Bob himself, SRP is not about doing one thing but about having one reason to change. This being said, I already suggested to split User identity and user authentication, and perhaps I missed a similar subtlety in UserActivity: could you be more specific about what exactly you’d split further?
    – Christophe
    Jan 11, 2021 at 7:34
3

There is a third option you haven't considered. The entities are directly linked to the user. E.g. every achievement is in fact an object with a field userID containing the ID of the user the achievement belongs to.

Achievement {
   id
   kind
   ... many more fields ...
   userID
}

For achievements that makes much more sense than storing them as static objects and just link to them. Consider the case that an achievement has to be changed in the future for some reason. If you do so, it will change for all users that have got this achievement in the past, which is incorrect as the new achievement is not the achievement those users actually did master. Instead of creating a copy of a static achievement object that is bound to the user makes more sense as this copy would never change.

Downside:

  • Requires more storage on disk and possibly more RAM during processing.
  • Will not work well if you want an entity to change for all users that changes.

Other than that I only see benefits.

3
  • Is not this exactly how one-to-many relationship is modeled by default in relational databases? This is exactly the first approach OP suggests. +1. IMO, data abstraction is not a design goal for DB, so other answers are annoying.
    – Basilevs
    Jan 11, 2021 at 10:21
  • @Basilevs No, the first approach is that the user object contains references to the achievements. My approach is that the achievements contain references to the user object.
    – Mecki
    Jan 11, 2021 at 11:51
  • @Mecki it is exactly how it would be modelled in a relational db - the user id in the achievement table being a foreign key linking to the primary key of the user table. Which gives you the object model from the first example. If you changed userId to userProfileId you'd get the second model - assuming everything is modelled as a table in a relational database. There are actually two distinct questions here - how to structure the data in the data store and the second is how to model the data in the app. The choice of data store is actually the fun bit...
    – Murph
    Jan 12, 2021 at 9:02
1

You are on the right track (pun intended) with creating additional abstractions to handle things like their training history. The challenge is naming things appropriately so that the class's responsibility, or reasons to change, do not expand too much.

The term "user profile" is vague. If the purpose of the class is too track training progress and achievements, what do workers at the gym call this thing? A "member"?

The single responsibility principal is a good guideline here. Keep all the things that change for the same reason together in one class. If something changes for a different reason, put it in a different class.

1

The object

User {
  id
  login
  password
  email
  fname, lname
  isBlocked
}

is a Data Transfer Object.

The object

User {
  id
  login
  password
  email
  fname, lname
  isBlocked

  trainings        # one to many
  training_history # one to one
  achievements     # one to many
  /** possibly many more relations */
}

is an Aggregate.*

Which is better? As always, the answer to that question is "it depends."

Aggregates are commonly used when you want to "chunk" your data; i.e. it is either a View Model which contains all of the data needed to materialize a view, or the result of a call to, say, a JSON endpoint. Chunking the data relieves the program from having to take the overhead of making multiple calls to the endpoint to retrieve the data.

Data transfer objects are used when you're working with individual entities and don't need the related data to complete your desired operations.

From a data store perspective, you will always have individual tables representing entities, and primary and foreign keys that tie them together. You cannot get normalization otherwise. That's where your "Possibility 2" comes in.

*A collection of objects that are bound together by a root entity, otherwise known as an aggregate root.

3
  • 1
    A data structure is never a "proper" object. I don't know what you mean by "aggregate", but that is still a data structure, therefore not a "proper" object either. Both are fine for a database table (I think the question may be related to databases), unacceptable as objects though. Jan 9, 2021 at 19:09
  • 2
    @RobertBräutigam: You must be one of those folks who believes that objects are not "proper" objects unless they contain behavior. I can assure you that's not the case. Objects that only contain data are a perfectly valid technique that is used all the time. See en.wikipedia.org/wiki/Data_transfer_object Jan 9, 2021 at 21:48
  • 1
    I've made some edits. Hopefully that clarifies. Jan 9, 2021 at 21:53
1

Can you do better? Absolutely!

But without any information regarding the access patterns, rules, anticipated load, etc. I cannot help you with the above. There is zero difference between the two options you listed for any practical application without more information.

You have simply arbitrarily denoted two different ways of slicing up your data. Of course there are many more ways you could organize that data as well. For example, you could further partition each piece of your UserProfile into even smaller units/collections. And for each of those smaller units there are likely different possibilities regarding how they could be organized. Though spending the energy enumerating all of these possibilities is a tremendous waste of time. It will not lead you to a satisfactory answer.

As you may have surmised already, it is meaningless (and frankly dangerous) to design a system by only focusing on the data. Save that exercise for when you want to fully normalize your database structure (though access patterns are often relevant when doing this as well). If you want do this “correctly”, start by designing the behavior of your system. In doing so you will find that the appropriate boundaries will present themselves. A domain model is discovered!

The “best” design is the one that meets your goals right? Well, what are your goals? Start there and work backwards.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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