(I am using Vuex but the question applies to any Flux architecture.)
I don't have a great understanding of the reasoning behind Flux architecture. I understand that it is nice to have all of the state in one place. I don't have a great understanding of other things though.
My question here is about where, exactly, the mutation logic should be. I get the impression that Flux architecture wants you to do have all of the mutation logic in one place - the store - because it gives you a good separation of concerns. Ie. when you want to know where mutation logic is, you know where to look.
But I find that it is nice to have classes and use instance methods to deal with state change. I think user.computeAndSetKarma()
looks a lot nicer than storeHelper.computeAndSetKarmaForUser(user)
. Robert Martin talks about this in his book Clean Code:
A function with two arguments is harder to understand than a monadic function. For example,
writeField(name)
is easier to understand thanwriteField(output-Stream, name)
. Though the meaning of both is clear, the first glides past the eye, easily depositing its meaning. The second requires a short pause until we learn to ignore the first parameter. And that, of course, eventually results in problems because we should never ignore any part of code. The parts we ignore are where the bugs will hide....
Dyads aren’t evil, and you will certainly have to write them. However, you should be aware that they come at a cost and should take advantage of what mechanims may be available to you to convert them into monads. For example, you might make the
writeField
method a member ofoutputStream
so that you can sayoutputStream.writeField(name)
. Or you might make theoutputStream
a member variable of the current class so that you don’t have to pass it. Or you might extract a new class likeFieldWriter
that takes theoutputStream
in its constructor and has awrite
method.
Consider this example (CodePen):
HTML
<div id="app">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Karma</th>
<th>Compute karma (with logic done in store)</th>
<th>Compute karma (with logic delegated to user)</th>
</tr>
</thead>
<tbody>
<tr v-for="(user, userIndex) in users">
<td>{{ user.name }}</td>
<td>{{ user.karma }}</td>
<td>
<button v-on:click="computeKarmaWithLogicDoneInStore(userIndex)">
Compute
</button>
</td>
<td>
<button v-on:click="computeKarmaWithLogicDelegatedToClass(user)">
Compute
</button>
</td>
</tr>
</tbody>
</table>
</div>
<script src="https://cdn.jsdelivr.net/g/vue@2.0.3,vuex@2.0.0"></script>
JS
class User {
constructor (name) {
this.name = name;
this.karma = 0;
this.posts = [];
this.comments = [];
};
computeAndSetNewKarma() {
let postQualityScore = this._getPostQualityScore();
let commentQualityScore = this._getCommentQualityScore();
let activityScore = this._getActivityScore();
let newKarma = this._getKarma(
postQualityScore,
commentQualityScore,
activityScore
);
this.karma = newKarma;
};
_getPostQualityScore() {
return 5;
};
_getCommentQualityScore() {
return 10;
};
_getActivityScore() {
return 15;
};
_getKarma(postQualityScore, commentQualityScore, activityScore) {
return postQualityScore + commentQualityScore + activityScore;
};
}
const storeHelper = {
getPostQualityScore: function (posts) {
return 5;
},
getCommentQualityScore: function (comments) {
return 10;
},
getActivityScore: function (posts, comments) {
return 15;
},
getKarma: function (postQualityScore, commentQualityScore, activityScore) {
return postQualityScore + commentQualityScore + activityScore;
},
};
const store = new Vuex.Store({
state: {
users: [
new User('Adam'),
new User('Bob'),
],
},
mutations: {
computeKarmaWithLogicDoneInStore: function (state, userIndex) {
let user = state.users[userIndex];
let newPostQualityScore = storeHelper.getPostQualityScore(user.posts);
let newCommentQualityScore = storeHelper.getCommentQualityScore(user.comments);
let newActivityScore = storeHelper.getActivityScore(user.posts, user.comments);
let newKarma = storeHelper.getKarma(
newPostQualityScore,
newCommentQualityScore,
newActivityScore
);
user.karma = newKarma;
},
computeKarmaWithLogicDelegatedToClass: function (state, user) {
user.computeAndSetNewKarma();
},
},
});
new Vue({
store: store,
el: '#app',
computed: {
users: function () {
return this.$store.state.users;
},
},
methods: {
computeKarmaWithLogicDoneInStore: function (userIndex) {
this.$store.commit('computeKarmaWithLogicDoneInStore', userIndex);
},
computeKarmaWithLogicDelegatedToClass: function (user) {
this.$store.commit('computeKarmaWithLogicDelegatedToClass', user);
},
}
});
You could perform the logic inside of the mutation, or you could just call user.computeAndSetNewKarma()
inside of the mutation, delegating the logic to the users instance method. The latter seems preferable to me, but I'm not sure if this violates convention, and I'm not sure if I am thinking about it properly.