Day 10 - Functional Programming 初探 (3) - 实战购物车流程

前言

这两天花了满多心力在介绍 FP 的观念跟方法,但其实大部分都停留在理论,或者教科书上的那种 apple、banana 的练习,大家都是在职场上走跳的,把理论转为实务是必要的能力。

所以,今天我们试着结合前两天讲的东西,今天来写一些实战练习吧!

实战 - 购物车流程

FP 最适合做那种「一连串」、「一个挨着一个」的程序,所以通常是有流程的,先做 A 再做 B 再做 C。

因此,我们来做一个购物车,模拟一个购物的流程,当然电商是很复杂的,我们只实作几个主要的基本功能:

  1. 把商品加入购物车
  2. 商品价格打折
  3. 购物车结帐
  4. 清空购物车

先定义生产线上的资料

虽然已经定义出流程,但现在脑中就是一片空白,到底要从什麽东西开始写啊?

首先要先定义「在生产线上跑」的资料,比如在水管这个生产线,「水」就是我们的资料;在厨房这个生产线,「青菜」就是我们的资料。

我们定义了:

const pipe = (f, g) => (...args) => g(f(...args));

const user = {
  name: 'Joey',
  cart: [],
  purchases: []
};

const item = {
    name: 'TV', 
    price: 24000
};

首先是 pipe,如果忘记他在干嘛,可以参考 Day 9 - Functional Programming 初探 (2),就记得它是用来组合积木、组合函式用的,把里面的所有函式组成一个生产线,而且是由左到右的顺序。

接着是 user,我们定义一个用来代表使用者的物件,里面的 cart 阵列代表购物车,而 purchases 阵列代表购买的项目。

最後是 item,代表我们要购买的物件。

从最小的积木开始

接着就要开始写 FP 啦,FP 的最小单位是 function,而且每个 function 做的事情都尽可能少,专业分工,最後再透过 pipe 组合起来就可以了。

首先来写「把商品加入购物车」:

const addItemToCart = (user, item) => {
    return { ...user, cart: [...user.cart, item] };
};

接着要结帐了,先帮「商品价格打折」:

const applyDiscountToItems = (user) => {
    const discount = 0.8;
    const updatedCart = user.cart.map(item => {
        return {
          name: item.name,
          price: item.price * discount
        }
    });
    return { ...user, cart: updatedCart };
};

然後是「购物车结帐」,其实不会放进阵列就真的结帐开始出货啦,不过那些与後端串接的逻辑就先省略了:

const buyItem = (user) => {
  return { ...user, purchases: [...user.cart] };
};

最後是「清空购物车」:

const emptyUserCart = (user) => {
  return { ...user, cart: [] };
};

把积木组装成生产线

有了上述四个积木,接着开始就是这三天锻链下来的精华了!要来验收罗!

首先,为了避免一开始就越级打怪,我们前面宣告的 pipe 只能够接受 2 个 function 组合,所以我们先从前面 2 个开始:

  1. 把商品加入购物车
  2. 商品价格打折
const pipe = (f, g) => (...args) => g(f(...args));
const user = {
  name: 'Joey',
  cart: [],
  purchases: []
};
const item = {
    name: 'TV', 
    price: 24000
};

const addItemToCart = (user, item) => {
  return { ...user, cart: [...user.cart, item] };
};

const applyDiscountToItems = (user) => {
    const discount = 0.8;
    const updatedCart = user.cart.map(item => {
        return {
          name: item.name,
          price: item.price * discount
        }
    });
    return { ...user, cart: updatedCart };
};

const newUser = pipe(addItemToCart, applyDiscountToItems)(user, item);
console.log(newUser);

执行结果

{
    name: 'Joey',
    cart: [{
        name: 'TV', 
        price: 19200
    }],
    purchases: []
};

真的写起来还是觉得很神奇,透过小 function 的组合出大 function,接着丢参数进去就跑出答案了,是不是很像数学在写 f(x),还有 f(x)。g(x) 的感觉呢?

那现在问题来了,虽然两个 function 成功组合了,但总共有四个啊!

如何组合多个 function

最直觉的想法是再用一次 pipe:

const pipe = (f, g) => (...args) => g(f(...args));

// ... 中间省略,同上一个范例

const buyItem = (user) => {
  return { ...user, purchases: [...user.cart] };
};

const emptyUserCart = (user) => {
  return { ...user, cart: [] };
};

const newUser = pipe(addItemToCart, applyDiscountToItems)(user, item);
const newUser2 = pipe(buyItem, emptyUserCart)(newUser);

console.log(newUser2);

执行结果

