mikeysee
mikeysee

Reputation: 1763

Separating Data From Logic

Say I have a User object that has an "xp" value. Now say that I have an XPLevelsModel which contains an array of XPLevels that define how many points are required for that level (among other things):

class User
{       
    xp : int;
}

class XPLevelsModel
{
    levels : XPLevel;
}

class XPLevel
{
    ...
    xpRequired : int;
    name : String;
    ...
}

Okay so now say I have a number of methods such as "getCurrentLevel()" "getPercentProgressThroughLevel()" etc. Where should those methods live?

I could put them on the User object:

class User
{
    constructor(levelsModel:XPLevelsModel) : {}

    xp : int;

    getCurrentLevel() : int {}
    getPercentProgressThroughLevel() : Number {}
    ...
}

but as we add more data such as "lives", "health" etc etc User will start to grow with lots of functions (not very good for SoC)

I could try to group these things up into components such as "XPComponent" which is an object on User:

class User
{
    xp : UserXPComponent;
}

class UserXPComponent
{
    constructor(levelsModel:XPLevelsModel) {}
    points : int;

    getCurrentLevel() : int {}
    getPercentProgressThroughLevel() : Number {}
    ...
}

but the problem is accessing this data from the rest of the game now means user.xp.getCurrentLevel(), which breaks the law of Demeter, plus its ugly, hard to test and then what happens if you go more levels deep?

There is one more option I thought of, to have something like:

class User
{
    xp : int 
}

class UserXPHelpers
{
    constructor(levelsModel:XPLevelsModel) {}

    getCurrentLevel(user:User) : int {}
    getPercentProgressThroughLevel(user:User) : Number {}
}

Here we have separated out the logic and the data and pass the user through the methods. The only thing is, does this not break encapsulation? As all those methods rely upon a user to be passed to them, shouldn't they live with the user?

If this IS the correct solution, is "Helpers" the correct name for this?

I hope you can help a problem that has challenged me time and time again and have only now been able to articulate it into a query.

Mike

Upvotes: 2

Views: 489

Answers (1)

neleus
neleus

Reputation: 2280

All three cases can be used where the better choice depends on the cohesion. In other words it depends on how the xp is related to User (whether it is attribute or an aggregated domain entity or even it belongs to other domain). Other values such as lives, health etc can have different solutions. So you don't have to apply the same approach to all your stuff, moreover, Im sure they play different role in your domain model.

1. High cohesion (attribute), the value is part of the parent object structure where the parent can't exist without it. Then it should be a field (attribute) and/or a set of methods as in your first example.

Nevertheless it looks like this is not true about the xp.

2. Aggregated entity, the value is highly connected to the parent (aggregate root) but can be used separately. This is your second example of XPComponent. So you can decide if the XPComponent is Aggregated entity of your domain or not. I don't see here big troubles with the law of Demeter because it makes sense on higher levels of system architecture, see also.

3. Separate entity. It has only a reference to the parent. This is not a case of xp.

4. Service function. So it is neither an entity nor a part of Domain model. In this case you can use your Helpers solution which is actually a Domain Service.

Upvotes: 2

Related Questions