基于纯函数实现应用逻辑
根据目前我们掌握的不可变状态树和相关操作,我们可以尝试实现投票应用的逻辑。应用的核心逻辑我们拆分成:状态树结构和生成新状态树的函数集合。
加载条目
首先,之前说到,应用允许“加载”一个用来投票的条目集。我们需要一个 setEntries
函数,它用来提供应用的初始化状态:
//test/core_spec.js
import {List, Map} from 'immutable';
import {expect} from 'chai';
import {setEntries} from '../src/core';
describe('application logic', () => {
describe('setEntries', () => {
it('adds the entries to the state', () => {
const state = Map();
const entries = List.of('Trainspotting', '28 Days Later');
const nextState = setEntries(state, entries);
expect(nextState).to.equal(Map({
entries: List.of('Trainspotting', '28 Days Later')
}));
});
});
});
我们目前 setEntries
函数的第一版非常简单:在状态 map 中创建一个 entries
键,并设置给定的条目 List。
//src/core.js
export function setEntries(state, entries) {
return state.set('entries', entries);
}
为了方便起见,我们允许函数第二个参数接受一个原生 js 数组(或支持 iterable 的类型),但在状态树中它应该是一个 Immutable List:
//test/core_spec.js
it('converts to immutable', () => {
const state = Map();
const entries = ['Trainspotting', '28 Days Later'];
const nextState = setEntries(state, entries);
expect(nextState).to.equal(Map({
entries: List.of('Trainspotting', '28 Days Later')
}));
});
为了达到要求,我们需要修改一下代码:
//src/core.js
import {List} from 'immutable';
export function setEntries(state, entries) {
return state.set('entries', List(entries));
}
开始投票
当 state 加载了条目集合后,我们可以调用一个 next
函数来开始投票。这表示,我们到了之前设计的状态树的第二阶段。
next
函数需要在状态树创建中一个投票 map,该 map 有拥有一个 pair
键,值为投票条目中的前两个元素。
这两个元素一旦确定,就要从之前的条目列表中清除:
//test/core_spec.js
import {List, Map} from 'immutable';
import {expect} from 'chai';
import {setEntries, next} from '../src/core';
describe('application logic', () => {
// ..
describe('next', () => {
it('takes the next two entries under vote', () => {
const state = Map({
entries: List.of('Trainspotting', '28 Days Later', 'Sunshine')
});
const nextState = next(state);
expect(nextState).to.equal(Map({
vote: Map({
pair: List.of('Trainspotting', '28 Days Later')
}),
entries: List.of('Sunshine')
}));
});
});
});
next
函数实现如下:
//src/core.js
import {List, Map} from 'immutable';
// ...
export function next(state) {
const entries = state.get('entries');
return state.merge({
vote: Map({pair: entries.take(2)}),
entries: entries.skip(2)
});
}
投票
当用户产生投票行为后,每当用户给某个条目投了一票后, vote
将会为这个条目添加 tally
信息,如果对应的
条目信息已存在,则需要则增:
//test/core_spec.js
import {List, Map} from 'immutable';
import {expect} from 'chai';
import {setEntries, next, vote} from '../src/core';
describe('application logic', () => {
// ...
describe('vote', () => {
it('creates a tally for the voted entry', () => {
const state = Map({
vote: Map({
pair: List.of('Trainspotting', '28 Days Later')
}),
entries: List()
});
const nextState = vote(state, 'Trainspotting');
expect(nextState).to.equal(Map({
vote: Map({
pair: List.of('Trainspotting', '28 Days Later'),
tally: Map({
'Trainspotting': 1
})
}),
entries: List()
}));
});
it('adds to existing tally for the voted entry', () => {
const state = Map({
vote: Map({
pair: List.of('Trainspotting', '28 Days Later'),
tally: Map({
'Trainspotting': 3,
'28 Days Later': 2
})
}),
entries: List()
});
const nextState = vote(state, 'Trainspotting');
expect(nextState).to.equal(Map({
vote: Map({
pair: List.of('Trainspotting', '28 Days Later'),
tally: Map({
'Trainspotting': 4,
'28 Days Later': 2
})
}),
entries: List()
}));
});
});
});
为了让上面的测试项通过,我们可以如下实现 vote
函数:
//src/core.js
export function vote(state, entry) {
return state.updateIn(
['vote', 'tally', entry],
0,
tally => tally + 1
);
}
updateIn 让我们更容易完成目标。
它接受的第一个参数是个表达式,含义是“定位到嵌套数据结构的指定位置,路径为:[‘vote’, ‘tally’, ‘Trainspotting’]”,
并且执行后面逻辑:如果路径指定的位置不存在,则创建新的映射对,并初始化为 0,否则对应值加 1。
可能对你来说上面的语法太过于晦涩,但一旦你掌握了它,你将会发现用起来非常的酸爽,所以花一些时间学习并
适应它是非常值得的。
继续投票
每次完成一次二选一投票,用户将进入到第二轮投票,每次得票最高的选项将被保存并添加回条目集合。我们需要添加
这个逻辑到 next
函数中:
//test/core_spec.js
describe('next', () => {
// ...
it('puts winner of current vote back to entries', () => {
const state = Map({
vote: Map({
pair: List.of('Trainspotting', '28 Days Later'),
tally: Map({
'Trainspotting': 4,
'28 Days Later': 2
})
}),
entries: List.of('Sunshine', 'Millions', '127 Hours')
});
const nextState = next(state);
expect(nextState).to.equal(Map({
vote: Map({
pair: List.of('Sunshine', 'Millions')
}),
entries: List.of('127 Hours', 'Trainspotting')
}));
});
it('puts both from tied vote back to entries', () => {
const state = Map({
vote: Map({
pair: List.of('Trainspotting', '28 Days Later'),
tally: Map({
'Trainspotting': 3,
'28 Days Later': 3
})
}),
entries: List.of('Sunshine', 'Millions', '127 Hours')
});
const nextState = next(state);
expect(nextState).to.equal(Map({
vote: Map({
pair: List.of('Sunshine', 'Millions')
}),
entries: List.of('127 Hours', 'Trainspotting', '28 Days Later')
}));
});
});
我们需要一个 getWinners
函数来帮我们选择谁是赢家:
//src/core.js
function getWinners(vote) {
if (!vote) return [];
const [a, b] = vote.get('pair');
const aVotes = vote.getIn(['tally', a], 0);
const bVotes = vote.getIn(['tally', b], 0);
if (aVotes > bVotes) return [a];
else if (aVotes < bVotes) return [b];
else return [a, b];
}
export function next(state) {
const entries = state.get('entries')
.concat(getWinners(state.get('vote')));
return state.merge({
vote: Map({pair: entries.take(2)}),
entries: entries.skip(2)
});
}
投票结束
当投票项只剩一个时,投票结束:
//test/core_spec.js
describe('next', () => {
// ...
it('marks winner when just one entry left', () => {
const state = Map({
vote: Map({
pair: List.of('Trainspotting', '28 Days Later'),
tally: Map({
'Trainspotting': 4,
'28 Days Later': 2
})
}),
entries: List()
});
const nextState = next(state);
expect(nextState).to.equal(Map({
winner: 'Trainspotting'
}));
});
});
我们需要在 next
函数中增加一个条件分支,用来匹配上面的逻辑:
//src/core.js
export function next(state) {
const entries = state.get('entries')
.concat(getWinners(state.get('vote')));
if (entries.size === 1) {
return state.remove('vote')
.remove('entries')
.set('winner', entries.first());
} else {
return state.merge({
vote: Map({pair: entries.take(2)}),
entries: entries.skip(2)
});
}
}
我们可以直接返回 Map({winner: entries.first()})
,但我们还是基于旧的状态数据进行一步一步的
操作最终得到结果,这么做是为将来做打算。因为应用将来可能还会有很多其它状态数据在 Map 中,这是一个写测试项的好习惯。
所以我们以后要记住,不要重新创建一个状态数据,而是从旧的状态数据中生成新的状态实例。
到此为止我们已经有了一套可以接受的应用核心逻辑实现,表现形式为几个独立的函数。我们也有针对这些函数的
测试代码,这些测试项很容易写:No setup, no mocks, no stubs。这就是纯函数的魅力,我们只需要调用它们,
并检查返回值就行了。
提醒一下,我们目前还没有安装 redux 哦,我们就已经可以专注于应用自身的逻辑本身进行实现,而不被所谓的框架
所干扰。这真的很不错,对吧?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论