Domanda

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?

È stato utile?

Soluzione

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

Altri suggerimenti

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.

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.

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.

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.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
scroll top