返回介绍

基于纯函数实现应用逻辑

发布于 2025-02-17 12:51:31 字数 8179 浏览 0 评论 0 收藏 0

根据目前我们掌握的不可变状态树和相关操作,我们可以尝试实现投票应用的逻辑。应用的核心逻辑我们拆分成:状态树结构和生成新状态树的函数集合。

加载条目

首先,之前说到,应用允许“加载”一个用来投票的条目集。我们需要一个 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文