[實作筆記] Storybook CI 使用 Github Actions

前情提要

前一篇文章中,
我們使用 TypeScript 開發 React Components ,
並使用 Storybook 作為測試的工具。

這篇會介紹如何與 chromatic 作結合,讓 CI/CD 運行時(本文將使用 Github Actions 作為 CI Server),
自動部署到 chromatic,同時提供自動化測試與人工審核的功能。

環境設置

使用 Github 登入 Chromatic,
雖然 Chromatic 也有提供 Bitbucket 與 GitLab 的登入方式,
但並不確定這些 CI Server 包含 Jenkins、TravisCI 或是 CircleCI 實際上怎麼結合 Storybook,
以下都以 Github 作介紹,

本機環境

安裝 chromatic

1
yarn add -D chromatic

發佈 Storybook 到 Chromatic 上

1
yarn chromatic --project-token=<project-token>

發佈完成你可以得到一個網址 https://www.chromatic.com/builds?appId=random
你可以分享網址給同事,對 UI 進行審查.
讓 Pull Request 時,自動執行的設定

雲端設定

新增專案後,可以取得 Token

新增 Project
取得 Token

在專案中設定 yaml 檔(Github Actions)
加上 .github/workflows/chromatic.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# .github/workflows/chromatic.yml
# name of our action
name: 'Chromatic Deployment'
# the event that will trigger the action
on:
# Trigger the workflow on push or pull request,
# but only for the main branch
push:
branches:
- main
# what the action will do
jobs:
test:
# the operating system it will run on
runs-on: ubuntu-latest
# the list of steps that the action will go through
steps:
- uses: actions/[email protected]
- run: cd src/marsen.react && yarn && yarn build && yarn build-storybook
- uses: chromaui/[email protected]
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}
storybookBuildDir: storybook-static

特殊設定,子專案

如何你和我一樣, 專案是由多個子專案組成,
那麼預設的 yaml 設定可能就不適合你.
可以參考這個 issue,
其中要特別感謝 yigityuce 的 solution,
我特別 fork 到我的 Github 帳號底下 Repo
設定調整如下:

1
2
3
4
5
6
7
8
9
# 上略
steps:
- uses: actions/[email protected]
- run: cd src/marsen.react && yarn && yarn build && yarn build-storybook
- uses: marsen/[email protected]
with:
workingDir: ./src/marsen.react
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
# 下略

驗收

如下圖, 左方會顯示舊版的 UI 畫面, 右方會顯示新版的 UI 畫面,
如果開啟 Diff 功能(右上角的眼鏡圖示),
即可以進行差異比對, 有差異的地方將以亮綠色顯示,
如果認同這次的變更, 選擇右上角的 Accept 反之, 選擇 Deny.
驗收

參考

(fin)

[實作筆記] React 與 Storybook 開發

前情提要

不論是 App 或是 Web, 與使用者第一線互動的就是 UI 了。
另一面在需求設計上, 我們總會想像一個畫面,
想像著使用者如何使用我們的產品,
也就是說 UI 是理想與真實的邊界。

Designer 完成了設計, Engineer 將之實作出來,
主流的開發方式會透過 Component 來節省時間。

為什麼我們需要 Storybook ?

但是真的能節省時間嗎 ?

開發人員彼此之間會不會重複造輪子? 他們又要怎麼溝通?
修改到底層元件會不會影響到上層元件? 會不會改 A 壞 B?
複雜的 Component, 特殊的情境如何測試 ?

Storybook 恰恰能解決這些問題,

  • 作為開發人員的指南和文件
  • 獨立於應用程式建立 Component
  • 測試特殊情境

對我來說,最重要的事,我可以用類似 TDD 的方式開發,
在 Storybook 的官方文件提到這個方法為 CDD.
在 TDD 中我們把一個個 Use Case 寫成 Test Case,
我們可以挪用這個觀念,
在 Storybook 中把每一個 Component 的各種狀態(State),
當作 Use Case, 然後透過 Mock State 讓 Component 呈現該有的樣貌。

心得

大前端的時代,僅僅只看 Web 的話,
我認為這個時代前端的重心就在兩個主要的技術之上,
Component 與 State Management。
而實作你可以有以下的選擇,
僅介紹我聽過的主流 Library,
Component 與 State Management 沒有絕對的搭配關係。

Component State Management
React Flux
Angular Redux
Vue Akita

改編 Storybook 教程

為什麼要改編 Storybook教程(React Version) ?

這個教程會以一個簡單的 Todo List,
從創建應用程式、簡單的 Component 到複雜,
與狀態管理器介接, 測試到部署。

但是他缺了一味,TypeScript,
所以我自已用 TypeScript 進行了改寫並稍作一下記錄。

環境

開始

設定初始化的環境

設定 React Storybook

開啟命令提示視窗,執行以下命令以創建 React App

1
2
3
4
# Create our application:
npx create-react-app taskbox

cd taskbox

安裝 Storybook

1
2
3
4
npm i storybook

# Add Storybook:
npx -p @storybook/cli sb init

啟動開發環境的 Storybook,

1
2
# Start the component explorer on port 6006:
yarn storybook

測試與執行

1
2
3
4
5
# Run the test runner (Jest) in a terminal:
yarn test --watchAll

# Run the frontend app proper on port 3000:
yarn start

npm Storybook

下載 CSS,存檔至 src/index.css

安裝 degit

1
npm i degit

加入 Add assets (字型與Icon)

1
2
npx degit chromaui/learnstorybook-code/src/assets/font src/assets/font
npx degit chromaui/learnstorybook-code/src/assets/icon src/assets/icon

Git Commit

1
2
> git add .
> git commit -m "first commit"

簡單的 component

src/components/ 資料夾建立 component Task.js

1
2
3
4
5
6
7
8
9
10
11
// src/components/Task.js

import React from 'react';

export default function Task({ task: { id, title, state }, onArchiveTask, onPinTask }) {
return (
<div className="list-item">
<input type="text" value={title} readOnly={true} />
</div>
);
}

建立 Task.stories.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// src/components/Task.stories.js

import React from 'react';
import Task from './Task';

export default {
component: Task,
title: 'Task',
};

const Template = args => <Task {...args} />;

export const Default = Template.bind({});
Default.args = {
task: {
id: '1',
title: 'Test Task',
state: 'TASK_INBOX',
updatedAt: new Date(2018, 0, 1, 9, 0),
},
};

export const Pinned = Template.bind({});
Pinned.args = {
task: {
...Default.args.task,
state: 'TASK_PINNED',
},
};

export const Archived = Template.bind({});
Archived.args = {
task: {
...Default.args.task,
state: 'TASK_ARCHIVED',
},
};

隨時你都可以執行 yarn storybook 試跑來看看 storybook
調整 Storybook 的 config 檔 (.storybook/main.js)

1
2
3
4
5
6
7
8
9
10
// .storybook/main.js

module.exports = {
stories: ['../src/components/**/*.stories.js'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/preset-create-react-app',
],
};

(.storybook/preview.js) 這設定為了 log UI 上的某些操作產生的事件,
在之後我們會看到 完成(onArchiveTask)或置頂(onPinTask) 兩個事件