{
    name: 'Joey',
    cart: [],
    purchases: [{
        name: 'TV', 
        price: 19200
    }]
};

嗯。。。虽然答案正确,但看起来很怪对不对?

好像明明只要一条生产线一次做完的事情,却硬是切成两条生产线,等第一条完成接着做第二条,非常多此一举。

接受多个 function 的 pipe

所以我们试着改良一下生产线 pipe:

const pipe = (f, g) => (...args) => g(f(...args));
const newPipe = (...fns) => fns.reduce(pipe);

// 上面是比较好读的版本,你也可以合并成一个:
const pipe = (...fns) => fns.reduce((f, g) => (...args) => g(f(...args)));

可以看到 newPipe 的部分,它是一个 function,参数 ...fns 代表可以带多个参数(逗点分开)进去,fns 进到函式内之後就会变成一个阵列(里面存着你丢进来的多个参数)。

还记得 reduce 代表什麽吗?它负责「整合」,目的是要把我们带进去的多个参数整合,变成一个统一的生产线。

因此做完上述的改良,我们从原本只能组合 2 个 function,变成你带几个 function 进来,全都给你组成一条生产线。

最终版本

所以我们的最终版本就出来了:

const pipe = (f, g) => (...args) => g(f(...args));
const newPipe = (...fns) => fns.reduce(pipe);
const user = {
  name: 'Joey',
  cart: [],
  purchases: []
};
const item = {
    name: 'TV', 
    price: 24000
};

const addItemToCart = (user, item) => {
  return { ...user, cart: [...user.cart, item] };
};

const applyDiscountToItems = (user) => {
    const discount = 0.8;
    const updatedCart = user.cart.map(item => {
        return {
          name: item.name,
          price: item.price * discount
        }
    });
    return { ...user, cart: updatedCart };
};

const buyItem = (user) => {
  return { ...user, purchases: [...user.cart] };
};

const emptyUserCart = (user) => {
  return { ...user, cart: [] };
};

const newUser = newPipe(
    addItemToCart, 
    applyDiscountToItems, 
    buyItem, 
    emptyUserCart
)(user, item);

console.log(newUser);

执行结果

{
    name: 'Joey',
    cart: [],
    purchases: [{
        name: 'TV', 
        price: 19200
    }]
};

此时 FP 的好处立刻就出现了!

假如今天突然不想打折了,可以把 applyDiscountToItems 直接抽掉,程序立刻就按照你的意图下去执行。

或者清空购物车之後还想做其它的事(比如返回首页),就可以自己再写个积木,然後放到 emptyUserCart 的後面即可,完全不用动到前面写的函式们。

注意哦!这只是我们组出来的其中一种 pipe 而已,只要你手上有积木(function),而且每个积木都专业分工,就可以用各种方式去组合它,有无限多可能啊!

感觉像乐高广告

结语

FP 不是 Javascript 专属的东西,而且已经非常有历史了,因此网路上累积的学习资源非常丰富,这边只用三天来讨论完全是冰山一角,但我想足以让完全没概念的新手,有个初步对於 FP 的想像与理解。

我相信应该还是很多人看完通篇,仍然不知道 FP 在干嘛,就跟我第一次学的时候一样QQ,欢迎留言写下你的疑惑,也许也是其它人的疑惑~

我相信学习不会有白费的过程,而是循序渐进的,即便不能一次读懂,下次再读到 FP 时,也就更有底气了!

在寂静与黑暗之中
闭上眼
已到了虫洞外的新世界

参考资料

Will - 理解函式编程核心概念与如何进行 JavaScript 函式编程
Les Lee - Functional Programming 一文到底全纪录


<<:  Day 21 - 研习计画之结案发表与业师心得篇

>>:  Android Studio初学笔记-Day10-RadioButton

【Day29】iOS相关分享

其实在github上面找 awesome 啥的可能就很容易找到相关的资料整理 另外,知乎上面的问题&...

【Day 10】 讨论 Data Analytics Pipeline - Google Analytics on AWS (整体)

大家好~ 昨天我们已经成功拉取 Google Analytics 资料到 AWS,可是我们发现『抓取...

常见网路问题(一):为什麽明明连上了 Wi-Fi,却还是上不了网?

举凡无法上网、网页出现 404、静态 IP 及浮动 IP 是什麽的等等问题,都是很常见的网路相关问题...

[Day 4] Course 1_Foundation - 分析思维(Thinking analytically)

《30天带你上完 Google Data Analytics Certificate 课程》系列将...

帮 Line Bot 加上身份验证(1)

昨天我们让 Line Bot 可以成功回应使用者验证码了,但是这样只要任何人加入 Line Bot ...