1
2
3
4
5
6
7
8
// .storybook/preview.js

import '../src/index.css';

// Configures Storybook to log the actions(onArchiveTask and onPinTask) in the UI.
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
};

調整 Task.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// src/components/Task.js

import React from 'react';

export default function Task({ task: { id, title, state }, onArchiveTask, onPinTask }) {
return (
<div className={`list-item ${state}`}>
<label className="checkbox">
<input
type="checkbox"
defaultChecked={state === 'TASK_ARCHIVED'}
disabled={true}
name="checked"
/>
<span className="checkbox-custom" onClick={() => onArchiveTask(id)} />
</label>
<div className="title">
<input type="text" value={title} readOnly={true} placeholder="Input title" />
</div>

<div className="actions" onClick={event => event.stopPropagation()}>
{state !== 'TASK_ARCHIVED' && (
// eslint-disable-next-line jsx-a11y/anchor-is-valid
<a onClick={() => onPinTask(id)}>
<span className={`icon-star`} />
</a>
)}
</div>
</div>
);
}

加入測試用的外掛(add on)

1
yarn add -D @storybook/addon-storyshots react-test-renderer

執行測試

1
> yarn test

測試結果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
yarn run v1.22.0
$ react-scripts test
(node:52888) DeprecationWarning: \`storyFn\` is deprecated and will be removed in Storybook 7.0.

https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#deprecated-storyfn
PASS src/components/storybook.test.js (14.703 s)
Storyshots
Task
√ Default (13 ms)
√ Pinned (2 ms)
√ Archived (1 ms)

› 3 snapshots written.
Snapshot Summary
› 3 snapshots written from 1 test suite.

Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 3 written, 3 total
Time: 16.716 s
Ran all test suites related to changed files.

簡單的 component 改用 typescript

首先,Task.js 調整副檔名為 Task.tsx
Task.stories.jsTask.stories.tsx.
測試檔案 storybook.test.js 也一併修改 storybook.test.ts

並修改 .storybook/main.js

1
2
3
4
module.exports = {
stories: ['../src/components/**/*.stories.tsx'],
/// 略…
};

建立 tsconfig.json

1
> tsc --init

用 TypeScript 改寫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// src/components/Task.tsx
import React from 'react';

export enum TaskState{
Inbox = 'TASK_INBOX',
Pinned = 'TASK_PINNED',
Archived = 'TASK_ARCHIVED'
}

export interface TaskArgs {
item:TaskItem,
onArchiveTask: (id:string) => void,
onPinTask: (id:string) => void
}

export class TaskItem{
id: string = ''
title: string = ''
state: TaskState = TaskState.Inbox
updatedAt?: Date
}

export default function Task(args:TaskArgs) {
return (
<div className={`list-item ${args.item.state}`}>
<label className="checkbox">
<input
type="checkbox"
defaultChecked={args.item.state === TaskState.Archived}
disabled={true}
name="checked"
/>
<span className="checkbox-custom" onClick={()=>args.onArchiveTask(args.item.id)} />
</label>
<div className="title">
<input type="text" value={args.item.title} readOnly={true} placeholder="Input title" />
</div>

<div className="actions" onClick={event => event.stopPropagation()}>
{args.item.state !== TaskState.Archived && (
// eslint-disable-next-line jsx-a11y/anchor-is-valid
<a onClick={()=>args.onPinTask(args.item.id)}>
<span className={`icon-star`} />
</a>
)}
</div>
</div>
);
}

改寫 Task.store.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// src/components/Task.stories.tsx

import React from 'react';
import Task, { TaskItem, TaskArgs, TaskState } from './Task';
import { Story } from '@storybook/react/types-6-0';

export default {
component: Task,
title: 'Task',
};

const Template:Story<TaskArgs> = args => <Task {...args} />;

var defaultItem:TaskItem = {
id:'1',
title:'Test Task',
state:TaskState.Inbox,
updatedAt: new Date(2018, 0, 1, 9, 0),
};

export const Default = Template.bind({});
Default.args = { item: defaultItem, };

export const Pinned = Template.bind({});
var pinnedItem = Copy(defaultItem);
pinnedItem.state=TaskState.Pinned
Pinned.args = { item: pinnedItem };

export const Archived = Template.bind({});
var archivedItem = Copy(defaultItem);
archivedItem.state=TaskState.Archived;
Archived.args = {item: archivedItem};

function Copy(obj:any) {
return Object.assign({},obj);
}

組合成複雜的 component (TypeScript版本)

與教程最主要的不同之處在於使用了 TypeScript 的語法撰寫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
 // src/components/TaskList.tsx
import React from 'react';
import Task, { TaskItem, TaskState } from './Task';
import { connect } from 'react-redux';
//import { archiveTask, pinTask } from '../lib/redux';

export interface TaskListProps {
loading?:boolean;
tasks: TaskItem[];
onArchiveTask: (id:string)=>void;
onPinTask: (id:string)=>void;
}

export function PureTaskList(props:TaskListProps) {
const events = {
onArchiveTask:props.onArchiveTask,
onPinTask:props.onPinTask,
};

const LoadingRow = (
<div className="loading-item">
<span className="glow-checkbox" />
<span className="glow-text">
<span>Loading</span> <span>cool</span> <span>state</span>
</span>
</div>
);

if (props.loading) {
return (
<div className="list-items">
{LoadingRow}
{LoadingRow}
{LoadingRow}
{LoadingRow}
{LoadingRow}
{LoadingRow}
</div>
);
}

if (props.tasks === undefined || props.tasks.length === 0) {
return (
<div className="list-items">
<div className="wrapper-message">
<span className="icon-check" />
<div className="title-message">You have no tasks</div>
<div className="subtitle-message">Sit back and relax</div>
</div>
</div>
);
}

const tasksInOrder = [
...props.tasks.filter(t => t.state === TaskState.Pinned), //< ==== 固定頂部
...props.tasks.filter(t => t.state !== TaskState.Pinned),
];


return (
<div className="list-items">
{tasksInOrder.map(item => (
<Task key={item.id} item={item} {...events}/>
))}
</div>
);
}

export default connect(
(props:TaskListProps) => ({
tasks: props.tasks.filter(t => t.state === TaskState.Inbox || t.state === TaskState.Pinned ),
})
)(PureTaskList);

TaskList.stories.tsx 設置,也是使用 TypeScript 撰寫。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// src/components/TaskList.stories.tsx

import React from 'react';
import { TaskList, TaskListArgs } from './TaskList';
import { TaskItem, TaskState } from './Task';
import { Story } from '@storybook/react/types-6-0';

export default {
component: TaskList,
title: 'TaskList',
decorators: [(story: () => React.ReactNode) => <div style={{ padding: '3rem' }}>{story()}</div>],
excludeStories: /.*Data$/,
};

const Template:Story<TaskListArgs> = args => <TaskList {...args} />

var defaultItem:TaskItem = {
id:'1',
title:'Test Task',
state:TaskState.Inbox,
updatedAt: new Date(2018, 0, 1, 9, 0)
};

export const Default = Template.bind({});
Default.args = {
tasks: [
{ ...defaultItem, id: '1', title: 'Task 1' },
{ ...defaultItem, id: '2', title: 'Task 2' },
{ ...defaultItem, id: '3', title: 'Task 3' },
{ ...defaultItem, id: '4', title: 'Task 4' },
{ ...defaultItem, id: '5', title: 'Task 5' },
{ ...defaultItem, id: '6', title: 'Task 6' },
],
};

export const WithPinnedTasks = Template.bind({});
WithPinnedTasks.args = {
tasks: [
...Default.args.tasks!.slice(0,5),
{ id: '6', title: 'Task 6 (pinned)', state: TaskState.Pinned },
],
};

export const Loading = Template.bind({});
Loading.args = {
tasks: [],
loading: true,
};

export const Empty = Template.bind({});
Empty.args = {
...Loading.args,
loading: false,
};

介接 Store 資料

建立 Redux

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// src/lib/redux.ts

// A simple redux store/actions/reducer implementation.
// A true app would be more complex and separated into different files.
import { createStore } from 'redux';
import { TaskItem, TaskState } from '../components/Task';

export const archiveTask = (id: string) => {
console.log("archive task:"+id);
return ({ type: TaskState.Archived, id })
};

export const pinTask = (id: string) => {
console.log("pin task:"+id);
return ({ type: TaskState.Pinned, id })
};

// The reducer describes how the contents of the store change for each action
export const reducer = (state: any, action: { id:string; type: TaskState; }) => {
switch (action.type) {
case TaskState.Archived:
case TaskState.Pinned:
return taskStateReducer(action.type)(state, action);
default:
return state;
}
};

// The initial state of our store when the app loads.
// Usually you would fetch this from a server
const defaultTasks:Array<TaskItem> = [
{ id: '1', title: 'Something', state: TaskState.Inbox },
{ id: '2', title: 'Something more', state: TaskState.Inbox },
{ id: '3', title: 'Something else', state: TaskState.Inbox },
{ id: '4', title: 'Something again', state: TaskState.Inbox },
];


// We export the constructed redux store
export default createStore(reducer, { tasks: defaultTasks });

// All our reducers simply change the state of a single task.
function taskStateReducer(taskState: TaskState) {
return (state: { tasks: TaskItem[]; }, action: { id: string; }) => {
return {
...state,
tasks: state.tasks.map(task =>
task.id === action.id ? { ...task, state: taskState } : task
),
};
};
}

修改 TaskList.tsx 視作一個 container 與 redux 作介接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/components/TaskList.tsx
import React from 'react';
import Task, { TaskItem, TaskState } from './Task';
import { connect } from 'react-redux';
import { archiveTask, pinTask } from '../lib/redux';

// 中略...

export default connect(
(props:TaskListArgs) => ({
tasks: props.tasks.filter(t => t.state === TaskState.Inbox || t.state === TaskState.Pinned ),
}),
dispatch => ({
onArchiveTask: (id: string) => dispatch(archiveTask(id)),
onPinTask: (id: string) => dispatch(pinTask(id)),
}))(TaskList);

加上 Page InboxScreen

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//src/components/InboxScreen.js

import React from 'react';
import { connect } from 'react-redux';
import TaskList from './TaskList';

export class InboxScreenArgs {
error:string | undefined
}

export function InboxScreen(args:InboxScreenArgs) {
if (args.error) {
return (
<div className="page lists-show">
<div className="wrapper-message">
<span className="icon-face-sad" />
<div className="title-message">Oh no!</div>
<div className="subtitle-message">Something went wrong</div>
</div>
</div>
);
}

return (
<div className="page lists-show">
<nav>
<h1 className="title-page">
<span className="title-wrapper">TaskBox</span>
</h1>
</nav>
<TaskList />
</div>
);
}

export default connect((props:InboxScreenArgs) => (props))(PureInboxScreen);

一樣也加上 Story ,InboxScreen.stories.tsx
讓我們可以透過 Storybook 作人工 E2E 測試

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//src/components/InboxScreen.stories.tsx

import React from 'react';
import { Provider } from 'react-redux';
import { InboxScreenArgs, InboxScreen } from './InboxScreen';
import { Story } from '@storybook/react/types-6-0';
import store from '../lib/redux'

export default {
component: InboxScreen,
decorators: [(story: () => React.ReactNode) => <Provider store={store}>{story()}</Provider>],
title: 'InboxScreen',
};

const Template:Story<InboxScreenArgs> = args => <PureInboxScreen {...args} />;

export const Default = Template.bind({});

export const Error = Template.bind({});
Error.args = {
error: 'Something',
};

完整代碼可以參考此處

參考

(fin)

[上課筆記] 針對遺留代碼加入單元測試的藝術

題目

  • 聽到需求的時候,不要先想邏輯,先想怎麼驗收 ?

  • OO 善用封裝繼承的特性,不要修改 Product 類別,不要修改測試方法

  • Extract and Override 的 SOP

    • 找到 Dependency
    • Extract Method
    • private → protected
    • 建立 SUT 的子類
    • Override Dependency
    • Create Setter
    • 改測試
    • Assign 依賴值
    • 無法適用的情境
      • final
      • static
  • SPR 的 anti Pattern

    • 職責過多
    • 單元切得過細
  • 測試案例為什麼用 Snake 寫法?

    • 測試案例是用來描述情境
    • 反思:中文表達力強但描述精準度低
    • 要看見測試案例的因果關係
    • 測試案例的設計
      • 2+2 = 4
      • 2x2 = 4
      • 2^2 = 4
  • 重構測試案例

    • 只呈現與情境相關的資訊
  • 養成用 Domain 情境而不是 Code Level 的語言

  • 不要為了測試把 Production Code 搞得複雜

  • Mock Framework 的本質與要解決的問題是什麼

  • 3A原則與Given When Then

  • 減少 Content Switch(利用工具)

  • 時間要花在刀口上

    • Unit Test
    • Pair Programming
    • Code Review
    • 整合測試
    • QA 測試
    • Alpha/Beta 測試
    • 部署策略
  • Test Anti-Patter

    • 小心過度指定(使用 Argument Matcher
    • 測試案例之間相依
    • 一次測不只一件事(Data Driven)
    • 測試案例名字過於實作細節
  • 不要為了寫測試使用 Virtual

  • StoreProcedure 的測試不是單元測試,但是很好測試,只有與 Table 相依,透過 ORM 可以輕易作到,

Nice to Have

  • Coverage

    • Missing Test Cases
    • Dead Code
    • 實例分享:大家都不知道*
    • 童子軍法則(趨勢>數字)
    • > 0%
  • ROI

    • 出過問題
    • 經常變動的
      • 對不會變動且運作正確的程式寫測試是種浪費
      • 共用的模組
      • 商業價值
  • 架構設計

    • 三層式架構
    • 六角架構

課程總覽與個人的建議順序

課程 說明 補充
熱血 Coding Dojo 活動 點燃動機的一堂課 對我個人影響最大的一堂課
極速開發 學過這堂課,會比較清楚 91 的日常開發是怎麼作的 這裡是一個檻,你需要學習 Vim 也可能需要買好一點的 IDE
針對遺留代碼加入單元測試的藝術 單元測試的基本概念 強烈建議讀過「單元測試的藝術」與 91的「30天快速上手TDD」雖然有點久遠,但是好東西是經得起年歲的
演化式設計:測試驅動開發與持續重構 2日課程,如果只能上一堂課的話,我會選這堂 資訊量很大的課程,不過如果上過前面的課,這時候應該可以苦盡甘來
Clean Coder:DI 與 AOP 進階實戰 進階課程 如果上過前面的課,這時候應該可以苦盡甘來,但是建議可以對 Design Patter 稍作功課

反思:如果是我會怎麼設計內訓課程?

  • 附錄課程

    • 為什麼寫測試?
    • 第一個測試(計算器或 FizzBuzz)
    • 5 種假物件(Test Double)
      • Stub(模擬回傳)/Mock(前置驗証)/Fake/Spy:驗証互動(後置驗証)/Dummy
      • 小心不要落入名詞解釋
  • 主要課程(假設學生已經有一定的理解)

    • 第一個測試
      • 3A原則
    • 3正1反
    • 三種互動
      • 呼叫次數,參數,順序
      • 狀態改變
      • 回傳值
    • 隔離相依
      • 原生操作
      • 使用框架
      • 測試案例
        • 可讀性:增進理解 / 規格書 / 穿越時空的傳承
    • TDD
    • 重構
    • Design Pattern

課程上的反思

  • Setter 的重構要加強
  • 依賴注入點,不一定要在建構子也不一定要在 Prod (抽方法)
  • 要有能力判斷切入點
    • 善用工具
    • 練習
  • C# 參數善用Optional
  • OOP & UT
  • Fp & UT
  • UT & Refactor & Design Pattern
  • Pattern 工廠/簡單工廠/
  • 存在監控 Running Code 計算方法呼叫次數的工具嗎 ?
  • 快速打出六個0 ?

參考

(fin)

[生活筆記] 有關單元測試的一些反思

前情提要

單元測試可以說是我這幾年投入最多學習項目,
我為什麼會這麼投入? 是因為我相信這是一個有效的開發方式,
持續的上課與練習,稍微記錄一下這些年來的反思。

想法

單元測試是我覺得不論何種程式語言(雖然我並不甚理解 FP),最最基礎的技能,
非常值得深耕,好的單元測試本身是一種規格書(spec)與使用者案例(use case),
你想搞 CI/CD、敏捷、DevOps、DDD,都一定要有單元測試,
甚至我認為這一切只是軟體開發的本質的多面。

我個人是相當推崇 XP (極限編程)的,雖然不知道是不是因為「每周工作 40 小時」這一條,
導致企業不愛,公司不疼、專案管理協會不推、主管不敢用,
但就我來說 XP 反而是最貼近軟體工程的具體實踐(而 Scrum 只是框架,你可以把 XP 放進去),
也是真正能快速帶來品質的作法(簡單而務實)。

缺少的拼圖(困難的現實)

  1. 學習曲線

    有寫單元測試的工程師非常非常稀少,能內化寫得好的更是稀少。
    為寫而寫的單元測試不但無法帶來好處,反而弊病叢生又無法為產品帶來價值。
    但是學好單元測試要多久 ? 要導入又要多久 ?
    這點有些因人而異,但我敢說不是只有一兩次的內訓或 WorkShop 就可以實際上戰場。

  2. 人員素質參次不齊

    承上,寫測試的好處,短期內很難有立竿見影的好處,
    人性就是多一事不如少一事。
    更糟的是,過沒多久這些人員離職、調任、昇遷,
    人走了留下了代碼,一年年過去,
    就只剩一個焦油坑,一代一代傳承的業力,等待被引爆。

  3. 教育體系的缺乏

    在台灣能把測試說到點上的講師,我只能推 91 大,
    也可能是我同溫層太厚或是見識太少,有很棒的講師或網路資源請推薦給我。
    如果可以的話,我希望所有軟體的教育機構都應該介紹並深入指導這門知識,
    包含大專院校與各類補教機構 。

    不過反過來說,91 大的課程對學生來說並不便宜,時間也相當短促資訊量大,課也很難搶。
    在我的觀點,台灣整體軟體產業不能只依靠一個人,希望有更多高品質的課程能夠出現。

  4. 產品生命周期短

    現在的公司壽命,往往比一個人的職涯短,更不用說是內部的專案壽命,
    人員也常常被調任,也導致開發人員對產品或代碼的擁有權與責任感降低,
    當你對自已的產出只當作是個過客,那就不會原意用單元測試去細細打磨

  5. 人各有志

    並不是所有人都與我有相同的想法,我認為身為開發人員必須對自已的負責,
    要有「匠人心態」所以我的基礎會建立在單元測試之上。
    但有得人就只是「討口飯吃」、有得人著重「商業思維」、
    有得人偏重「架構設計」、有得人在乎「市場行銷」。
    退一步說,這都沒錯,但是這些開發者往往不會投入太多的精力在單元測試上(或是排序上靠後)

與未來的展望

單元測試可以幫開發人員可以用更少的資源,對代碼有更深的理解,
一切都是案例,而案例是對產品的解釋,
一但理解了案例,工程師只需要組合單元或是創造新的單元。
而代碼就會是活的工程文件,可以跨越時間傳承給下一位 RD,
你傳承的不再會是業力,而是產品品質的火炬。

一但單元測試的基本知識抓住後,
測試趨動開發與重構就會水到渠成,
這兩門技術也不容易,本質上也易學難精,很需要經驗值,
測試趨動會讓我們更接近使用者,這裡指的是代碼的使用者。

反思:也許是我們太常一人同時飾演多角才會這樣的問題,那麼 Pair Programming 是不是能解決這樣的問題)

重構會引導我們到設計模式
當然設計(架構與軟體)又是另一個領域了,未來會多找這一些課程來上。
目前這塊我也覺得是有缺憾的,TDD 之後就是重構了。
但是我並不認為硬背重構的準則或是 Design Pattern 就是解決之道,
而是應該結合兩者有系統化的設計課程,
理想上是有一個開發案例,可以循序漸進,反覆迭代的開發,
讓學徒可以由作中學,進而掌握這兩本藝術。

我希望可以有更多課程與資源可以學習讓學生們在接觸程式的當下,就接觸測試。
如何讓初階的開發者在學習路上,就接觸到正確的單元測試的姿勢呢 ?
如果未來有一天,人人都能寫出水平之上的測試,這個產業將會進入什麼樣領域呢 ?

參考

(fin)

[生活筆記] 職涯回顧

前情提要

本來想寫寫單元測試活動的心得,
但是覺得需要先說說我對單元測試的想法,
但是在那之前有需要說明我經歷了什麼,我又是怎麼樣的人。
寫著寫著越來越多,就拆了這篇文章出來。

剛好就業也差不多 10 年有餘了,也算是給自已一個重新審視的機會。

經歷

退伍的第一份開發工作就被開除;
原因不明,但我的解釋就是戰力不足不是即戰力,
但是因為該公司很快就放掉我,我反而是感謝的。
那個時候工作很難找(08年金融海嘯),就業面臨了沒經驗找工作的蛋生雞問題。

沒經驗找工作

所以就想去補習(資策會的課程),
但是家裡沒錢(房貸/學貸)還有一些長輩的不良債務(家裡被潑漆之類的),

後來跑去一個電子線材公司作資訊助理工作了 1 年半,
存夠了錢就去上課了,半年後也順利找到 W 社的工作,
這個時候去各個地方面試就蠻搶手的 Offer 拿到蠻多的,
但是我開的薪資,現在想想蠻都低的,另外比較印象深刻的是面試被保哥打槍了兩次(無聲卡),
W 社最主要讓我認識了 Gelis 大大,還有獨立接手一個專案的經驗。
但離開的原因也是因為缺乏刺激,一個人怎麼作都不會有人管你,
看著一大份前人遺物,同事們的想法就是能動就好,沒有人想改進代碼,
那個時候我蠻受「學徒模式」一書的影響,想成為一個匠人,沒有回饋(與加薪)的環境我選擇了離開。

到了 C 社以後,蠻受主管肯定,工作氛圍比較接近新創,
同事之間的互動也蠻不錯的,最主要的成長是獨立開發了幾個專案,而且蠻賺錢的。
那個時候的角色也不錯,能清楚知道需求端開發端的狀況,
而且有幾個蠻重要的技術導入經驗。
但是後來碰到一個蠻低的薪資天花板(就知道我起薪有多低XD),所以離開了。
現在想想離開也蠻正確的,不單單是薪水,對技術過於保守,其實現在有點吃老本的感覺。

後來就進了 N 社,我是在 N 社的成長期進去的,裡面很多大大,
也有一些 MVP,也是這個機會接觸到 TDD 與 SkillTree 的課程,
但是也是蠻掙扎的,一直無法作主要的專案,理想的開發方式也常常被現實打臉,
社群活動也參加的蠻多的,前期一些中國的講者的課程影響我蠻深的(也跟測試有關),
跟 Ruddy 老師也有蠻近的接觸,看了蠻多的好書「軟技能」、「軟體開發本質論」、「進化」等…,
工作上實作了敏捷(雖然有蠻多地方怪怪),也取得 Scrum Master 的認証,
但是我其實不是太認同社群的造神運動,還有認証機構進來以後說一些虛無飄渺的東西。
我還是想將「理論」與「現實」的鴻溝抹平,
離開的原因是想要有程式外的與人(妹子)接觸與時間(不太想 10-19 的上班方式),薪資當然也有所提昇。

想法

早期的我很受「學徒模式」一書的影響,
想成為一個匠人,但是現在開始反思這個想法會不會過時了,
現在會比較想要斜槓,但是我不認為這兩者是衝突的。
主因是受前端開發影響,真的是一年萬變。
以前的我是前後端都開發,現在主流的開發方式都是分離的,
有其時空背景的因素在,而敏捷團隊講求的是跨職能(ps.我認為應是團隊而不是個人的跨職能)。
對於我來說,我個人也願意多學一點,但是反而有些貪多嚼不爛,
時間會是我最大的敵人,也是朋友
而要考慮的會是價值與風險,單純的一直學「新」技術,沒有累積話只是一種浪費(比如說:SliverLight),
所以我現在遇到新技術,會讓子彈飛一會兒,畢竟時間與精力都是稀缺資源。

開發之外也有很多重要的事,這些在「軟技能」一書可以看到,
包含維持自已身體的健康、習慣的建立、時間管理、財務管理、第二外語與溝通的技巧,
甚至是你的社交圈、家庭與親密關係,到這裡其實就是你的人生了。
軟體工程師不應該只是一個開發工具,軟體開發應該是未來人人具備的技能,

而你擁有這些技能,如何善用讓自已可以過得更好,反而才是一個議題。
舉例來說,有人用來增加收入(接案),有人用來分析投資(股票爬蟲&程式交易),
有人用來找伴侶,有人用來建立品牌或創業。
不要讓開發只在工作之中,要讓開發落入你的生活之中。

(fin)

[活動筆記] 工程師如何提升英文聽說讀寫能力

前情提要

2020 年搭客運南下高雄,時間長達 5 小時,
有在關注的 Podcast/YouTube 節目都聽完了。
百般無聊的情況下,滑 facebook 滑到了保哥的直播節目,
講的題目恰巧是我一直想改善的軟勒—英文,
裡面有提到相當多的資源,稍作記錄一下。

摘要

保哥

  • 學習的順序應該是「聽」「說」「讀」「寫」。(但我們的環境「聽」「說」的機會很少)
  • 三個月連續上課的衝次班
  • 語感的建立,先看懂文字與脈絡,再聽慢版,再聽快版(莫彩曦的建議是不要看字幕)
  • 要找自已有興趣的主題
  • 動機是學習新的技術

David

  • 強烈的動機:交女朋友
  • 快樂的環境:能讓你快樂的環境
  • 選擇口說補習班
  • 人會有惰性,要有強烈的動機

其它

  • 自發學習的動機,認識外國朋友,與外國人溝通的虛榮心,承擔家庭責任
  • 中英文的思考模式不相同,不要用翻譯的方式(在其它地方也有聽過中文腦與英文腦的說法)
  • 學習資源:空中英語教室,大家說英語
  • 不要怕犯錯,可以犯錯的環境
  • 有 Context 的情況下,你可以用肢體語言協助理解
  • 有興趣的東西:英文攻略
  • 英文單字量不夠
  • 如何維持語感? 多與外國人聊講話
  • 閱讀原文書,先要有大概的理解
  • 美劇的看法,看現代美劇
    • 第1遍,聽英文看中文字幕
    • 第2遍,聽英文看英文字幕
    • 第3遍,聽英文不看字幕
  • 不是所有的外國人都是友善的
  • 師大國語文中心附近公園,會有外國人主動攀談
  • Meetup.com 的語言交流活動

小結

  • 要有強烈的動機
  • 建立環境(反思,應該是建立系統與生活結合;ex:看同儕有在追的美劇(如:后翼棄兵)可以成為聊天話題&訓練英文)

參考

(fin)

[生活筆記] 2020 回顧

大事記

社會

  • 武漢肺炎
    • 東京奧運停辦
    • 無限量 QE
  • 香港民主運動失敗
  • 美國大選與黑人平權運動
  • Kobe 墜機死亡
  • 中天新聞停播

生活

足球

今年只有打一場比賽,2020 Arsenal 足總盃冠軍 2:1 Chelsea,然後賽季表演持續探底。

Lindy Hop

生涯首次在百人面前表演,所屬教室(YM)結束經營,
學了 BaLboa 到放棄,上了 36 堂長課(YM & YM 2.0),中間因為疫情有延期,
會持續到 2021 (還有6堂)

今年是 Podcast 興起的一年,年初聽了股癌、光說不設計(偶然發現竟然是跳舞的老師,但是我沒跟他說)
後來聽了百靈果、台灣通勤第一品牌、徐豫切入點、大人學等…
身邊也有一些朋友開始在作,我心裡反而有一個 voice log 的想法,
但是還沒有很清晰,2021 或許可以作一些探索,當作日記用聲音記錄一些想法與事情。

  • 閱讀
    • 大話設計模式
    • 硝煙中的敏捷
    • DDD
      • Domain-driven Design
      • Implementing Domain-driven Design
      • Domain-driven Design Distilled
  • 婚禮
    • 阿棠
    • 蛋頭
  • 英文
    • TUTORING APP 第四屆體驗大使
    • Voice Tube 零元挑戰成功
  • 體驗
    • 潛水: 考証中
    • 抱石: v0(8/10),目標 v2

職涯

  • 2月 N 社離職
  • A 社入職,Scrum Master
    • 3 月入職
    • 8 月產品上線
    • 12 月完整團隊開會
      • QA 納入團隊
      • 具有 PO 職權的人全數進入會議之中
  • Scrum Guide 2020 改版

小結

2020是外面風好大的一年,今年的我反過得還算不錯(?),
但是大家都在躲風避雨,所以相對有些寂寞(?)

生活上最主要的改變是轉換了工作到了 A 社,
主要的工作項目也從開發者變成了引導者(Scrum Master),
這裡其實也有一點反思,這是我要的路嗎?
長期以來我是以「學徒模式」一書的模式在發展我的職涯,
以成為工匠為目標,但是實際上帶來的收入並不豐碩。
自動化的時代,自媒體的時代,純粹的工匠思維還足夠嗎 ?
狐芳自賞的模式能讓我生存下去嗎 ?
在「軟技能」一書中也提到了一些開發之外的選擇,
自我營銷、健身、理財等…
甚至在現今主流的開發流程框架之中,
跨職能跨領域才是未來的生存之道。
而我的職業選擇帶我到了這個地方,我應該冷靜評估自身與自處的優劣,
找到自已的一片天才是。

來談談 2021 幾個大目標,

  • 人際關係
  • 英文
  • 作息
  • 財務
  • 職涯

人無遠慮,必有近憂,明年應該調整自已生活的節奏。

人際關係方面,想改善我的表達技巧,
我的表達方式在工作場合相當適用,就事論事講求效率極高,
在有相同領域的認知下,可以高度輸出大量訊息。
但是在人與人的場合,反而缺乏溫度,太過一板一眼,
可以試著模仿一些人,但不確定是否適合我。

在財務還算穩定的狀況下,我想強化我的英文與身體健康。
英文是個痼疾,我想找到可以直接口說的英文夥伴或老師。
具體來說,目前會考慮莫彩曦的課程與 myClass 大人的英語課,
要更加有意識的系統化思考時間的運用。

財務方面,我已經規劃好一連串系統操作,運作順利的話,
未來理財會更加的輕鬆寫意才對,關鍵點會在 1/1、1/4、1/5 這幾天。
幾個項目:

  1. 設定孝親費轉帳
  2. 主帳戶的轉移
  3. 証券戶的轉移
  4. 消費的預算制
  5. 業外的收入

而身體隨著年紀漸長,需要更細心保養了,
我想先從健身與早睡早起開始,希望體重可以維持在 BMI 正常的水準。
並不像理財那樣有長期執行的經驗,希望可以有系統化的作法來維持;
此外,飲食也是相同重且複雜的環節,在 2021 年要綜觀全局的認真思考這件事,
並減少不必要的聚餐。

職涯短期我打算更加詳細的記錄我在 A 社推動的敏捷改變,
同時鍛鍊我的寫作與表達能力。
另外要有計劃的建立我的專案,
我想將單元測試與 TDD 的一些概念與心得分享在 YouTube 與 LBRY 上(免費)
另外我會想討論一下專業養成與市場需求的落差。

(fin)

[活動筆記] 2020 SCRUM GUIDE UPDATE

前情提要

2020/11/18 將迎來 Scrum 25 周年與 Scrum Guide 的更新,
活動有一些大師對談,錄影放在文末連結之中,
另稍微比較一下 2017 與 2020 Scrum Guide 的差異(以中文為主),
特別感謝譯者這麼有效率的更新,我差不多在 19 號就可以在網站下載

排版與封面

2017 版本主要視覺的位置放了兩個大師的合照,首行字為「Scrum 指南™」
2020 版本主要視覺的位置為「Scrum 指南」,首行字為兩位大師的名字。
理想我會希望使用 2020 的版本,然後兩位大師的名字縮小到角落 :D

目錄

2020 將目的前移到目錄之前,我覺得這樣的調整很好。
讓人可以先讀取本指南的目的。

Scrum 指南的目的

篇幅相比 2017 更長了,並將 2017 版「Scrum 的運用」縮減並挪移至此處。
簡介了 Scrum 的起源,概述了 Scrum 的運用範圍與現況。

另外有兩句預防性的警語,是這個段落的重點

  • 改變 Scrum 的核心設計或 Scrum 的各種理念,遺漏其中任何元素,或是不遵照 Scrum 的規則,是在掩蓋問題,並限制了 Scrum 的各種好處,甚至可能使其變得毫無用處。
  • 這些使用 Scrum 框架內的戰術技巧有很大的變化,因此不在此描述。

這個篇幅簡述了 Scrum 已是被時間與不同產業實証有用的框架,
但擅自修改 Scrum 可能會導致 Scrum 效用打折甚至無用,
並具體說明 Scrum 的戰術實作不會在此指南描述。

Scrum 的定義

相比 2017 的版本,這裡直接提到了 Scrum 中所有的角色與其職責。
並明確的說明「原封不動地應用 Scrum」, 並強調這份指南是人之間的指引
而不是具體的流程、技術與方法。

這裡也是我覺得很好的地方,以前在首次與團隊成員說明 Scrum Guide 時,
如果按照 2017 的章節說明,我都要另外安排一個段落先簡述 Scrum 中的角色,
因為 2017 在角色出現的篇幅比角色的說明還要早。

Scrum 的理論

2020 的版本加上精實思維(Lean Thinking),
並具體說明了檢視、調適的事件( 2017 翻譯為活動)與三大支柱的關係。

透明性的段落強調了没有透明性的檢視會產生誤導和浪費(做出讓價值減少且風險增加的決策)
很有感觸,我在團隊導入 Scrum 的首要宗旨就是看見事實。
(有機會再談談如何定義事實與 Ruddy 老師說的「看見全貌」的差異之所在)

檢視性強調 Scrum 將以 5 個事件有節奏實踐檢視性(具體作法與事件在這個段落還沒提),

檢視性促成調適性。 沒有調適性的檢視是沒有意義的。Scrum 的事件旨在激發改變。

這句話作到了承先起後的作用,可以將檢視性視作三大支柱的樞鈕。

調適性的篇幅提到了授權與自我管理,這也許是導入常常碰到的地雷。
團隊不被授權或缺乏自我管理的能力或意識(有點像有沒有病識感)。

2020 的編排我很喜歡,除了一再預告事件如何產生三大支柱之外,不再僅僅以順序暗示三大支柱的關係。
而是明確的說明透明性促成檢視性。檢視性促成調適性。
(有點像真善美,沒有真後面就會變成假善、假美那就豪無意義了)

Scrum 的價值觀

對承諾、專注、開放、尊重、與勇氣的排版更明顯了,另外 Stakeholders 也明文納入其中了。

誒、不對啊,這些態度不是跟四維八德一樣,小朋友都知道嗎?
而且應該不論是你的老闆、雞巴主管、龜毛客戶、秋條前輩到所有人都應該要有相同的態度,不是嗎?
這就是知易行難吧… 說個滑坡的,最近在思考尊重與尊敬的差異,
朋友給了我一個例子:

  • 尊重:念在你是一代宗師,你自盡吧
  • 尊敬:我的戰鬥力只有六千,他起碼有一萬以上

聽說看得懂的都是老人。

Scrum Team

引言強調一位 Product Owner(2017 在後面的段落才提到),
強調了沒有子團隊與階級架構(具體實務上會影響到組織結構,實作起來並不容易,需要更多的經驗)。
這次沒有翻譯目標/目的了,直接使用 Product Goal 並在後面的段落具體指由 PO 開發、描述溝通。

然後強化了對當責的描述,調整了後面篇幅的介紹順序
2017:PO > Development Team > Scrum Master
2020:Developers > PO > Scrum Master
我認為順序都是有暗喻性的,但解讀方式是自由的,就不過多解釋了。

重點是責任的部份,我覺得比起 2017 更能簡單的用 Scrum Guide 說明現在角色的職責所在了。

Developers

● 打造一份 Sprint 的計畫,也就是 Sprint Backlog;
● 藉由遵循完成之定義,以灌輸品質;
● 每天調適其邁向 Sprint Goal 的計畫;和,
● 作為專業人士對彼此負責。

Product Owner

● 開發並明確的描述溝通 Product Goal;
● 創造並清楚的描述溝通 Product Backlog items;
● 對 Product Backlog items 進行排序;和,
● 確保 Product Backlog 是透明的、可見的與可理解的

這裡特別加上了

Product Owner 可以自己做上述工作,或者也可以將職責委託他人,然而,Product Owner 仍肩負最終責任。

這句話我視為對大型組織導入 Scrum 的困難之處的回應。
錯誤 Scrum (其實就不是 Scrum)會產生缺乏實際權限的 PO
或是有權無(卸)責的傳統型領導。

Scrum Master

明文Scrum Master 對 Scrum Team 的效能負責;職責更明確了,語句更洗鋉,贅字更少。
但我覺得「真正的領導者」這段文字將會產生轉型時的爭議,特別是將「僕人式領導」文字又被拿掉。
我會建議作為 Scrum Master 要把這件事放在心中,Title 只是浮雲啊。

Scrum 事件(原為 Scrum 活動)

Sprint

明文採用時間較短的 Sprint,可以建立更多學習周期,此外更加強調三大支柱與 Sprint 的關係。

特別提醒經驗主義的重要性,更勝於實際的做法(諸如:燃盡圖、燃起圖,或是累積流量圖等…)
這與我的經驗也是不謀而合,主管一開始就投入過多心力在要求製作圖表,
而忽略了在圖表之前,進行預估其實是需要訓練的,最後圖表變成作假帳…
失去透明度,檢視性與調適性將無法發揮功能,Scrum/Sprint 將會失敗(或是不知道成功或失敗)。

取消 Sprint 的章節被大幅縮減,僅以不合時宜一句代過。
反而釋放更多空間給 Product Owner。
我覺得 PO 當要取消 Sprint 時,要思考以下的問題,

  • 要如何與其它角色互動?
  • 要如何持續實現 Product Goal?

這樣的文字編排方式,我覺得是很大的改善,強調在 Scrum 之中,
我們的所有行為都是為了實現三大支柱,而我們相信這樣的方法可以領我們到達終點之地。
後面的事件也都有類似的描述,我就不再補充。

Sprint Planning

2017 版本
第一個討論題目:這次 Sprint 能做出什麼?
第二個討論題目:如何完成所選的工作?

2020 版本
主題一:為什麼這次 Sprint 有價值?
主題二:這次 Sprint 能完成(Done)什麼?
主題三:如何完成所挑選的工作?

明顯多了一個有關價值的主題,但是需要與 Stakeholders 在 Sprint Planning 結束前被確定下來,
就我實務的經驗 Stakeholders 與 PO 會比 Developers 提早決定未來的目標,
所以 Stakeholders 依然不是會議中必要的角色。

Daily Scrum

1
2
3
4
Developers 可以選擇他們想要的任何 Daily Scrum 的結構和技術,
只要他們的 Daily Scrum 專注於實現 Sprint Goal 的進展,
並且產生下一個工作天可執行的計畫。
這樣可以更專注並改進自我管理(self-management)。

把經典的三個問題拿掉了,這也符合我的經驗,不要流於形式,
而是更專注在實現 Sprint Goal,不流於形式則更能讓團隊自我管理。

Sprint Review 與 Sprint Retrospective

這兩個段落的篇幅都縮短了,但是我覺得更言簡意賅。
但投影片展示的描述,我還比較喜歡 2017 版本的描述 是為了引發意見的反饋和提升協同合作
但的確實務上往往會淪為簡報報告。

關於 Retrospective 這個會議對我的定義,
我目前的團隊沒有在跑 Scrum ,但是我直接引入 Retrospective。
Retrospective 是一個可以雕塑團隊的會議。
有趣的事,團隊現在調整的越來越像 Scrum (當然依然不是 Scrum)

Scrum Artifacts

明文: Artifacts 的設計是為了使關鍵資訊之透明性極大化。(其實 2017 年的版本也有提到)
2020 版本的文字組織更簡明之外,都加上了承諾的區塊,

  • Product Backlog 是為了實現 Product Goal 的承諾
  • Increment 是為了實現對完成之定義(Definition of Done) 的承諾
  • Sprint Backlog 是為了實現 Sprint Goal 的承諾

結語

結語的部份並沒有太大幅度的修改,
但是我想要再一次強調 「雖然實施部分的 Scrum 是可能的,但結果就不是 Scrum 了。」

小結

整體的文章的結構完整性更好了,三大支柱與活動的連結,產出與承諾的連結。
然後將實作(戰術)面與角色的行為(戰鬥)樣版移除,這樣給了更多空間讓團隊發揮。
也會促使團隊思考背後的原因(Why) ?

想起以前聽過「守、破、離」的演說,我想這次 Scrum Guide 有一點這樣的味道,
在 25 年前,什麼經驗都沒有情況,這樣的樣版帶來了相當大的幫助,
現在我們都有一些實作有一些失敗有一些成功,是時候把樣版移除了(或是回到問題的本質)。

  • 你真要的三個問題嗎 ? 背後想問到的是什麼 ?
  • 真的不取消 Sprint 嗎 ? 為什麼不行 ?
  • 不站立真的不能開 Daily Scrum 嗎 ?

20201125 補充:
Adrian 的分享相當清晰明瞭,補充連結如下:

校錯

  1. 的的
  2. 的的
  3. Artifacts

參考

(fin)

[生活筆記] 收支流程設計

耶和華所賜的福使人富足,並不加上憂慮。— 箴10:22.

前情提要

投資理財是現代人生活極為重要的部份,
但是我在學生時期並沒有學習到這方面的知識,
出社會也大概 10 年了。稍微作個記錄與分享。

概念

開源節流是基本理財認識,簡單分為收入與支出。
收入方面,最常為工資所得,在網路時代也有很其它的賺錢方式,
最主要我想還是要建立系統,這裡不過多著墨,
我的系統屬於股票投資,但是記得你可以不只有一個系統,而建立系統是需要時間與反覆思考的。

支出方面,其實滿足一個人的生活所需,金額不會太大,
當然想過得比較滋潤的話,會比較花錢,
跟據你所居住的區域來說(不是要戰南北),還是會有比例上的差異,
你也可以用時間來轉換成本,不過裡面藏著一些魔鬼細節,小心得不償失。

舉例來說:
如果是自行開伙,飲食的費用並不會太高(特別是家庭人數較多的情況),
但是就要考量到材料準備、保鮮、整理等環節,外食也可以有便宜的選擇,
但是可能會是太油太鹹太甜的餐點,導致健康受到影響。

但除此之外,孝親、偶爾為之的旅行、進修、稅、保險等等…什麼都要花錢。
簡單的一句,所有的支出,我都建議預算制。

以下是以一個小資上班族(就是我啦)的概念去設計的,
不包含保險、股票、信用卡等…其它的工具。
如果有人喜歡,也許未來才再寫相關文章吧。

帳戶分類

受薪帳戶(InCome Acct)

受薪階級通常會有薪轉戶,至少要有1次以上的跨行轉帳免手續費
如果非工資收入(接案/紅利等…),應該儘可能直接轉入主帳戶(Main Acct)

主帳戶(Main Acct)

目的:負責最多轉出轉入功能的帳戶,主要的活存會放在這裡,
應該有一定次數的跨行轉帳免手續費,或是約定帳戶免手續費
依個人使用習慣,建議要有 5~10 次以上,此外要有好用的APP/網銀
最後才是活存利率要儘可能的高,因為你大多數的生活費會停滯在此。

目前有些網銀放 5 萬或 10 萬就有 2% 以上的利率,比定存還高。
10 萬放一年就有 2000 是蠻值得的,當然不要本末導致,
省下的跨行交易手續費與其方便性才是我們要的。
這個帳戶的設計上金錢會有小額高頻的流通,
不要為了 2% 而讓錢鎖死不流動,
假設股市的投資如果有 5% 報酬的話應該讓現金流向股市市場(需要考慮投資風險),
當然有閒錢還是可以考慮…

2020 年可以參考這篇文章,轉帳免手續費帳戶比較
雖然開戶作業有時候很麻煩,這些優惠也常常過幾年後就消失與改變。
但是僅僅這幾年中帶來的方便性也是很高的喔,手續費這種磨血的花費真的建議能省則省。

証券戶/証券銀行

目的:主要的股票投資帳戶,銀行與証券戶轉帳不應有手續費,
還沒有投資就先虧一筆,就好像跑百米你讓人家 10 公分,
你覺得沒差嗎?我覺得有差,跑百米我不是 Bolt ,理財我不是連公子。
証券銀行轉到其它銀行有一定次數的跨行轉帳免手續費
這裡的次數不用多 1~5 次即可,主要是我的交易頻率其實不高,
需要交易,通常是有大筆資金需要轉投資其它項目的時候,
反而是單日/周/月轉帳上限比較重要,可以在開戶的時候作約定。
最重要的是証券交易手續費要儘可能的低
基本上會有 1.425‰*0.6(ex:富邦) 的折扣,
高交易額或有活動開戶的券商可以更低 (ex:華南 1.425‰*0.3),
不要小看這千分一點四二五的 3 折,以前我不知道,傻傻被扣的錢可以訂好幾年的 Spotify 了
最後要有 好用的App (實際上好像每家都一樣,都是三竹作的?)

其他投資

活存

在低/零利率時代,活存已經不適合作投資的首選,
所以通常會與 主帳戶(Main Acct) 作結合,
主要用於生活上食衣住行娛樂所需的花費,
方便性將大於投資目的,但是仍要選擇活存利率高,
或是回饋高(ex:刷卡回饋/ShopBack)的消費方式

定存

利率要高,但是在低/零利率時代,定存已經不是良好的投資標的。
但建議可以將緊急預備金作為定存

外幣:

性質會與活存類似,但以外幣計價,應為國際通用貨幣(美金、歐元等…),利率要高,
主要的目的為美股、旅遊、海外置產(目前沒有),特性是交易手續費都不便宜,所以持有比例不會太多。

美股/其它市場股票

台股之外第一選擇,以美股為主,
好處是標的超多,可以賺世界上所有標的的錢,
缺點是美金計價所以想轉換成現鈔或是台幣手續費都很高,
沒特殊情況這些錢不會拿出來花,會變成純粹的投資。
研究中…

黃金/貴金屬:

有使用黃金帳戶買過,非實體黃金,
只能賺差價而不生息,所以不愛,目前已經不再投入。

支出

所有的支出應該都採取預算制
每年編列並支出,作為個人,不應有消耗預算的行為,
但是可以挪移至下一年度。

Fixed Charge (固定支出)

  • 保費(綁定信用卡)
  • 稅金(分期 0 利率)
  • 孝親費
  • 訂閱制服務
    • 域名( Domain )
    • Spotify (綁定信用卡)
    • 水/電/瓦斯
    • 網路/手機電信/第四台
  • 生活費
  • 投資
    • 定期不定額
    • 主動式投資

浮動支出

預算制設定上限,但是以實支實付為主

  • 學習費用
    • 買書
    • 上課
  • 娛樂費用
    • 旅行
    • 聚餐
  • 婚喪禮金
    • 好友/親人
    • 普通同事/朋友,只包不去通常可以省時省錢

Over View

Cash Flow Over View

參考

(fin)

[生活筆記] 簽署軟體工藝宣言

As aspiring Software Craftsmen we are raising the bar of
professional software development by practicing it and helping
others learn the craft. Through this work we have come to value:

Not only working software, but also well-crafted software
Not only responding to change, but also steadily adding value
Not only individuals and interactions, but also a community of professionals
Not only customer collaboration, but also productive partnerships

That is, in pursuit of the items on the left we have found the items on the right to be indispensable.

作為有理想的軟件工匠,我們一直身體力行,
提升專業軟件開發的標準,並幫助他人學習此工藝。
通過這些工作,我們建立以下的價值觀:

不僅要讓軟件工作,更要精益求精
不僅要響應變化,更要穩步增加價值
不僅要有個體與互動,更要形成專家的社區
不僅要與客戶合作,更要建立卓有成效的伙伴關係

也就是說,左項固然值得追求,右項同樣不可或缺。

簽署心得

這些價值觀我是相同認同的,
可是這個網站的感覺並沒有呈現出那樣價值觀啊…
或許還少了一點美感.

我的簽署資訊如下,
30196 Marsen (Taiwan) 2020/10/28,
查詢功能不能查 ID 與國家有點弱,
沒有正體中文的翻譯, 補充資料的部份很不錯,
但是也只是沒有系統的散落在那裡.
想給點回饋,也找不到留言區或討論區, 或是網站的 Repository.

參考

(fin)

Please enable JavaScript to view the LikeCoin. :P