├── ch1 ├── images │ ├── risk.jpg │ ├── communication.jpg │ └── coordination-work.jpg └── README.md ├── ch5 ├── images │ ├── sale.jpg │ ├── proxy.jpeg │ ├── analyst.jpg │ ├── customer.jpg │ ├── manager.jpg │ ├── marketing.jpg │ ├── support.jpg │ ├── trainer.jpg │ ├── develop-leader.jpg │ ├── domainexpert.png │ └── former-users.jpg └── README.md ├── ch6 ├── images │ └── atdd.jpg └── README.md ├── images ├── starting.jpg └── user-stories-applied-guide-in-agile-scrum-user-story-best-practices.jpg ├── ch2 ├── images │ └── INVEST.jpg └── README.md ├── ch4 ├── images │ ├── survey.gif │ ├── observe.jpg │ ├── trawlnet.gif │ ├── workshop.png │ ├── sanqizhouyu.jpeg │ ├── simple-mockup.png │ ├── user-interview.jpg │ ├── user-interview.png │ ├── user-interview-01.png │ ├── user-interview-02.png │ ├── user-interview-03.png │ ├── user-interview-04.png │ ├── user-interview-05.png │ └── user-interview-06.png └── README.md ├── ch7 ├── images │ ├── criterion.jpg │ ├── slicing-the-cake.jpeg │ └── Slicing-The-Cake-ATM-Horizontal-And-Vertical-User-Stories.png └── README.md ├── ch3 ├── images │ ├── miejueshitai.png │ ├── user-modeling.jpg │ ├── init-role-collections.png │ └── integrated-role-card.png └── README.md ├── graffle └── user-stories.graffle ├── theme ├── theme.css ├── theme.js ├── pagetoc.css ├── pagetoc.js ├── css │ ├── general.css │ ├── variables.css │ └── chrome.css ├── simple-lightbox.css ├── index.hbs └── simple-lightbox.min.js ├── SUMMARY.md ├── .gitignore ├── .editorconfig ├── book.toml ├── deploy-to-github.sh ├── .github └── workflows │ └── mdbook-deploy.yml ├── book.json ├── README.md └── .travis.yml /ch1/images/risk.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch1/images/risk.jpg -------------------------------------------------------------------------------- /ch5/images/sale.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch5/images/sale.jpg -------------------------------------------------------------------------------- /ch6/images/atdd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch6/images/atdd.jpg -------------------------------------------------------------------------------- /images/starting.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/images/starting.jpg -------------------------------------------------------------------------------- /ch2/images/INVEST.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch2/images/INVEST.jpg -------------------------------------------------------------------------------- /ch4/images/survey.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch4/images/survey.gif -------------------------------------------------------------------------------- /ch5/images/proxy.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch5/images/proxy.jpeg -------------------------------------------------------------------------------- /ch4/images/observe.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch4/images/observe.jpg -------------------------------------------------------------------------------- /ch4/images/trawlnet.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch4/images/trawlnet.gif -------------------------------------------------------------------------------- /ch4/images/workshop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch4/images/workshop.png -------------------------------------------------------------------------------- /ch5/images/analyst.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch5/images/analyst.jpg -------------------------------------------------------------------------------- /ch5/images/customer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch5/images/customer.jpg -------------------------------------------------------------------------------- /ch5/images/manager.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch5/images/manager.jpg -------------------------------------------------------------------------------- /ch5/images/marketing.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch5/images/marketing.jpg -------------------------------------------------------------------------------- /ch5/images/support.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch5/images/support.jpg -------------------------------------------------------------------------------- /ch5/images/trainer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch5/images/trainer.jpg -------------------------------------------------------------------------------- /ch7/images/criterion.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch7/images/criterion.jpg -------------------------------------------------------------------------------- /ch1/images/communication.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch1/images/communication.jpg -------------------------------------------------------------------------------- /ch3/images/miejueshitai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch3/images/miejueshitai.png -------------------------------------------------------------------------------- /ch3/images/user-modeling.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch3/images/user-modeling.jpg -------------------------------------------------------------------------------- /ch4/images/sanqizhouyu.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch4/images/sanqizhouyu.jpeg -------------------------------------------------------------------------------- /ch4/images/simple-mockup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch4/images/simple-mockup.png -------------------------------------------------------------------------------- /ch4/images/user-interview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch4/images/user-interview.jpg -------------------------------------------------------------------------------- /ch4/images/user-interview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch4/images/user-interview.png -------------------------------------------------------------------------------- /ch5/images/develop-leader.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch5/images/develop-leader.jpg -------------------------------------------------------------------------------- /ch5/images/domainexpert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch5/images/domainexpert.png -------------------------------------------------------------------------------- /ch5/images/former-users.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch5/images/former-users.jpg -------------------------------------------------------------------------------- /graffle/user-stories.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/graffle/user-stories.graffle -------------------------------------------------------------------------------- /ch1/images/coordination-work.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch1/images/coordination-work.jpg -------------------------------------------------------------------------------- /ch4/images/user-interview-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch4/images/user-interview-01.png -------------------------------------------------------------------------------- /ch4/images/user-interview-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch4/images/user-interview-02.png -------------------------------------------------------------------------------- /ch4/images/user-interview-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch4/images/user-interview-03.png -------------------------------------------------------------------------------- /ch4/images/user-interview-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch4/images/user-interview-04.png -------------------------------------------------------------------------------- /ch4/images/user-interview-05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch4/images/user-interview-05.png -------------------------------------------------------------------------------- /ch4/images/user-interview-06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch4/images/user-interview-06.png -------------------------------------------------------------------------------- /ch7/images/slicing-the-cake.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch7/images/slicing-the-cake.jpeg -------------------------------------------------------------------------------- /ch3/images/init-role-collections.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch3/images/init-role-collections.png -------------------------------------------------------------------------------- /ch3/images/integrated-role-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch3/images/integrated-role-card.png -------------------------------------------------------------------------------- /theme/theme.css: -------------------------------------------------------------------------------- 1 | /* 默认样式间距过大,显得过于紧凑 */ 2 | table { 3 | width: 100%; 4 | } 5 | table tbody td, table thead th { 6 | padding: 6px 20px; 7 | } 8 | -------------------------------------------------------------------------------- /ch7/images/Slicing-The-Cake-ATM-Horizontal-And-Vertical-User-Stories.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/ch7/images/Slicing-The-Cake-ATM-Horizontal-And-Vertical-User-Stories.png -------------------------------------------------------------------------------- /images/user-stories-applied-guide-in-agile-scrum-user-story-best-practices.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonydeng/user-stories-applied/HEAD/images/user-stories-applied-guide-in-agile-scrum-user-story-best-practices.jpg -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [敏捷的用户故事方法](README.md) 4 | * [1. 什么是用户故事?](ch1/README.md) 5 | * [2. 编写故事](ch2/README.md) 6 | * [3. 用户角色建模](ch3/README.md) 7 | * [4. 收集故事](ch4/README.md) 8 | * [5. 与用户代理合作](ch5/README.md) 9 | * [6. 用户故事验收测试](ch6/README.md) 10 | * [7. 优秀的用户故事准则](ch7/README.md) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node rules: 2 | ## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 3 | .grunt 4 | 5 | ## Dependency directory 6 | ## Commenting this out is preferred by some people, see 7 | ## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git 8 | node_modules 9 | 10 | # Book build output 11 | _book 12 | 13 | # eBook build output 14 | *.epub 15 | *.mobi 16 | *.pdf 17 | 18 | .DS_Store 19 | 20 | yulib.* 21 | -------------------------------------------------------------------------------- /theme/theme.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | document.addEventListener('DOMContentLoaded', function () { 3 | var imgs = document.getElementsByTagName('img'); 4 | Array.prototype.forEach.call(imgs, wrapImageWithA); 5 | new SimpleLightbox('.image-previewer', { /* options */ }); 6 | }); 7 | 8 | function wrapImageWithA(img) { 9 | var parent = img.parentElement; 10 | if (parent.nodeName === 'A'){ 11 | return 12 | } 13 | var a = document.createElement('a'); 14 | a.className = 'image-previewer'; 15 | a.href = img.src; 16 | a.appendChild(img); 17 | parent.appendChild(a); 18 | } 19 | }()); 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | # Use 4 spaces for the Python files 13 | [*.py] 14 | indent_size = 4 15 | max_line_length = 80 16 | 17 | # The JSON files contain newlines inconsistently 18 | [*.json] 19 | insert_final_newline = ignore 20 | 21 | # Minified JavaScript files shouldn't be changed 22 | [**.min.js] 23 | indent_style = ignore 24 | insert_final_newline = ignore 25 | 26 | # Makefiles always use tabs for indentation 27 | [Makefile] 28 | indent_style = tab 29 | 30 | # Batch files use tabs for indentation 31 | [*.bat] 32 | indent_style = tab 33 | 34 | [*.md] 35 | trim_trailing_whitespace = false 36 | 37 | -------------------------------------------------------------------------------- /book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | title = "敏捷的用户故事方法" 3 | description = "关于在敏捷开发中如何进行用户故事开发和编写的一些方法和实践" 4 | authors = ["Tony Deng"] 5 | language = "zh-CN" 6 | multilingual = true 7 | src = "." 8 | 9 | [preprocessor.theme] 10 | pagetoc = true 11 | turn-off = true 12 | root-font-size = "70%" 13 | sidebar-width = "120px" 14 | pagetoc-width = "15%" 15 | content-max-width = "80%" 16 | 17 | [output.html] 18 | additional-css = ["theme/simple-lightbox.css", "theme/theme.css", "theme/pagetoc.css"] 19 | additional-js = ["theme/simple-lightbox.min.js","theme/theme.js", "theme/pagetoc.js"] 20 | git-repository-url = "https://github.com/tonydeng/user-stories-applied" 21 | git-repository-icon = "fa-github" 22 | 23 | [output.html.fold] 24 | enable = false 25 | level = 0 26 | 27 | [output.html.playground] 28 | editable = true 29 | 30 | [output.html.print] 31 | enable = false 32 | 33 | [build] 34 | build-dir = "_book" 35 | -------------------------------------------------------------------------------- /deploy-to-github.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | setup_git(){ 4 | git config --global user.name "Tony Deng" 5 | git config --global user.email "wolf.deng@gmail.com" 6 | git remote add origin-pages https://github.com/tonydeng/user-stories-applied.git > /dev/null 2>&1 7 | } 8 | 9 | build_gitbook(){ 10 | gitbook install 11 | gitbook build 12 | } 13 | 14 | commit_website_files(){ 15 | git checkout -b gh-pages 16 | git pull origin-pages gh-pages --rebase 17 | cp -R _book/* . 18 | git clean -fx node_modules 19 | git clean -fx _book 20 | git add . 21 | git commit -a -m "deploy books" 22 | } 23 | 24 | upload_files(){ 25 | git push --quiet --set-upstream origin-pages gh-pages 26 | } 27 | 28 | clean_env(){ 29 | git remote rm origin-pages 30 | git checkout master 31 | git branch -D gh-pages 32 | } 33 | 34 | setup_git 35 | build_gitbook 36 | commit_website_files 37 | upload_files 38 | clean_env -------------------------------------------------------------------------------- /.github/workflows/mdbook-deploy.yml: -------------------------------------------------------------------------------- 1 | name: mdbook deploy github pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Setup mdBook 15 | uses: peaceiris/actions-mdbook@v1 16 | with: 17 | mdbook-version: 'latest' 18 | 19 | - name: Setup mdbook-theme latest 20 | run: | 21 | echo $PWD >> $GITHUB_PATH 22 | 23 | - run: mdbook build 24 | 25 | - name: Deploy 26 | uses: peaceiris/actions-gh-pages@v3 27 | with: 28 | github_token: ${{ secrets.GITHUB_TOKEN }} 29 | publish_dir: ./_book 30 | force_orphan: true 31 | user_name: 'github-actions[bot]' 32 | user_email: 'github-actions[bot]@users.noreply.github.com' 33 | commit_message: ${{ github.event.head_commit.message }} 34 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins" : [ "anchors", "ga", "github-buttons"], 3 | "pluginsConfig": { 4 | "ga": { 5 | "token": "UA-80673215-1" 6 | }, 7 | "github-buttons": { 8 | "buttons": [ 9 | { 10 | "user": "tonydeng", 11 | "repo": "user-stories-applied", 12 | "type": "star", 13 | "size": "small", 14 | "count": true 15 | }, 16 | { 17 | "user": "tonydeng", 18 | "type": "follow", 19 | "size": "small", 20 | "width": "170", 21 | "count": true 22 | }, { 23 | "user": "tonydeng", 24 | "type": "watch", 25 | "size": "small", 26 | "count": true 27 | } 28 | ] 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 敏捷的用户故事方法 2 | 3 | [](https://travis-ci.org/tonydeng/user-stories-applied) 4 | 5 |  6 | 7 | 人的大脑同时处理事物的能力是有限的,传统的产品`PRD`既要思考内容是否表述了产品的真实意图,还要想着符合公司对于格式、用词等等方面的要求,这是一件非常烦琐、心生厌烦的事情。更何况在写的同时还总是回响着一个声音:“这东西写出来有人看吗?” 8 | 9 | 在维基百科上对用户故事是这样描述的: 10 | 11 | > 使用用户故事的目的,以更快的速度、更少的消耗应对现实世界需求的快速变化。 12 | 13 | 但是,我们使用用户故事,不仅仅是为了快。 14 | 15 | 从大脑的认知的角度来看,面对同样一个主题,通过多种不同的方式、不同活动的刺激,大脑才能深刻的理解和记忆。 16 | 17 | 著名的极限编程创始人之一`Ron Jeffries`提出了`3C`原则: 18 | 19 | - `Card`: 使用卡片记录用户故事,一方面可以隐藏底层细节,另一方面也方便各方人员在白板上将其移动,以整体图形的方式将与客户需求有关的内容深深印在团队脑海中,更不用说这样给项目规划带来的好处。 20 | - `Conversation`:对话是为了促进团队与客户之间的沟通,让大家谈论需求,大声说出来,这种活动也调动了大脑不同区域,让人们能把相关内容学的更快,记得更牢,同时还促进团队和客户之间的沟通,加强人际联系。 21 | - `Confirmation`:用户故事确认则是以反复的方式,与用户确认某个具体使用场景中的关键细节,从而不会导致遗漏。 22 | 23 | 一个好的用户故事,应该符合上述的`3C`原则。 24 | 25 | 26 | 另外,用户故事、计划会议等类似非技术实践,实施起来可能并不复杂,但是必须要结合`TDD`、持续集成、重构等技术实现,否则想要产生高质量的代码就是空谈。 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: stable 3 | before_install: 4 | - export TZ='Asia/Shanghai' 5 | install: 6 | - npm install -g gitbook 7 | - npm install -g gitbook-cli 8 | - gitbook install 9 | script: 10 | - gitbook build 11 | 12 | branches: 13 | only: 14 | - master 15 | 16 | deploy: 17 | provider: pages 18 | skip_cleanup: true 19 | github-token: 20 | secure: 1hdGpqo5g0DQ5ZphQO2mMz/rtVG3QUJRCA/a/rX9FMXe8rBl0MdB4pHjBW0kUJ37c7bN4Xq15b1d3vEDlHWdRJWHtbLg4A/ePxpUcqfaB4Fz42Wq+/VYezEvTLceCk9ruMiSWAcjViFrCPI7rRysF02U8kRgP12hrMVty5Zth4hFbMEjp3iJPXSLPvD1GqysedZjXqcxDqRixNJo/x0g9OfoHuKRFV8K0f4PlMQgEhqm2Y6D8otoDKUNPcOl8eBswW4Ph1nbzxbZFwfeTaZXlDQVqkbI9yNuN6r5JGqdH6F211nb9qe5qmWfft1OTY1KMKypLZhKMpaoMHtedIpdQtbiKRun9Vh7Za943LKTk2ZuAjtTMUF6aGVg4QjsTEdtwSpE4+LOcXtCPTJXFt28jftoqiK/w/PlekVxjvtTsgrW/mFz93LPZzBewulE9VWTlaXzxaGlUbOUSUO9nx6WN+4y596GIT5bWM+sCO9+qOrKUrlQDvzY/gQeEyBu3jqjgMbY7UM3ve+OmmAdbxawSY6qGVcatarjtYKVWjsE0eRUpNzTI1XTJP4NZyWxCjyKK6Wo4qozTuZF2hxwNqDBuaSLut2Ga+QoHBv99E8gEZmWoARBYBO2K7Oe9c6PvTsQzu6prDGQ+CjCdsB5sZGVv4M3msTLMuB1QnZmnWDN4Qo= 21 | local_dir: _book 22 | fqdn: tonydeng.github.io 23 | name: Travis CI 24 | email: travis@travis-ci.org 25 | on: 26 | branch: master -------------------------------------------------------------------------------- /theme/pagetoc.css: -------------------------------------------------------------------------------- 1 | /* src: https://github.com/JorelAli/mdBook-pagetoc */ 2 | 3 | @media only screen and (max-width:1439px) { 4 | .sidetoc { 5 | display: none; 6 | } 7 | } 8 | 9 | @media only screen and (min-width:1440px) { 10 | main { 11 | position: relative; 12 | } 13 | .sidetoc { 14 | margin-left: auto; 15 | margin-right: auto; 16 | /* left: calc(90% + (var(--content-min-width))/4 - 110px); */ 17 | left: 101%; 18 | position: absolute; 19 | font-size: var(--pagetoc-fontsize); 20 | } 21 | .pagetoc { 22 | position: fixed; 23 | width: var(--pagetoc-width); 24 | } 25 | .pagetoc a { 26 | border-left: 1px solid var(--sidebar-bg); 27 | /* color: var(--fg); */ 28 | /* color: var(--sidebar-fg); */ 29 | color: var(--links); 30 | display: block; 31 | padding-bottom: 5px; 32 | padding-top: 5px; 33 | padding-left: 10px; 34 | text-align: left; 35 | text-decoration: none; 36 | font-weight: normal; 37 | background: var(--sidebar-bg); 38 | } 39 | .pagetoc a:hover, 40 | .pagetoc a.active { 41 | background: var(--sidebar-bg); 42 | /* color: var(--sidebar-fg); */ 43 | color: var(--sidebar-active); 44 | font-weight: bold; 45 | font-size: var(--pagetoc-fontsize); 46 | } 47 | } -------------------------------------------------------------------------------- /theme/pagetoc.js: -------------------------------------------------------------------------------- 1 | // src: https://github.com/JorelAli/mdBook-pagetoc 2 | 3 | // Un-active everything when you click it 4 | Array.prototype.forEach.call(document.getElementsByClassName("pagetoc")[0].children, function(el, i) { 5 | el.addEventHandler("click", function() { 6 | Array.prototype.forEach.call(document.getElementsByClassName("pagetoc")[0].children, function(el, i) { 7 | el.classList.remove("active"); 8 | }); 9 | el.classList.add("active"); 10 | }); 11 | }); 12 | 13 | var updateFunction = function() { 14 | 15 | var id; 16 | var elements = document.getElementsByClassName("header"); 17 | Array.prototype.forEach.call(elements, function(el, i) { 18 | if (window.pageYOffset >= el.offsetTop) { 19 | id = el; 20 | } 21 | }); 22 | 23 | Array.prototype.forEach.call(document.getElementsByClassName("pagetoc")[0].children, function(el, i) { 24 | el.classList.remove("active"); 25 | }); 26 | 27 | Array.prototype.forEach.call(document.getElementsByClassName("pagetoc")[0].children, function(el, i) { 28 | if (id.href.localeCompare(el.href) == 0) { 29 | el.classList.add("active"); 30 | } 31 | }); 32 | }; 33 | 34 | // Populate sidebar on load 35 | window.addEventListener('load', function() { 36 | var pagetoc = document.getElementsByClassName("pagetoc")[0]; 37 | var elements = document.getElementsByClassName("header"); 38 | Array.prototype.forEach.call(elements, function(el, i) { 39 | var link = document.createElement("a"); 40 | 41 | // Indent shows hierarchy 42 | var indent = ""; 43 | switch (el.parentElement.tagName) { 44 | case "H2": 45 | indent = "20px"; 46 | break; 47 | case "H3": 48 | indent = "40px"; 49 | break; 50 | case "H4": 51 | indent = "60px"; 52 | break; 53 | default: 54 | break; 55 | } 56 | 57 | link.appendChild(document.createTextNode(el.text)); 58 | link.style.paddingLeft = indent; 59 | link.href = el.href; 60 | pagetoc.appendChild(link); 61 | }); 62 | updateFunction.call(); 63 | }); 64 | 65 | 66 | 67 | // Handle active elements on scroll 68 | window.addEventListener("scroll", updateFunction); -------------------------------------------------------------------------------- /ch6/README.md: -------------------------------------------------------------------------------- 1 | # 6. 用户故事验收测试 2 | 3 |  4 | 5 | **写验收测试的好处有很多,其中之一就是很多客户和开发人员讨论的很多细节可以通过验收测试记录下来**。 6 | 7 | 比起写冗长的需求列表,像“系统应该...”,可以用测试来充实很多用户故事的细节。 8 | 9 | 整理验收测试是基本是两步流程: 10 | 11 | - 第一,将测试要点记录在故事卡的背面,任何时候发现新的测试,都可以记录到故事卡的背面 12 | - 第二,将测试要点变成全面的测试,这些测试可以用来演示故事已正确、完整的实现 13 | 14 | 比如,一个记录在故事卡背面的测试要点的例子,“公司可以用信用卡支付发布工作的费用”,这个故事卡的背面可能一下这些测试要点: 15 | 16 | - 用Visa信用卡、万事达信用卡和运通卡测试。(通过) 17 | - 用大莱卡测试。(失败) 18 | - 用正确的、错误的和空的卡测试 19 | - 用过期的信用卡测试 20 | - 测试不同的交易金额(包括超出信用卡额度限制) 21 | 22 | 这些测试要点记录了客户提出的一些假设。 23 | 24 | > 假定在招聘网站的例子中的客户写了一个故事“求职者可以查看指定工作的详细信息”。 25 | > 26 | > 客户和开发人员讨论这个故事,确定一些需要显示的一些工作信息 -- 职位名称,描述,工作地点,薪水范围,如何申请等等。 27 | > 28 | > 然而,可以了解并不是所有公司都会提供所有这些信息,所以他希望网站能够自动处理未填的数据。 29 | > 30 | > 比如,没有提供薪水信息,客户甚至不希望提供“薪水范围”标签出现在屏幕上。 31 | > 32 | > 这应该在一个测试里反映,因为程序员可能会假定系统发布工作模块要求所有工作都提供薪水信息。 33 | 34 | **验收测试也提供了确认故事是否被完整实现的基本标准**。有了这样的标准,我们就知道什么时候某件事算是做完了,这是避免了花太多或太少的时间和精力的最好方法。 35 | 36 | > 举个生活的例子,我妻子烤蛋糕时,她的验收标准就是在蛋糕里插一根牙签。如果牙签拿出来是赶紧的,那么蛋糕就算是做好了。 37 | > 而我则是将手指插入蛋糕,然后尝尝,以此来验收测试她做的蛋糕。 38 | 39 | ## 6.1. 在写代码之前写测试 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 | > 74 | > 否则,程序员很有可能写出不一样的软件行为,如将新扫描的页面插入到当前文档。 75 | 76 | 那如果我们有了这样清晰的期望,我们应该**告诉程序员你的期望**。 77 | 78 | 比如,我们可以这样描述我们的期望。 79 | 80 | > 用户可以扫描页面并将其插入新的文档。如果已经打开一个稳定,那么程序应该提示用户并关闭当前文档。 81 | 82 | ## 6.2. 客户定义测试 83 | 84 | 既然软件时用来实现用户的愿景,验收测试当然就应当由客户来定义。 85 | 86 | 客户可以和程序员和测试人员合作创建测试,但是**客户至少应该给我们详细指出一些测试,用以验证故事的实现是正确的**。 87 | 88 | 另外,一个开发团队(特别是有资深测试人员的)经常还会定义其他的测试。 89 | 90 | ## 6.3. 测试是过程的一部分 91 | 92 | 最近我和一家公司一起工作,这里测试人员对软件的理解都来自于程序员。 93 | 94 | > 程序员为新功能编写了代码,他们向测试人员解释这个功能,然后测试人员严重程序是否表现出所描述的行为。 95 | 96 | > 一般情况下,程序都能通过测试,但轮到用户开始使用时,却总出现这样那样的问题。 97 | 98 | > **问题当然是出自测试人员总是按照程序员的描述去测试**。如果没有客户或用户的参与,我们不会真正从他们的角度来测试软件。 99 | 100 | **测试是开发过程中的一部分,而不是在编码完成后要做的事**,这点对使用用户故事非常重要。 101 | 102 | 一般情况下,产品经理和测试人员共同负责列出详细的测试。**产品经理带来驱动项目的公司目标的知识;测试人员则带来了怀疑的心态**。 103 | 104 | 在一轮迭代开始阶段,他们应该一起列出尽可能多的测试。但这还不够,也不是他们每周碰一次就足够了。随着故事细节逐步展现,往往又能找出更多的测试。 105 | 106 | ## 6.4. 多少测试才算多? 107 | 108 | **只要这些测试还在继续为故事增加价值和使它更加清晰,客户就应该继续写测试**。 109 | 110 | > 如果针对“不能用过期万事达卡付费”这种情况已经写了一个测试,那就没有必要再为`Visa`卡写同样的测试。 111 | 112 | 同时记住,一个优秀的开发团队会为很多详细的用例写单元测试。 113 | 114 | > 例如,开发团队应该制定能识别2月30日和6月31日不合法日期的单元测试。 115 | 116 | 客户不负责定义所有可能的测试。可以**应该更专注于那些能向开发团队说明故事意图的测试**。 117 | 118 | ## 6.5. 验收测试 119 | 120 | 客户团队负责引领系统的开发,而**验收测试则向客户演示软件是可以接受的**。 121 | 122 | 这意味着客户团队应该执行验收测试。 123 | 124 | 至少,**在每轮迭代结束时应该执行验收测试**。因为**每轮迭代产生的可工作的代码在接下来的迭代开发中可能遭到破坏**,所以每轮迭代都要**执行以往迭代的所有验收测试是非常重要的**。 125 | 126 | 这样,每轮迭代都要花更多的时间来执行验收测试。如果可能,开发团队应该自动化部分或全部验收测试。 127 | 128 | ## 6.6. 测试类型 129 | 130 | 测试类型有很多,客户和开发团队**共同确保系统测试涵盖了项目所需要的不同类型的测试**。 131 | 132 | 对于大多数的系统来说,故事测试主要是功能性测试,用来确定应用程序是如预期一样的运行。不过,也应当考虑其他类型的测试。 133 | 134 | 示例如下: 135 | 136 | - **用户交互测试**,确保所有用户交互组件如期工作 137 | - **可用性测试**,确保程序好用 138 | - **性能测试**,测量应用程序在各种负荷下的工作状况 139 | - **压力测试**,使应用程序在用户和事务的极限情况或其他任何让应用程序处在压力的情况下运行 140 | 141 | **测试的是缺陷,而不是覆盖率** 142 | 143 | 在一个敏捷的、由故事驱动的项目中,测试并不像很多团队那样是一个对抗性的活动。发现缺陷时,不应该有“被我逮到了吧”这样的心态。 144 | 145 | 在敏捷开发中,若有缺陷直到系统投产的时候才被发现,团队成员是不应该互相推卸责任的。高度协作的团队以及“我们共同负责”的心态能防范这种事情的发生。 146 | 147 | 在敏捷项目中,**测试的目的是发现并消除缺陷**,所以没有必要追求100%的代码覆盖率或测试所有边界条件。我们运用我们的直觉、知识和过去的经验来指导测试。 148 | 149 | 选择最合适的人来执行测试。客户应定义验收测试,但是需要开发人员和专职测试人员的帮助和信息。 150 | 151 | 随着时间的推移,**通过频繁的沟通和观察哪些类型的测试经常出现问题**,项目中所有人都可以知道测试**重点在哪些地方**。 152 | 153 | ## 6.7. 职责 154 | 155 | ### 客户团队职责 156 | 157 | - 负责编写验收测试 158 | - 负责执行验收测试 159 | 160 | ### 开发人员职责 161 | 162 | - 若团队觉得有需要,则负责实现自动化验收测试 163 | - 开始开发一个新的故事时,负责考虑更多的验收测试 164 | - 负责为代码做单元测试,是验收测试不必顾及故事的每个细节 165 | 166 | ## 6.8. 小结 167 | 168 | - 验收测试可用用来记录客户和开发人员讨论的很多细节 169 | - 验收测试记录了有关故事的一些假设,这些假设可能还没有和开发人员讨论过 170 | - 验收测试提供了检查故事是否被完整实现的基本标准 171 | - 验收测试应由客户团队来编写而不是开发人员 172 | - 验收测试应该在程序员写代码之前就写好 173 | - 如果新的测试对阐明故事的细节或意图没有任何帮助,就不用在写了 174 | -------------------------------------------------------------------------------- /theme/css/general.css: -------------------------------------------------------------------------------- 1 | /* Base styles and content styles */ 2 | 3 | @import 'variables.css'; 4 | 5 | :root { 6 | /* Browser default font-size is 16px, this way 1 rem = 10px */ 7 | font-size: 70%; 8 | } 9 | 10 | html { 11 | font-family: "Open Sans", sans-serif; 12 | color: var(--fg); 13 | background-color: var(--bg); 14 | text-size-adjust: none; 15 | } 16 | 17 | body { 18 | margin: 0; 19 | font-size: 1.5rem; 20 | overflow-x: hidden; 21 | } 22 | 23 | code { 24 | font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace !important; 25 | font-size: 0.9em; /* please adjust the ace font size accordingly in editor.js */ 26 | } 27 | 28 | /* Don't change font size in headers. */ 29 | h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { 30 | font-size: unset; 31 | } 32 | 33 | .left { float: left; } 34 | .right { float: right; } 35 | .boring { opacity: 0.6; } 36 | .hide-boring .boring { display: none; } 37 | .hidden { display: none !important; } 38 | 39 | h2, h3 { margin-top: 2.5em; } 40 | h4, h5 { margin-top: 2em; } 41 | 42 | .header + .header h3, 43 | .header + .header h4, 44 | .header + .header h5 { 45 | margin-top: 1em; 46 | } 47 | 48 | h1:target::before, 49 | h2:target::before, 50 | h3:target::before, 51 | h4:target::before, 52 | h5:target::before, 53 | h6:target::before { 54 | display: inline-block; 55 | content: "»"; 56 | margin-left: -30px; 57 | width: 30px; 58 | } 59 | 60 | /* This is broken on Safari as of version 14, but is fixed 61 | in Safari Technology Preview 117 which I think will be Safari 14.2. 62 | https://bugs.webkit.org/show_bug.cgi?id=218076 63 | */ 64 | :target { 65 | scroll-margin-top: calc(var(--menu-bar-height) + 0.5em); 66 | } 67 | 68 | .page { 69 | outline: 0; 70 | padding: 0 var(--page-padding); 71 | margin-top: calc(0px - var(--menu-bar-height)); /* Compensate for the #menu-bar-hover-placeholder */ 72 | } 73 | .page-wrapper { 74 | box-sizing: border-box; 75 | } 76 | .js:not(.sidebar-resizing) .page-wrapper { 77 | transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */ 78 | } 79 | 80 | .content { 81 | overflow-y: auto; 82 | padding: 0 10px; 83 | padding-bottom: 50px; 84 | } 85 | .content main { 86 | margin-left: 2%; 87 | margin-right: 2%; 88 | max-width: var(--content-max-width); 89 | } 90 | .content p { line-height: 1.45em; } 91 | .content ol { line-height: 1.45em; } 92 | .content ul { line-height: 1.45em; } 93 | .content a { text-decoration: none; } 94 | .content a:hover { text-decoration: underline; } 95 | .content img, .content video { max-width: 100%; } 96 | .content .header:link, 97 | .content .header:visited { 98 | color: var(--fg); 99 | } 100 | .content .header:link, 101 | .content .header:visited:hover { 102 | text-decoration: none; 103 | } 104 | 105 | table { 106 | margin: 0 auto; 107 | border-collapse: collapse; 108 | } 109 | table td { 110 | padding: 3px 20px; 111 | border: 1px var(--table-border-color) solid; 112 | } 113 | table thead { 114 | background: var(--table-header-bg); 115 | } 116 | table thead td { 117 | font-weight: 700; 118 | border: none; 119 | } 120 | table thead th { 121 | padding: 3px 20px; 122 | } 123 | table thead tr { 124 | border: 1px var(--table-header-bg) solid; 125 | } 126 | /* Alternate background colors for rows */ 127 | table tbody tr:nth-child(2n) { 128 | background: var(--table-alternate-bg); 129 | } 130 | 131 | 132 | blockquote { 133 | margin: 20px 0; 134 | padding: 0 20px; 135 | color: var(--fg); 136 | background-color: var(--quote-bg); 137 | border-top: .1em solid var(--quote-border); 138 | border-bottom: .1em solid var(--quote-border); 139 | } 140 | 141 | 142 | :not(.footnote-definition) + .footnote-definition, 143 | .footnote-definition + :not(.footnote-definition) { 144 | margin-top: 2em; 145 | } 146 | .footnote-definition { 147 | font-size: 0.9em; 148 | margin: 0.5em 0; 149 | } 150 | .footnote-definition p { 151 | display: inline; 152 | } 153 | 154 | .tooltiptext { 155 | position: absolute; 156 | visibility: hidden; 157 | color: #fff; 158 | background-color: #333; 159 | transform: translateX(-50%); /* Center by moving tooltip 50% of its width left */ 160 | left: -8px; /* Half of the width of the icon */ 161 | top: -35px; 162 | font-size: 0.8em; 163 | text-align: center; 164 | border-radius: 6px; 165 | padding: 5px 8px; 166 | margin: 5px; 167 | z-index: 1000; 168 | } 169 | .tooltipped .tooltiptext { 170 | visibility: visible; 171 | } 172 | 173 | .chapter li.part-title { 174 | color: var(--sidebar-fg); 175 | margin: 5px 0px; 176 | font-weight: bold; 177 | } -------------------------------------------------------------------------------- /theme/simple-lightbox.css: -------------------------------------------------------------------------------- 1 | /*! 2 | By André Rinas, www.andrerinas.de 3 | Documentation, www.simplelightbox.de 4 | Available for use under the MIT License 5 | Version 2.5.0 6 | */ 7 | body.hidden-scroll { 8 | overflow: hidden; } 9 | 10 | .sl-overlay { 11 | position: fixed; 12 | left: 0; 13 | right: 0; 14 | top: 0; 15 | bottom: 0; 16 | background: #fff; 17 | opacity: 0.7; 18 | display: none; 19 | z-index: 1035; } 20 | 21 | .sl-wrapper { 22 | z-index: 1040; } 23 | .sl-wrapper * { 24 | box-sizing: border-box; } 25 | .sl-wrapper button { 26 | border: 0 none; 27 | background: transparent; 28 | font-size: 28px; 29 | padding: 0; 30 | cursor: pointer; } 31 | .sl-wrapper button:hover { 32 | opacity: 0.7; } 33 | .sl-wrapper .sl-close { 34 | display: none; 35 | position: fixed; 36 | right: 30px; 37 | top: 30px; 38 | z-index: 10060; 39 | margin-top: -14px; 40 | margin-right: -14px; 41 | height: 44px; 42 | width: 44px; 43 | line-height: 44px; 44 | font-family: Arial, Baskerville, monospace; 45 | color: #000; 46 | font-size: 3rem; } 47 | .sl-wrapper .sl-close:focus { 48 | outline: none; } 49 | .sl-wrapper .sl-counter { 50 | display: none; 51 | position: fixed; 52 | top: 30px; 53 | left: 30px; 54 | z-index: 1060; 55 | color: #000; 56 | font-size: 1rem; } 57 | .sl-wrapper .sl-navigation { 58 | width: 100%; 59 | display: none; } 60 | .sl-wrapper .sl-navigation button { 61 | position: fixed; 62 | top: 50%; 63 | margin-top: -22px; 64 | height: 44px; 65 | width: 22px; 66 | line-height: 44px; 67 | text-align: center; 68 | display: block; 69 | z-index: 10060; 70 | font-family: Arial, Baskerville, monospace; 71 | color: #000; } 72 | .sl-wrapper .sl-navigation button.sl-next { 73 | right: 5px; 74 | font-size: 2rem; } 75 | .sl-wrapper .sl-navigation button.sl-prev { 76 | left: 5px; 77 | font-size: 2rem; } 78 | .sl-wrapper .sl-navigation button:focus { 79 | outline: none; } 80 | @media (min-width: 35.5em) { 81 | .sl-wrapper .sl-navigation button { 82 | width: 44px; } 83 | .sl-wrapper .sl-navigation button.sl-next { 84 | right: 10px; 85 | font-size: 3rem; } 86 | .sl-wrapper .sl-navigation button.sl-prev { 87 | left: 10px; 88 | font-size: 3rem; } } 89 | @media (min-width: 50em) { 90 | .sl-wrapper .sl-navigation button { 91 | width: 44px; } 92 | .sl-wrapper .sl-navigation button.sl-next { 93 | right: 20px; 94 | font-size: 3rem; } 95 | .sl-wrapper .sl-navigation button.sl-prev { 96 | left: 20px; 97 | font-size: 3rem; } } 98 | .sl-wrapper.sl-dir-rtl .sl-navigation { 99 | direction: ltr; } 100 | .sl-wrapper .sl-image { 101 | position: fixed; 102 | -ms-touch-action: none; 103 | touch-action: none; 104 | z-index: 10000; } 105 | .sl-wrapper .sl-image img { 106 | margin: 0; 107 | padding: 0; 108 | display: block; 109 | border: 0 none; 110 | width: 100%; 111 | height: auto; } 112 | @media (min-width: 35.5em) { 113 | .sl-wrapper .sl-image img { 114 | border: 0 none; } } 115 | @media (min-width: 50em) { 116 | .sl-wrapper .sl-image img { 117 | border: 0 none; } } 118 | .sl-wrapper .sl-image iframe { 119 | background: #000; 120 | border: 0 none; } 121 | @media (min-width: 35.5em) { 122 | .sl-wrapper .sl-image iframe { 123 | border: 0 none; } } 124 | @media (min-width: 50em) { 125 | .sl-wrapper .sl-image iframe { 126 | border: 0 none; } } 127 | .sl-wrapper .sl-image .sl-caption { 128 | display: none; 129 | padding: 10px; 130 | color: #fff; 131 | background: rgba(0, 0, 0, 0.8); 132 | font-size: 1rem; 133 | position: absolute; 134 | bottom: 0; 135 | left: 0; 136 | right: 0; } 137 | .sl-wrapper .sl-image .sl-caption.pos-top { 138 | bottom: auto; 139 | top: 0; } 140 | .sl-wrapper .sl-image .sl-caption.pos-outside { 141 | bottom: auto; } 142 | .sl-wrapper .sl-image .sl-download { 143 | display: none; 144 | position: absolute; 145 | bottom: 5px; 146 | right: 5px; 147 | color: #000; 148 | z-index: 1060; } 149 | 150 | .sl-spinner { 151 | display: none; 152 | border: 5px solid #333; 153 | border-radius: 40px; 154 | height: 40px; 155 | left: 50%; 156 | margin: -20px 0 0 -20px; 157 | opacity: 0; 158 | position: fixed; 159 | top: 50%; 160 | width: 40px; 161 | z-index: 1007; 162 | -webkit-animation: pulsate 1s ease-out infinite; 163 | -moz-animation: pulsate 1s ease-out infinite; 164 | -ms-animation: pulsate 1s ease-out infinite; 165 | -o-animation: pulsate 1s ease-out infinite; 166 | animation: pulsate 1s ease-out infinite; } 167 | 168 | .sl-scrollbar-measure { 169 | position: absolute; 170 | top: -9999px; 171 | width: 50px; 172 | height: 50px; 173 | overflow: scroll; } 174 | 175 | .sl-transition { 176 | transition: -moz-transform ease 200ms; 177 | transition: -ms-transform ease 200ms; 178 | transition: -o-transform ease 200ms; 179 | transition: -webkit-transform ease 200ms; 180 | transition: transform ease 200ms; } 181 | 182 | @-webkit-keyframes pulsate { 183 | 0% { 184 | transform: scale(0.1); 185 | opacity: 0.0; } 186 | 50% { 187 | opacity: 1; } 188 | 100% { 189 | transform: scale(1.2); 190 | opacity: 0; } } 191 | 192 | @keyframes pulsate { 193 | 0% { 194 | transform: scale(0.1); 195 | opacity: 0.0; } 196 | 50% { 197 | opacity: 1; } 198 | 100% { 199 | transform: scale(1.2); 200 | opacity: 0; } } 201 | 202 | @-moz-keyframes pulsate { 203 | 0% { 204 | transform: scale(0.1); 205 | opacity: 0.0; } 206 | 50% { 207 | opacity: 1; } 208 | 100% { 209 | transform: scale(1.2); 210 | opacity: 0; } } 211 | 212 | @-o-keyframes pulsate { 213 | 0% { 214 | transform: scale(0.1); 215 | opacity: 0.0; } 216 | 50% { 217 | opacity: 1; } 218 | 100% { 219 | transform: scale(1.2); 220 | opacity: 0; } } 221 | 222 | @-ms-keyframes pulsate { 223 | 0% { 224 | transform: scale(0.1); 225 | opacity: 0.0; } 226 | 50% { 227 | opacity: 1; } 228 | 100% { 229 | transform: scale(1.2); 230 | opacity: 0; } } 231 | -------------------------------------------------------------------------------- /ch7/README.md: -------------------------------------------------------------------------------- 1 | # 7. 优秀用户故事准则 2 | 3 |  4 | 5 | 到现在,我们有了一个很好的基础,了解了什么是故事,如何利用拖网式捕捞以及编写故事,如何识别关键的用户角色以及验收测试在其中起到的作用。 6 | 7 | 下面,我们将了解一些额外的**编写优秀故事的准则**。 8 | 9 | ## 7.1. 从目标故事开始 10 | 11 | 在一个大型项目中,尤其是有许多用户角色的项目,"如何确定用户故事"这个事情有时让人**无从下手**。 12 | 13 | 我发现最好的办法是考虑每一个用户角色,**了解用户使用我们软件的目的**。 14 | 15 | 例如,思考一下招聘网站例子中的[求职者](../ch3/#什么是用户角色)角色。他的确有一个最高优先级的目标:**找到一份工作**。 16 | 17 | 但我们可以认为这个目标包括以下目标: 18 | 19 | - 搜索他感兴趣的工作(基于他的技能、期望薪资、工作地点等) 20 | - 自动搜索,以便于不用每次都手动搜索 21 | - 让他的简历可见,以便于招聘公司能搜索到他 22 | - 很容易申请他喜欢的任何工作 23 | 24 | 这些**目标**(实际上是高层次的故事)可以**用来衍生出新的故事**。 25 | 26 | ## 7.2. 切蛋糕 27 | 28 |  29 | 30 | 当面临一个大的故事的时候,通常有许多方法可以将它分解成较小的故事。许多**开发人员首先想到的是将故事按照技术路线分割**。 31 | 32 | 比如,假设团队觉得故事“求职者可以发布简历”在当前这轮迭代中太大了,就必须分割。开发人员可能想沿着技术边界分割,示例如下: 33 | 34 | - 求职者可以填写简历表 35 | - 简历表上的信息被写入数据库 36 | 37 | 在这个案例中,一个故事会在当前迭代中完成,而另一个故事则(很可能)推迟到下一轮迭代里。这种做法的缺陷是,没有一个故事是单独对用户很有用的。 38 | 39 | > 第一个故事说的是求职者可以填写简历表,但数据没有被保持。 40 | > 41 | > 第二个故事说的是从简历表上搜集的数据会写入数据库。如果没有第一个故事提供表格给用户,第二个故事就没有什么价值。 42 | 43 | 44 | 一个更好的办法是换一种方式编写故事,每个故事都提供某种程度的完整(`end-to-end`)的功能。 45 | 46 | > Bill Wake(2003a)将其称之为“切蛋糕”(`slicing the cake`) 47 | 48 |  49 | 50 | 根据这个**切蛋糕原则**,我们可以把故事“求职者可以发布简历”像下面这样分。 51 | 52 | - 求职者可以提交简历,简历上只包括诸如名字、地址、和教育背景这样的基本信息 53 | - 求职者可以提交简历,简历上包括雇主想看的所有信息 54 | 55 | 在编写用户故事时,更倾向编写像一块完整蛋糕那样功能完整的故事。 56 | 57 | 具体有两个原因: 58 | 59 | - 首先,在开发中,**及早涉及软件应用架构的每一层能够有效地降低最后时刻才发现层次架构方面问题的风险**。 60 | - 其次,尽管不十分完美,**即使只提供部分功能,但只要发布的功能可以跑,就可以放心的把应用程序发布给用户使用**。 61 | 62 | ## 7.3. 编写封闭的故事 63 | 64 | `Soren Laueson(2002)`在他的[《Software Requirements》](https://book.douban.com/subject/2696709/)一书中引入了**任务闭包性**的想法。这个想法统一适用于用户故事。 65 | 66 | **一个封闭的故事是指随着一个有意义的目标的实现而结束的故事,能让用户使用后觉得他完成了某个任务**。 67 | 68 | 例如,假设招聘网站项目包含故事“招聘者可以管理他发的招聘广告”,这不是一个闭合的故事。管理他发布的招聘广告是没有办法彻底完成的事情。相反,它是一个持续进行的活动。 69 | 70 | 这个故事可以更好的创建成一个闭合故事的集合。 71 | 72 | - 招聘者可以审核针对他发布的招聘广告发的简历 73 | - 招聘者可以更改招聘广告的过期日期 74 | - 招聘者可以删除不适合的申请 75 | - ...... 76 | 77 | 这种**封闭的每个故事都是原来那个非封闭故事的一部分**。使用完这些封闭故事之后,用户可能会有一种**成就感**。 78 | 79 | **编写封闭故事其实是在互相冲突的各种需求之间权衡的结果**。因为,故事也要小到能做评估,小到可以方便的安排一轮迭代中。 80 | 81 | 但故事也要足够大(粗颗粒的、高层次的、抽象的),从而避免过早捕获当下还不需要的细节。 82 | 83 | ## 7.4. 卡片约束 84 | 85 | `Newkirk`和`Martin`(2001)推荐过一种实践,我觉得它是很有用的。 86 | 87 | 他们引入的实践,是对于任何必须遵守而不需要直接实现的故事,在其故事卡上标识“约束”(`constranint`)。 88 | 89 | 比如这样的故事: 90 | 91 | > **约束:** 系统必须支持最大50个并发用户的峰值 92 | 93 | 其他约束的例子如下: 94 | 95 | - 设计的软件要便于今后实现国际化 96 | - 新系统必须使用我们现有的订单数据库 97 | - 该软件必须能在所有版本的`Windows`系统上运行 98 | - 该系统的无故障运行时间要求达到`99.999%` 99 | - 该软件要很好用 100 | 101 | 尽管约束卡不需要做估算,也不会像普通卡片那样别安排到迭代中,但它们仍然很有用处。至少,可以把约束卡贴在墙上作为提醒。更秒的是,可以编写验收测试来确保系统没有违反约束。 102 | 103 | > 例如,为上面的故事编写测不是一件难事。 104 | 105 | 理想情况下,团队可以在最初几轮迭代中一轮中编写测试,那是系统违反约束的可能性还很小。然后团队可以在后续的迭代中持续运行这些测试。只要可能,就要编写自动化测试来确保系统满足约束。 106 | 107 | 想进一步了解如何约束请参考[第16章 其他话题](../ch16/) 108 | 109 | ## 7.5. 根据实现时间来确定故事规模 110 | 111 | 你想把注意力集中在最需要的地方。通常,这意味着你必须更加关注在不久的将来发生的事情而不是更长远的事情。 112 | 113 | 对于故事,你可以**根据故事的实现时间跨度写出不同层级的故事**。举例来说,对于接下来几次迭代的故事,将按照可以计划进入这些迭代的大小来写,而更远迭代的故事可能会更大且精确度更低。 114 | 115 | > 例如,假设在最高层级上,我们确定BigMoneyJobs网站将包含4个故事。 116 | 117 | - 求职者可以发布简历。 118 | - 求职者可以搜索职位空缺。 119 | - 招聘人员可以发布招聘信息。 120 | - 招聘人员可以搜索简历。 121 | 122 | **客户决定第一次迭代侧重于允许用户发布简历。只有在添加大量简历发布功能之后,才会关注搜索职位,发布职位空缺和搜索简历**。这意味着项目团队和客户将开始进行关于故事“求职者可以发布简历”的对话。 123 | 124 | 通过这些对话来扩展这个故事的细节,其他3个高层级的故事将被单独留下。 125 | 126 | 一个可能的故事列表将变成下面这样。 127 | 128 | - 求职者可以向该网站添加新的简历。 129 | - 求职者可以编辑已经在网站上的简历。 130 | - 求职者可以从网站上删除自己的简历。 131 | - 求职者可以将简历状态设置为不活跃。 132 | - 求职者可以将简历设置为隐藏某些雇主。 133 | - 求职者可以看到自己的简历被查看了多少次。 134 | 135 | 关于发布简历的故事…… 136 | 137 | - 求职者可以搜索职位空缺。 138 | - 招聘人员可以发布职位空缺。 139 | - 招聘人员可以搜索简历。 140 | 141 | 在写故事时,**要充分利用故事的灵活性,以便应用于各个层级**。 142 | 143 | ## 7.5. 不要过早涉及用户界面 144 | 145 | 困扰软件需求方法的问题之一是将需求与解决方案混在一起。也就是说,在说明一个需求时,也要明确说明或者暗示解决方案,通常这种解决方案就体现在用户界面上。但是这种方式会将需求和解决方案混在一起,无法清晰的将需求表达清楚,并且会在项目前期有大量的用户界面设计的工作需要进行设计和澄清。 146 | 147 | 作为PO,你会希望尽可能把用户界面和故事分隔开。 148 | 149 | > **用户故事示例:** 打印对话框允许用户编辑打印机列表。**用户可以从打印机列表中添加或删除打印机,用户可以通过自动搜索或者手动指定DNS打印机名称或者IP地址添加打印机。高级搜索选项还允许用户在限制指定的IP地址和子网范围内搜索。** 150 | 151 | 如上面的用户故事示例中就会包含了太多用户界面的细节。这个故事的实现者和用户就会被告知了有打印对话框、打印机列表以及至少4种搜索方式。最终,用户界面细节将不可避免地塞进故事。随着软件变得越来越完整,故事从完全的新功能实现转移到功能的修改或者扩展时,这种情况就会导致产生大量的用户故事或需求变更。 152 | 153 | ## 7.6. 需求不止故事 154 | 155 | 尽管用户故事是一种非常灵活的格式,可以很好地描述许多系统的许多功能,但它们并不适用于所有的系统。如果需要以非用户故事的形式描述一些需求(如产品设计原型、PRD等),那就以相应的产出要求来进行对应的需求制品的生产和输出。 156 | 157 | > 例如,用户界面通常使用具有大量界面截图的文档进行描述。 158 | 159 | 同样,除了用户故事之外,你可能需要文档记录并对重要系统之间的接口达成一致,尤其是有外部供应商参与开发时。 160 | 161 | **如果发现系统的某个方面可以从不同格式的需求描述中受益,请使用该格式来描述相应的需求。** 162 | 163 | ## 7.7. 故事中包括用户角色 164 | 165 | 如果项目团队已经识别了用户角色,那么他们在编写故事时就应该使用这些角色。因此,不要写成“用户可以发布自己的简历”,应该写成“求职者可以发布自己的简历”。 166 | 167 | 这种差异很小,但以这种方式编写故事会让用户存在于开发人员的头脑中。开发人员不会去思考平淡的、不形象的、可替换的用户,他们会想象真实的、具象的用户,从而开发出满足用户需求的软件。 168 | 169 | 英国公司Connextra[插图]是极限编程的早期采用者之一,他们在2001年使用简短的模板将角色融入故事中。每个故事都是用以下格式编写的:**我作为(角色)想要(功能)以便(商业价值)** 170 | 171 | 你可能想试试这个模板或者使用你自己的模板。“role-feature-reason”这样的模板可以帮助区分重要的和无价值的故事。 172 | 173 | ## 7.8. 为一个用户编写故事 174 | 175 | 如果只为单个用户编写故事,故事通常最具有可读性。对于许多故事来说,为一个或者多个用户编写不会有什么差异。但是,对于某些故事,差异可能很大。例如,考虑一下“求职者可以从网站上删除简历”这个故事。这可以解释为,一个求职者可以删除自己的简历,也可能删除其他人的简历。 176 | 177 | 通常情况下,**当你在心中只考虑一个单独用户的故事时,这类问题就会变得清晰起来**。 178 | 179 | 例如,上面的故事可以写成“求职者可以删除简历”。当写成这样时,一个求职者可能会删除其他人简历的问题就变得更加明显,所以故事可以进一步改写为“求职者可以删除自己的简历”。 180 | 181 | ## 7.9. 用主动语态 182 | 183 | 主动语态就像直接走向目的地--清晰、有活力,通常因其简单明了而备受青睐。它非常适合大多数类型的交流,让你的句子听起来自信而充满活力。 184 | 185 | > 示例:香料[宾语]被小贩[主语]卖掉了[动词]: 186 | > 公式:宾语[动作的执行者]+动词[动作]+主语[动作的接受者]。 187 | > 在这里,"香料 "成为重点,而小贩在句子中处于次要地位。 188 | 189 | 用户故事最好使用主动语态来编写,更易于团队和用户来阅读和理解用户故事,能够形成讨论,并达成对用户故事的一致的认知。 190 | 191 | > 例如,不要说“简历可以被求职者发布”,而应该说“求职者可以发布简历”。 192 | 193 | ## 7.10. 客户编写故事 194 | 195 | 理想情况下,客户会编写故事。 196 | 197 | 在许多项目中,开发人员可以帮忙编写故事,要么在最初的故事编写工作坊中实际编写,要么向客户建议新的故事。但是,编写故事的责任就在于客户了,而不能很便捷的传递给开发人员。 198 | 199 | 此外,由于客户有责任确定每次迭代的故事优先顺序,因此客户了解每个故事至关重要。做到这一点的最好方法就是客户亲自把故事写出来,但现实情况下是比较难的意见事情,需要我们的PO非常善于挖掘用户需求,并通过条目化的用户故事来客户来确认故事的优先顺序。 200 | 201 | ## 7.11. 不要给故事卡编号 202 | 203 | > 该准则仅适用于使用卡片来编写故事的实践方式。 204 | 205 | 我们第一次使用故事卡时,许多人都想要给卡片进行编号。通常的理由是,这将有助于跟踪个别卡片或者为故事添加一定程度的可追溯性。 206 | 207 | > 例如,当我们发现卡片13上的故事太大时,我们就撕掉卡片13,并用卡片13.1,13.2和13.3替换它。然而,给故事卡编号给流程增加了无谓的开销,并会导致我们抽象地讨论需要形象化的特性。 208 | 209 | 我宁愿讨论“故事添加用户组”,也不想讨论“故事13”,特别不想讨论“故事13.1”。如果觉得不得不对故事卡进行编号,可以尝试在卡片上添加一个简短的标题,并在其他的故事文本中使用这个标题的简写。 210 | 211 | ## 7.12. 不要忘记目的 212 | 213 | 不要忘记,故事卡的主要目的是提示人们讨论该特性。需要注意保持故事的简短性,不要向故事卡中添加更多细节,用它来取代团队的对话讨论。 214 | 215 | ## 扩展阅读 216 | 217 | - [Slicing the Cake - User story slicing](http://tracks.roojoom.com/r/1757) 218 | - [Slicing your development as a multi-layer cake -- Luis Fernando Mizutani](http://www.linkedin.com/pulse/slicing-cake-useful-guidelines-breakdown-development-work-mizutani) 219 | - [Slicing Stories - Agile Business Conference](https://www.agileconference.org/wp-content/uploads/2015/10/How-to-Slice-Product-Backlog-Items-Matt-Roadnight-v1-2.pdf) 220 | - [Software Requirements: Styles & Techniques -- Soren Lauesen](https://www.pearson.com/us/higher-education/program/Lauesen-Software-Requirements-Styles-Techniques/PGM11471.html) 221 | -------------------------------------------------------------------------------- /ch3/README.md: -------------------------------------------------------------------------------- 1 | # 3. 用户角色建模 2 | 3 |  4 | 5 | 在很多项目中,需求分析人员只从一个角度来写用户故事, 这样往往会忽略一些需求(故事),因为有些故事针对的不是系统的一般用户。 6 | 7 | 以**用户为中心的设计**(`user-centered design, Constantine and Lockwood, 1999`)和**交互设计**(`interaction design, Cooper, 1999`)的规则使我们懂得,在编写故事前识别用户角色和虚构人物(`persona`)有很多好处。 8 | 9 | 我们接下来将要讨论如何利用**用户角色**、**角色建模**、**角色映射**和**虚构人物**这些初始步骤来编写更好的故事,开发更好的软件。 10 | 11 | ## 3.1. 用户角色 12 | 13 | ### 用户是谁? 14 | 15 | 假设,我们依然从可以招聘网站谈起,这类网站会有许多中**不同类型的用户**。 16 | 17 | 当我们谈起“用户故事”时,我们说的**用户是谁**? 18 | 19 | >我们是在谈论`张无忌`吗?他现在“骑驴找马”总在留意更好的工作。是`殷离`吗?她是大学应届毕业生,正在找第一份工作。还是`杨逍`?他将接受任何工作,只要那份工作可以让他搬到昆仑山光明顶。或是`韦一笑`?他不讨厌现在的工作,但他觉得是时候换一份工作了。也许我们讨论的是`丁敏君`,她六个月前被裁员了,正在找一份工作地点在峨眉山的工作。 20 | 21 | 或许我们应该考虑一下需要发布工作的公司内的用户? 22 | 23 | > 用户可能是`灭绝师太`,她来自于峨眉派的人力资源部,由她来发布工作信息。或者是`孤鸿子`,他也在峨眉派的人力资源部工作,但是他的职责是审核简历。或者是`黛绮丝`,她是独立的猎头,同时关注好工作和优秀人才。 24 | 25 | 显然,我们不能从单一的角度来编写故事,要让这些故事反应所有这些用户的经历、背景和目标是不现实的。 26 | 27 | - `张无忌`,会计师,可能每个月只上一次我们的网站,以保留他选择的余地 28 | - `杨逍`,服务员,可能想创建一个过滤器。此过滤器可以第一时间通知他光明顶上有新的工作发布。除非我们提供这个功能,要不然他实现不了这个想法。 29 | - `丁敏君`,可能每天花几个小时来寻找工作,并不断扩大她的搜索范围。 30 | - `灭绝师太`和`孤鸿子`他们的公司比较大,有很多职位需要他们填补,那么他们可能要在我们网站上消耗4个小时,甚至更多。 31 | 32 | ### 什么是用户角色 33 | 34 | 虽然用户有着不同的背景,有不同的目标,但我们仍然可以把这些单独的客户进行分组,把每一类作为一种“用户角色(`User Role`)”。 35 | 36 | **用户角色是一组属性的集合,这些属性刻画了一群人的特征以及这群人与系统可能的交互。** 37 | 38 | 我么可以看看之前例子中的用户,将他们进行角色分组: 39 | 40 | | 类型 | 姓名 | 41 | |--|--| 42 | | 求职者 | 韦一笑 | 43 | | 初次求职者 | 殷离 | 44 | | 裁员受害者 | 丁敏君 | 45 | | 工作地点搜索者 | 杨逍 | 46 | | 监视者 | 张无忌 | 47 | | 工作发布者 | 灭绝师太、黛绮丝 | 48 | | 简历阅读者 | 孤鸿子、黛绮丝 | 49 | 50 | > 上面这个表并不是对招聘网站的用户进行角色分组的唯一方式。例如,可以包含诸如兼职者、全职者和合同工等角色。 51 | 52 | 显然,针对不同用户角色的故事之间会有些重复。求职者、初次求职者、裁员受害者、工作地点搜索者和关注者都会使用网站的工作搜索特性,但是他们使用搜索功能的方式和频率可能会不同,针对简历阅读者和工作发布者的故事也可能重复,因为这些角色的目标都是找到好的候选人。 53 | 54 | ## 3.2. 角色建模的步骤 55 | 56 | 我们将使用下面的步骤来**识别、选择有用的用户角色集合**。 57 | 58 | ### 1. 通过头脑风暴,列出初始的用户角色集合 59 | 60 | 为了识别用户角色,客户和开发人员(多多益善)聚集在一个房间里,房间里要有一张大桌子或一堵墙,这样他们就有地方粘贴或固定卡片。 61 | 62 | 理想情况是在项目启动时,把团队所有成员聚集在一起进行用户角色建模,但这并不是必须的,只要有一定数量的开发人员和客户一同参与,会议往往能取得成功。 63 | 64 | 每个参与者从桌子中间堆放的记录卡中取出一叠(即打算用电子文档来记录用户角色,也应该从手写记录开始)。每个人先在卡片上写下角色名称,然后把他们放在桌子上,或者贴在墙上。 65 | 66 | 放上新的角色卡片后,作者只说出新角色的名字,不做其它任何事情。这个会议只做头脑风暴,无需对卡片进行讨论,也不需要对角色进行评估。 67 | 68 | 每个人要做的至少尽量在卡片上写出自己想到的角色。不需要让大家轮流给出新的角色。想到一个新角色就把它写到卡片上。 69 | 70 | 在头脑风暴过程中,房间里会充满书写卡片的声音,偶尔夹杂着放置新卡片或朗读角色名称的声音。这样继续下去直到大家没有新的进展,并且很难再相出新的角色。 71 | 72 | 尽管此时有可能还没有找到所有的角色,但其实已经很接近了。这样的头脑风暴很少会超过15分钟。 73 | 74 | > 一个用户角色是一个用户 75 | 76 | 对项目的角色进行头脑风暴时,要坚持“**已确认的角色代表的是单一用户**”的原则。 77 | 78 | ### 2. 整理最初的角色集合 79 | 80 | 接下来需要**整理**这些角色了。 81 | 82 | 在桌子上或墙上移动卡片的位置,以表明角色之间的关系。对于有重叠的角色,把它们对应的卡片也重叠到一起。如果角色只有一点点重叠,那么卡片也只重叠一点点。如果角色完全重叠,那么卡片也完全重叠。 83 | 84 |  85 | 86 | > 上面的图中显示了这样的角色之间的关系 87 | 88 | - “大学毕业生”和“初次找工作者”,他们的角色有显著的重叠 89 | - 其他将使用工作搜索功能的人而言,它们的角色卡片也有较小但类似的重叠 90 | - “监视者”的角色卡片与其他卡片仅仅稍有重叠,因为这个角色代表的是那些对现有工作相对满意的人,但他们同时有喜欢留意好的机会 91 | - 右侧角色是“工作发布者”、“招聘人员”和“简历阅读者”的角色卡片,由于“招聘者”既需要发布招聘广告,也会阅读简历 92 | - “管理者”角色也在其中展示,这个代表这个招聘网站的内部用户,他们要支持这个网站的运营 93 | 94 | #### 系统角色 95 | 96 | 尽量坚持一个原则:**用户角色定义的是人,而不是其他外部系统**。如果觉得有必要,可以偶尔引入一个非人物的**系统角色**(non-human user role)。然而,确认用户角色的目的是确保我们很周到的为用户考虑,我们要绝对的、积极地让用户对新系统感到免疫,我们不需要为每一个可以想到的系统用户简历角色,但需要那些能影响项目或成败的角色。 97 | 98 | 由于其他外部系统很少回事我们系统的购买者,它们很少能决定我们系统的成败。 99 | 100 | 不过,事情总有例外,如果觉得加入一个非人物的系统就是有助于思考系统,将它加入也未尝不可。 101 | 102 | ### 3. 整理角色 103 | 104 | 在角色分组完成后,需要试着**整合和合并**角色了。可以从完全重叠的卡片入手。 105 | 106 | 首先,这些卡片的作者描述一下他们的角色名究竟代表什么,在简短的小组讨论之后,再判断这些角色是否等同。如果等同,那么这些角色要么合并成单一的角色(也行可以根据这两个初始的角色名去一个新的名字),要么丢弃掉其中一张角色卡。 107 | 108 | 从[整理最初的角色集合](#2-整理最初的角色集合)的图中可以看到“大学毕业生”和“初次找工作者”这两个角色有很大重叠,基本这两个角色的故事都是相同的,所以决定丢弃“大学毕业生”角色卡。 109 | 110 | 虽然“初次找工作者”、“裁员受害者”、“工作地点搜索者”和“求职者”有显著的重叠,但是还是决定留下它们,是因为这些角色**每个都代表了系统需要满足的重点方面**。这些角色也使得网站的目标也有微妙的不同,这很重要。 111 | 112 | 图的右侧,小组认为**区分“简历发布者”和“简历阅读者”没有什么价值**。所以决定,**“招聘者”这个角色会充分覆盖另外两个角色**,所以这两个角色的卡片被抛弃。然后小组觉得内部招聘者(为某个公司工作)和外部招聘者(为任何公司寻找合适的人选)有所不同。他们为内部招聘者和外部招聘者都写了新的卡片,并将这两个角色当成招聘者角色的特殊版本。 113 | 114 | 除了需要合并重叠的角色外,小组还应该**丢弃那些对系统成功不太重要的角色卡**。例如,“监视者”角色卡代表了哪些只关注工作市场的人,他们可能好几年都不换工作。即使不关注这样的角色,网站也能做的很出色。 115 | 116 | 他们认定**最好能关注那些对公司成功更重要的角色**,比如“求职者”和“招聘者”角色。 117 | 118 | **最终整理的角色卡组合** 119 | 120 |  121 | 122 | ### 4. 提炼角色 123 | 124 | 一旦我们整合好角色,并对角色之间的关系有了一个基本的了解,就可以能通过**给每个角色定义一些特征来建立角色的模型**。 125 | 126 | **角色特征是关于同属于这一类的用户事实或者有用信息。** 127 | 128 | 这里有一些适用于任何角色建模的角色特征: 129 | 130 | - 用户使用软件的频率 131 | - 用户在相应领域的知识水平 132 | - 用户使用计算机和软件的总体水平 133 | - 用户对当前正在开发的软件的熟悉程度 134 | - 用户使用该软件的总体目标。有些用户注重使用的便捷性,有些关注丰富的用户体验,等等。 135 | 136 | 除了这些标准的特征,应该考虑对于正在开发的软件,是否有意向对描述其用户有帮助的特征。例如,对于我们正在设计的招聘网站,可以考虑某个用户是在寻找全职工作还是兼职工作。 137 | 138 | 在确定角色的有趣特征时,可以在角色卡片上写下注释。完成后,可以把角色卡挂在团队的公共区域,用来提示团队成员。 139 | 140 | |用户角色:内部招聘者| 141 | |--| 142 | |1. 不是很擅长使用电脑,但使用网络相当娴熟。| 143 | |2. 不经常使用该软件,但每次使用强度大。| 144 | |3. 他将阅读其他公司招聘广告,以此选择最好的措辞来完成他们的招聘广告。| 145 | |4. 使用简单很重要,但更重要的是,他学会的东西必须在几个月后能够很容易的回想起来。| 146 | 147 | ## 3.3. “虚构人物”和“极端人物” 148 | 149 | 如果之前描述[用户角色建模](#32-角色建模的步骤)都做完了,已经绝对比99%的其他软件团队多费了心思。大部分的团队确实可以到此为止了。 150 | 151 | 不过,还有两个额外的技术值得探讨,因为它们**有助于我们考虑某些系统的用户**。当然,我们会在它们**适用于为项目带来明显的好处是才会引入**。 152 | 153 | ### 虚构人物 154 | 155 | 识别用户角色是一个伟大的飞跃,但对于有些更为重要的用户角色,再进一步为角色创建一个虚构人物是很值得的。 156 | 157 | > 虚构人物是假象的用户角色代表。 158 | 159 | 创建虚构人物不只是在用户角色上加上一个名字。对于虚构人物的描述应当是十分充分的,让团队的每个人都觉得他们知道这个任务。 160 | 161 | 例如,之前我们提到“灭绝师太”,她要为她的公司发布工作信息。那她的描述可能是这样: 162 | 163 | > 灭绝师太在峨眉派的人事部负责招聘工作,该公司是一个高端网络组件制造商。她已经在该公司工作6年。 164 | > 灭绝师太有弹性的时间安排,每周周五她在家工作。 165 | > 灭绝师太对电脑相当在行,她觉得对于自己所使用的软件产品,她几乎都是超级用户。 166 | > 由于灭绝师太年轻时感情上受过伤害,并且一直忙于工作,40多了还没有结婚,也没有男朋友。她对男性的简历会吹毛求疵,要求很高。 167 | > 由于峨眉派几乎一直在扩张,灭绝师太总是在物色优秀的工程师。 168 | > 由于灭绝师太自以为是电脑行家,她对使用的软件和系统要求都很挑剔,容忍度极低。 169 | 170 | 假如选择为项目创建虚构人物,要注意,应当**事先做好充分的市场和目标用户群调查**,要确保虚构人物能够**真正代表产品的目标用户**。 171 | 172 | 我们加上了对灭绝师太的描述,如果有照片就更加生动了,应该找一幅灭绝师太的照片,将它也放入到虚构人物的定义中。 173 | 174 |  175 | 176 | 从用户角色或虚构人物的角度描述会使故事变得更加生动。识别出用户角色,并且可能有一两个虚构人物后,就可以开始从角色和虚构人物的视角来说话,而不是宽泛的“用户”。 177 | 178 | 你可以写一个“用户地点搜索者可以限定他搜索到的工作都在指定的地理区域内”的故事,而不是“用户可以限定他搜索到的工作都在指定的地理区域内”的故事。以这种方式编写的用户故事可以提醒团队想起“杨逍”,他在寻找光明顶的工作。 179 | 180 | 使用用户角色和虚构故事的名字来编写故事,并不意味着其他角色就不能执行那些故事,而是说明在讨论或实现故事时,用特定的用户角色或虚构人物来思考问题,总是有一些好处的。 181 | 182 | ### 极端人物 183 | 184 | `Djajadiningrat`和其他合著者(2000)提出了第二种可以考虑的技术:**考虑新系统的设计是,使用极端人物。** 185 | 186 | 他们用设计`PDA`(掌上电脑)作为例子。他们建议不要只为一个典型的穿着考究、驾驶宝马的管理顾问做设计,系统设计师应该考虑那些有鲜明个性的用户。具体来说,作者建议为毒贩、教皇和穿梭多个男友之间的女子设计`PDA`。 187 | 188 | 考虑极端人物很可能会让你编写出原表可能遗漏的故事。例如,很容易想到毒贩和有多个男友的女子都想要维护多份单独的时间表,以防被警察或男友看见。教皇可能没有那么多保密需求,但可能想要更大字号的字体。 189 | 190 | 使用极端人物可以导致新的故事产生,但是很难事先确定是否应该讲哲学故事包含在产品中。当然,在极端人物投入大量时间也是不值得的,但是你可以尝试一下这两个方法。至少,你可以花几分钟去考虑一下教皇如何使用你的软件,这可能会带来一到两个灵感。 191 | 192 | ## 3.4. 如果有现场用户该如何? 193 | 194 | 即使有真实的用户在办公室现场,咱们使用的用户角色建模仍然是有帮助的。与实际用户一起工作会大大提高交付成果所需要的软件的可能性。 195 | 196 | 然而,即使与实际用户在一起,也无法保证有正确的用户或用户组合。 197 | 198 | 为了降低无法满足重要客户的可能性,即使有内部用户在的二十号,你们还是应该对项目做一些简单的角色建模。 199 | 200 | ## 3.5. 职责 201 | 202 | ### 客户团队职责 203 | 204 | - 负责寻找用户(多多益善),并识别恰当的用户角色 205 | - 负责参与与识别用户角色和虚构人物的过程 206 | - 负责确保软件没有关注不恰当的用户 207 | - 在编写故事时,负责确保每个故事都能和至少一个用户角色或虚构人物联系起来 208 | - 开发软件时,负责考虑不同用户角色对于软件如何运行的不同偏好 209 | - 确保在识别和描述用户角色时,它们只是这个过程中的工具,不应超越作为工具之外的用途 210 | 211 | ### 开发人员职责 212 | 213 | - 负责参与确认用户故事和虚构人物的过程 214 | - 负责理解美国用户角色或虚构人物,以及他们之间的异同 215 | - 开发软件时,负责考虑通的用户角色对于软件运行的不同偏好 216 | - 负责确保在识别和描述用户角色时,它们只是这个过程中的工具,不应超越作为工具之外的任何用途 217 | 218 | ## 3.6. 小结 219 | 220 | - 大部分的项目小组只考虑单一的用户类型。这会导致软件忽略原本需要的一些用户类型 221 | - 为了避免从单一用户的角度编写的所有故事,要识别到软件交互的不同用户角色 222 | - 通过对每个用户角色定义相关特征,可以更清楚地看到不同角色间的不同点 223 | - 对于有些用户角色而言,用代表人物来描述会很有帮助。虚构人物是假想出来的用户角色代表。他们有名字,有照片,还有足够的相关细节,因为对项目成员来说,很真实 224 | - 对于有些应用程序,极端人物可能有助于搜集原本被遗漏的故事 225 | 226 | ## 扩展阅读 227 | 228 | - [User-Centered Design](http://www.e-learning.co.il/home/pdf/4.pdf) 229 | - [Notes on User Centered Design Process (UCD)](https://www.w3.org/WAI/redesign/ucd) 230 | -------------------------------------------------------------------------------- /ch2/README.md: -------------------------------------------------------------------------------- 1 | # 2. 编写故事 2 | 3 |  4 | 5 | **用户故事(`User Story`)是从用户角度对功能的简要描述。** 6 | 7 | 可以使用如下用户故事格式: 8 | 9 | > 作为一个<**角色**>,可以<**活动**>,以便于<**价值**>。 10 | 11 | - 角色: 谁要使用这个功能? 12 | - 活动: 需要执行什么操作? 13 | - 价值: 完成操作后带来什么好处? 14 | 15 | 为了构造好的故事,我们关注**六个特征**。 16 | 17 | 一个优秀的故事应该具备以下特点: 18 | 19 | - 独立的(`Independent`) 20 | - 可讨论的(`Negotiable`) 21 | - 对用户或客户有价值的(`Valuable to Purchasers or Users`) 22 | - 可估计的(`Estimatable`) 23 | - 小的(`Small`) 24 | - 可测试的(`Testable`) 25 | 26 | > 《探索极限编程》和《重构工作手册》的作者`Bill Wake`,建议用英文缩写`INVEST`来代表这六个特征。 27 | 28 | ## 2.1. 独立的 29 | 30 | > 要尽量避免故事间的互相依赖。 31 | 32 | 在对故事排列优先级时,或者使用故事做计划时,故事间的相互依赖会导致一些问题。 33 | 34 | 例如,假设客户团队已经选择了一个高优先级的故事,但它对一个低优先级的故事有依赖,就会出现问题。 35 | 36 | **故事间的依赖也会使做估计变得更加困难。** 37 | 38 | 比如,我们正在开发那个招聘网站,现在需要编写客户公司如何对发布职位进行付费的故事。我们可以编写这些故事。 39 | 40 | - 公司可以用`Visa`信用卡对发布职位进行付费 41 | - 公司可以使用万事达信用卡对发布职位进行付费 42 | - 公司可以使用运通卡对发布职位进行付费 43 | 44 | 假设,开发人员估计支持第一种信用卡(不考虑是哪一种)需要3天,而然后支持第二种和第三种各需要一天时间。对于这些很高相互依赖的故事,你不知道给每个故事估计多少时间--哪个故事应该给3天的估计? 45 | 46 | 当出现上述的依赖时,有两个方法可以绕过这种依赖。 47 | 48 | - 将相互依赖的故事**合并**成一个大的、独立的故事 49 | - 用一个不同的方式来**分割**故事 50 | 51 | 在上述的案例中,合并成一个大的故事是非常可行的,因为这个合并后的故事仅需要5天时间。 52 | 53 | 如果合并后的故事需要远远大于5天时间,那么最好**找一个不同的维度来分割故事**。比如下面的分割方法。 54 | 55 | 1. 客户可以用一种信用卡(`Visa`)支付 56 | 2. 客户可以用另外两种信用卡(万事达、运通卡)支付 57 | 58 | 如果你实在是不想合并故事,也找不到合适的方法来分割它,还有一个简单的方法,就是**在故事上记录两种不同的估计方法**: 59 | 60 | - 如果早于另一个故事的估计 61 | - 如果晚于另一个故事的估计 62 | 63 | ## 2.2. 可讨论的 64 | 65 | > 故事应该是可以讨论的 66 | 67 | 故事不是签署好的合同或软件必须要实现的需求。故事是功能的简短描述,细节在客户团队和开发团队的讨论中产生。因为故的作用是提醒客户团队和开发团队在以后要进行关于需求的对话,它并不是具体的需求本身,因而,他们不需要包含所有的相关细节。 68 | 69 | 如果,我们在编写故事的时候已经知道了一些重要的细节,那么应该在故事上以注释的形式记录这些细节。 70 | 71 | ### 一张提供有额外细节的故事卡 72 | 73 | > 公司可以用信用卡支付发布工作信息的费用。 74 | > 75 | > 备注:接受Visa、万事达和运通卡,考虑支持发现卡。 76 | 77 | 这是一个非常好的故事,因为它提供了适量的信息给开发人员和客户团队进行交流。当一个开发人员开始编码和实现这个故事时,这张故事卡可以提醒他也能根据故事上的注释去询问客户团队是否已经做了决定。 78 | 79 | 理想情况下,不论对话的双方无论是开发人员还是客户人员是否与原来相同,这种对话一般都很容易继续进行。 80 | 81 | **把细节加入故事时,请参考上面这个故事卡。** 82 | 83 | ### 细节太多的故事卡 84 | 85 | > 公司可以用信用卡支付发布工作信息的费用。 86 | > 87 | > 备注:接受Visa、万事达和运通卡,考虑支持发现卡。 88 | > 当支付金额超过100美元时,需要提供信用卡背面的ID号。 89 | > 系统可以根据卡号的前两位数字识别客户使用的是何种类型的信用卡。 90 | > 系统可以保存卡号以备用将来使用。 91 | > 搜集信用卡的过期月份和日子。 92 | 93 | 这个故事有太多的细节(“搜集信用卡过期月份和日期”),同时也合并了本该成为单独故事的部分("系统可以保存卡号以备将来使用") 94 | 95 | 处理这个故事是很困难的。大部分人阅读到这种类型的故事时,会过多的关注本不应该关注的细节。在许多的案例中,过早的制定细节只会带来更多的工作量。 96 | 97 | 另外,这样的故事会更容易让人觉得确定和真实。这会导致一种错觉:这个故事反映了所有细节,没有必要跟用户进行下一步的讨论。 98 | 99 | 如果我们将故事永远提醒开发人员与客户团队进行关于需求的讨论,那么故事包含下面的信息就变的有意义了。 100 | 101 | - 一两句短语,用于提醒开发人员和客户团队进行对话 102 | - 一些注释,用以表明在对会中等待解决的问题 103 | 104 | **讨论中确定的细节将变成测试**,这些测试可以留在故事中。 105 | 106 | ### 修正后的故事,只有故事和将要讨论的内容 107 | 108 | > 公司可以用信用卡支付发布工作信息的费用。 109 | > 110 | > 备注: 我们将支持发现信用卡吗? 111 | > 用户界面备注:不需要专门的字段来输入信用卡的类别卡片种类(可以从卡号的前两位数组获得该信息)。 112 | 113 | ## 2.3. 对用户或客户有价值的 114 | 115 | "每个故事必须对用户有价值",这句话说起来很诱人。但是许多项目中包含了对用户没有意义的故事。 116 | 117 | 要记住**用户(软件的使用者)**和**客户(购买软件的人)**之间的**区别**。 118 | 119 | 120 | 假设,一个开放团队正在构建一个支持大量用户的软件,可能需要在公司内5000台电脑上实施。像这样的客户比较关心5000台电脑是否在使用同样的软件配置。这就会产生一个例如这样的故事:“所有的配置都从一个中心读取”。但是用户不关心配置信息在哪里存储,但是购买者可能比较关心。 121 | 122 | ### 隐含测试用例的细节要和故事本身分开 123 | 124 | > 用Visa信用卡、万事达信用卡和运通卡测试(通过) 125 | > 用大来卡(Diner's Club)测试(失败) 126 | > 用有效、无效和丢失卡ID号的信用卡测试 127 | > 用过期卡测试 128 | > 用高于100元和低于100元测试 129 | 130 | 类似,下面的故事显示客户在购买时要考虑的价值,却不是用户所需要考虑的。 131 | 132 | - 整个开发过程中,开发团队要提供符合`ISO9001`标准审核的文档 133 | - 开发团队要按照`CMM3`级的标准来构建软件 134 | 135 | ### 应当避免那些只对开发人员有价值的故事。 136 | 137 | 例如,应该避免下面类似的故事。 138 | 139 | - 所有的数据库连接要通过一个连接池 140 | - 所有的错误处理和记录应在一系列公共类中完成 141 | 142 | 这些故事都在**关注技术**和**实现细节**。很可能这些故事背后的想法是好的,但是故事的编写方法应当体现对客户或用户的价值。这样使客户团队能方便的在开发中对那些故事排优先级。 143 | 144 | **更好的故事版本** 145 | 146 | - 这个应用软件,最多50位用户能使用一个5用户的数据库许可 147 | - 所有的错误应以统一的方式呈现给用户并做记录 148 | 149 | 150 | 同样,也应当避免在故事中出现用户界面和技术方面的定义。 151 | 152 | 保证每个故事对客户或用户有价值的最好方法是让客户来编写故事。开始时,客户一般都会觉得不舒服,可能因为他们觉得写下了的东西都有可能成为未来对他们不利的证据(“好吧,需求文档并没有这么说.......”)。但是故事只是为了提醒他们需要之后进行需求讨论,而不是一个正式的承诺或某个功能的具体描述。大多数的用户一旦接受这个概念,就会开始自己写故事了。 153 | 154 | ## 2.4. 可评估的 155 | 156 | 对于开发人员来说,能估算故事的大小(至少能猜一下),或者把故事变成可用代码的时间是很重要的。一般有以下3个原因会导致故事不可评估。 157 | 158 | 1. 开发人员缺少领域知识 159 | 2. 开发人员缺少技术知识 160 | 3. 故事太大了 161 | 162 | 首先,开发人员可能缺少领域知识。如果开发人员不理解故事,他们应该和写故事的客户一起讨论。同样没有必要理解故事所有细节,但是开发人员需要对故事有个大概的了解。 163 | 164 | 其次,故事无法苹果干是因为开发人员不掌握所涉及的技术。比如,在一个Java项目中,我们需要提供一个CORBA接口给系统。团队没有人有相关经验,所以当然无法评估这个任务。这种情况下,可以让一个或多个开发人去实施极限编程(XP)中所谓的探针试验(`spike`)。这是一个简短的试验,用于研究应用程序的某一方面。在做探针试验的时候,开发人员不需要做十分深入的研究,只要能大体了解足够信息来估计这个任务即可。 165 | 166 | > 探针试验本身总是会限定一个最大时间量(称为时间箱,TimeBox),用这个时间量作为探针试验的估计。 167 | 168 | 如此,一个不可估计的故事变成了两个故事:一个快速的探针故事(用来获得足够的信息)和一个故事(真正实现功能)。 169 | 170 | 最后,如果故事太大了,我们要估计它,就要把它分解成多个更小的故事。 171 | 172 | 即使故事太大不可能进行可靠的评估,有时候编写例如“一个找工作的人可以找到一份工作”这样的史诗故事也是很有用的,因为它们可以做为系统中有待讨论的一大块功能占位符或提示。 173 | 174 | 如果希望暂时不细化系统的一部分功能,可以考虑写一两个史诗故事。也可以给史诗故事一个大的、比较虚的估计值。 175 | 176 | ## 2.5. 小的 177 | 178 | 有些故事可能太大,有些可能太小,有些则刚刚好。 179 | 180 | 故事的大小很关键,故事太大或太小都无助于制定计划。 181 | 182 | 使用史诗故事来开展工作会很困难,因为它们通常包含多个故事。 183 | 184 | 举个例子:在一个旅行预订网站,“一个用户可以计划一次度假”是一个史诗故事。对于任何旅行预订系统,计划一次度假都是非常重要的供,包括一系列任务。史诗故事需要分成更小的故事。 185 | 186 | 合适的故事大小最终取决于团队、它的容量以及所使用的技术。 187 | 188 | ### 分割故事 189 | 190 | [史诗故事](../ch1/#史诗故事)通常分为以下两种。 191 | 192 | - 复合故事(`compound story`) 193 | - 复杂故事(`complex story`) 194 | 195 | #### 复合故事 196 | 197 | **复合故事有多个小故事组成的史诗故事**。在做系统初始设计计划是,复合故事可能是比较合适的,但是通常它都能分割成多个更小的故事。 198 | 199 | 比如,“用户可以发布他的简历”这个故事,可以分割成以下的故事。 200 | 201 | - 用户可以创建简历,包含教育情况、工作经历、薪资历史、出版物、演讲情况、社区服务和求职目标 202 | - 用户可以修改简历 203 | - 用户可以删除简历 204 | - 用户可以有多份简历 205 | - 用户可以激活简历,也可以让简历失效 206 | 207 | 一般有很多方法来分解一个复合故事。上面的分解方法,沿用一种常见的分解方式,即按照“**创建**”、“**编辑**”和“**删除**”这些动作来分解故事。 208 | 209 | 另一个可行的方法是根据**数据边界**来分解。比如,我们将简历的各种部分当成单独的部分来增加和修改。 210 | 211 | - 用户可以增加、修改教育信息 212 | - 用户可以增加、修改工作经历信息 213 | - 用户可以增加、修改薪资历史信息 214 | - ...... 215 | 216 | #### 复杂故事 217 | 218 | 不同于复合故事,**复杂故事是本身就很大并且不容易分解的故事**。如果一个故事因为不确定性而复杂,可以将它分为两个故事:一个**调研**的故事和一个**开放**的故事。 219 | 220 | 例如,假设给开发人员这样一个故事:“公司可以用信用卡支付发布职位的费用”,但没有一个开发人员曾经做过处理信用卡相关的工作。他们可以将故事这样分割: 221 | 222 | - 调研网络上处理信用卡的相关技术 223 | - 用户可以用信用卡付费 224 | 225 | 第一个故事会让一个或多个开发人员实施探针试验。这样分割复杂故事时,我们仍然非常可能定义需要花多少时间来进行调研。 226 | 227 | **当我们开发新的算法或扩展已知算法时,复杂故事是很普遍的**。 228 | 229 | ##### 考虑将探针试验放在不同的迭代里 230 | 231 | 如果有可坑,一种较好的做法是把调研的故事放在一轮迭代中,另外的故事放在接下来的一轮或几轮迭代中。 232 | 233 | 一般我们只能对调研的故事做评估,将另一个无法估计的故事与调研故事放在同一轮迭代中,不确定性会高于平春,因为我们无法知道在哪一轮迭代中能完成多少工作。 234 | 235 | 对无法估计的故事进行分解,主要的好处是允许把调研工作从新功能中分离出来,以便对调研工作排列出优先级。 236 | 237 | #### 合并故事 238 | 239 | 有时候,故事太小了。对于太小的故事,开发人员会说,他不想写下这个故事或者对它进行评估,因为那么做可能比实施该故事花的时间更长。 240 | 241 | 一个比较好的方法通常是将这些太小的故事合并到需要半天活几天完成的故事中。给合并后的故事命名后,就可以同其他故事一样计划实现它。 242 | 243 | ## 2.6. 可测试的 244 | 245 | 故事必须是可测试的。成功通过测试可以证明开发人员正确的实现了故事。如果故事不能被测试,开发人员怎么知道他们什么时候才算是完成了代码? 246 | 247 | 通常,不可测试的故事发生在一些非功能性的需求上,这些需求和软件有关,但不直接与功能有关。 248 | 249 | 例如,下面两个非功能性的故事。 250 | 251 | - 用户必须觉得软件好用 252 | - 用户绝不需要花很长时间等待界面出现 253 | 254 | 前面两个故事都是不可测试的。 255 | 256 | **无论什么时候,只要有可能,就要把测试自动化。** 这意味着我们需要争取99%都自动化,而不是10%。 257 | 258 | 能自动化的测试基本上总是比你认为的要多。 259 | 260 | 当产品是增量开发的,很多东西变化得很快,昨天能工作的代码,今天就会出现问题。这需要自动化测试来帮助你尽早发现这些问题。 261 | 262 | 实际情况中,总有极小部分的测试是不能进行自动化测试的。比如上面的故事“用户绝不需要花很长时间等待界面出现”是不可测试的,因为它用了“绝不”,而且,“长时间等待”没有明确定义。要想演示某些东西永远不会出现时不可能的。**一个更容易、更合理的目标是演示某些东西极少出现**。这个故事可以改为“在95%的情况下,界面会在2秒内打开”。这样就可以测试,并且最好是写一个自动化测试来验证它。 263 | 264 | ## 2.7. 职责 265 | 266 | ### 客户团队职责 267 | 268 | - 编写故事,这些故事要能提醒你们同开发人员交谈,而不是记录详细的需求定义,它们对用户或你们自己是有价值的,它们时独立的、可测试的、大小合适的。 269 | 270 | ### 开发人员职责 271 | 272 | - 负责帮助客户团队编写故事,这些故事要能提醒你们同客户团队交流,而不是记录详细的需求定义,故事应该对用户和客户有价值,它们是独立的、可测试的、大小合适的 273 | - 如果被问及实现故事所用的技术或基础架构信息,应该使用对用户或可以有价值的术语来描述 274 | 275 | ## 2.8. 小结 276 | 277 | - 理想情况下,故事之间是独立的。有时很难做到这一点,但我们哟啊尽量实现这一目标。故事之间的交付顺序应该是无关的,可以任意拿一个故事来实现 278 | - 故事细节有用户和开发人员讨论得出 279 | - 故事应该很清晰体现对用户或客户的价值。最好的做法是让客户编写故事。 280 | - 故事可以注释一些细节,但是过多的细节会使故事难以理解,也可能给人一种开发人员和客户无需交流的错觉 281 | - 给故事加上注释的最好方式是给它编写测试用例 282 | - 如果故事太大,复合故事和复杂故事可以拆分成多个小故事 283 | - 如果故事太小,几个小故事可以合并成一个较大的故事 284 | - 故事应该是可以测试的 285 | -------------------------------------------------------------------------------- /ch1/README.md: -------------------------------------------------------------------------------- 1 | # 1. 什么是用户故事? 2 | 3 |  4 | 5 | 这一部分会说明**什么是用户故事**,如何使用它们开始,介绍**如何编写**用户故事,如何利用系统的**用户种类**来确定故事,如果在难以接触到用户的情况下**与充当用户角色的人一起工作**,如何**编写测试**来**验证**故事已经完成。 6 | 7 | 最后,给出一些有助于编写良好故事的**指导原则**。 8 | 9 | ## 1.1. 背景 10 | 11 | ### 1.1.1. 软件需求是一个沟通的过程 12 | 13 |  14 | 15 | 一个项目的成功,依赖于很多不同的信息,这些信息来自各自不同的人员: 16 | 17 | - 一方是客户和用户,有时还有分析人员、领域专家和其他从业务或组织视角来审视软件的人 18 | - 另一方是技术团队 19 | 20 | > 一旦任何一方在沟通中把持绝对地位,项目就会遭受损失。 21 | 22 | - 如果业务方把持绝对地位,他们就会关注**软件功能**和**交付日期**,却很少关注开发人员**是否能够同时满足这两个目标**,或者开发人员**是否确切了解需求**。 23 | - 如果是开发人员把持绝对地位,**技术术语就会代替业务语言**,从而导致开发人员**无法倾听业务方的实际需求**。 24 | 25 | ### 1.1.2. 需求的协同工作方法 26 | 27 |  28 | 29 | 我们需要一种需求协同工作的方法,让双方都不占绝对主导地位,共同面对感情用事和办公室政治化的资源分配问题。 30 | 31 | **如果资源分配问题完全落在一方,项目必定会失败。** 32 | 33 | - 如果只让开发人员来承担这些问题(他们通常被告知“我不关心你们怎么做,但请你们在6月份之前完成”),他们可能会**牺牲质量来换取额外的特性**,也可能**只部分实现一个特性**,或者**自行做出一些该在有客户和用户参与的情况下才能做出的决定**。 34 | - 如果只是客户和用户承担资源分配的责任,那么我们会通常在项目**开始阶段看到一系列漫长的讨论**,项目中的**特性逐渐减少**。之后,在最终发布版本的时候,**只剩下很少的功能,甚至少于被减少的功能**。 35 | 36 | ### 1.1.3. 如何规避风险 37 | 38 |  39 | 40 | 软件开发类项目本身是很难预先评估的,中间有很多因素能决定最终项目的结果和质量,这是软件开发本身的特点。那么我们应该如何来做?如何来更好的规避风险? 41 | 42 | 基本原则: 43 | 44 | 1. 不要在项目开始阶段就做一套完善的、包罗万象的决策 45 | 2. 把各个决策分散项目过程中 46 | 47 | 因此,我们需要有一个**获取信息的过程,越早越好,越频繁越好**。 48 | 49 | ## 1.2. 什么是用户故事? 50 | 51 | **用户故事描述了对用户、系统或软件购买者有价值的功能**。 52 | 53 | 用户故事由以下三个方面组成: 54 | 55 | - 一份书面的故事描述,用来做计划和提示 56 | - 有关故事的对话,用于具体化故事细节 57 | - 测试,用来表达和编档故事细节并且可以用于确定故事何时完成 58 | 59 | 基于[`Ron Jeffries`提出了`3C`原则](../),对用户故事的最佳诠释应该是这样。 60 | 61 | > 卡片(`Card`)包含了故事的文字说明,然而需求细节要在“对话(`Conversation`)”中获得,并在“确认(`Confirmation`)”部分得以记录。 62 | 63 | **例子** 64 | 65 | > 我们所有的例子都是来自一个假想的职位发布和搜索的招聘网站。 66 | 67 | **好的故事雏形例子** 68 | 69 | - 用户可以在网站上发布简历 70 | - 用户可以搜索职位 71 | - 公司可以发布新职位 72 | - 用户可以限制浏览其简历的人 73 | 74 | **不好的例子** 75 | 76 | - 这个软件将用C++语言进行编写 77 | - 程序将通过连接池连接数据库 78 | 79 | 第一个不好的例子,对于招聘网站来说,它的用户根本不关心系统是用什么语言来写的。 80 | 81 | 第二个不好的例子也不是一个很好的用户故事,因为用户没有必要关心应用如何连接数据库之类的技术细节。 82 | 83 | **关键在于故事应该以对用户有价值的方式写下来。** 84 | 85 | ## 1.3. 细节在哪里? 86 | 87 | “用户可以搜索工作”,说起来容易,但**仅以这句话为指南着手开发和测试确实另外一回事**。 88 | 89 | ### 1.3.1. 故事细节 90 | 91 | 那细节都在哪里?下面这些未解决的问题又该怎么办? 92 | 93 | - 用户可以搜索哪些值?省份?城市?职位?关键字? 94 | - 用户必须是网站的会员? 95 | - 搜索参数可以保存吗? 96 | - 要显示哪些与工作匹配的信息? 97 | 98 | > 许多这样的细节可以用另外的用户故事来描述。 99 | 100 | 事实上,**多个小故事远胜于一个庞大的故事**。 101 | 102 | 例如,这个招聘网站大部分的功能可以描述成下面两个故事。 103 | 104 | 1. 用户可以搜索工作 105 | 2. 公司可以发布工作信息 106 | 107 | 但是这两个故事**太庞大了**,派不上用场。 108 | 109 | ### 1.3.2. 史诗故事 110 | 111 | > 如果一个故事很大,我们会称之为“史诗故事(`Epic`)”. 112 | 113 | 史诗故事可以**分成多个小故事**。 114 | 115 | 例如,“用户可以搜索工作”可以分为下面几个小故事。 116 | 117 | - 用户可以通过地区、薪水范围、职位、公司名和发布日期之类的属性来搜索工作 118 | - 用户可以查看搜索结果中每个工作的信息 119 | - 用户可以查看发布工作的公司的详细信息 120 | 121 | 然而,我们不需要不断的分解故事,知道有一个故事能够覆盖每一个细节。 122 | 123 | 例如,“用户可以查看搜索结果中每个工作的信息”,就是一个比较合理且实际的故事。我们不需要在进行下一步的分解成更小的故事。 124 | 125 | - 用户可以查看工作范围 126 | - 用户可以查看薪水范围 127 | - 用户可以查看工作地点 128 | 129 | > 与其写下这些故事细节,还不如让开发团队和客户讨论这些细节,即在这些细节变得重要时才讨论。 130 | 131 | **讨论才是关键**,而不是故事卡上的注释。 132 | 133 | **故事并不具有契约性质**,达成的协议将由测试来记录,这些测试将演示故事是否**被正确开发**。 134 | 135 | ## 1.4. 必须多长时间完成? 136 | 137 | > “必须多长时间完成?”这个问题能够从某种角度来了解项目用户的期望是什么? 138 | 139 | 用户的期望最好以**验收测试**的形式记录下来。 140 | 141 | 如何测试验收故事的提示语句就是用户的期望。 142 | 143 | 可以记录如何测试用户故事的这样一些提示 144 | 145 | - 用空的工作描述来试试 146 | - 用很长的工作描述来试试 147 | - 用缺少薪资来试试 148 | - 用六位数的薪资来试试 149 | - ... 150 | 151 | 测试描述可以很简短、不完整,可以在任何时候加入或者删除。 152 | 153 | 写这些测试描述的目的是传递故事的额外信息,以便于开发人员知道故事于什么时候结束。 154 | 155 | 开发人员如果能够了解客户的期望,他们便能知道什么时候算是完成了客户要求的功能。 156 | 157 | ## 1.5. 客户团队 158 | 159 | 在一个理想的项目中,我们应该会有一个专职的人员为开发人员的工作排列优先级,回答他们所有的问题,在软件完成时使用软件,并且写下所有的故事。这个人在互联网公司一般来说是产品经理。 160 | 161 | 不过,我们依然要想办法组建一个客户团队,这个团队中应该包括**确保软件满足用户需求的所有人**。 162 | 163 | 这意味着这个客户团队应该包括**产品经理**、**测试人员**、**实际用户**和**交互设计师**。 164 | 165 | ## 1.6. 使用故事的过程是怎样的? 166 | 167 | 对于故事驱动的项目而言,最引人注目的是客户团队在项目整个过程中全称参与,我们不希望(或者不允许)他们在项目进行时离开,不管团队是否在使用XP、Scrum之类的敏捷过程或开发人员自己发展出来的故事驱动的敏捷过程。 168 | 169 | 客户团队应该在编写用户故事时承担着非常活跃的角色。 170 | 171 | 编写用户故事的过程最好从考虑系统的用户类别开始。 172 | 173 | 例如,如果是在构建一个旅行预订网站,你可能会有诸如经常旅行者、假期计划着等用户类别。客户团队应该尽量包括了这些实际的用户类别。 174 | 175 | > 可以参考使用[用户角色建模](../ch3/)。 176 | 177 | ### 1.6.1. 客户团队为什么要编写故事? 178 | 179 | 有客户团队而不是开发团队来编写用户故事主要基于两个原因。 180 | 181 | 1. 每个故事必须用商业语言来写,而不是技术用语。这样一来,客户端团队可以排列故事的优先级,放入迭代和发布。 182 | 1. 作为主要的产品构想者,客户团队所处的位置最适合描述产品行为。 183 | 184 | ### 1.6.2. 通过故事评估效率 185 | 186 | 一个项目的用户故事初稿,肯定是在一开始就要写好的,但是用户故事可以项目生命周期的任何时候编写。 187 | 188 | 在每次迭代结束时,开发人员将负责发布完全可用的应用程序子集。客户团队在迭代周期间高度参与,与开发燃油谈论迭代期间正在开发的故事。 189 | 190 | 在迭代期间,客户团队也会详细定义测试,并且和开发人员一起编写运行自动化测试。此外,客户团队要保证项目能够达到交付所需产品的目标。 191 | 192 | 一旦确定了迭代长度,开发人员就会评估每轮迭代中可以做多少事情。我们称之为速率(`velociry`)。 193 | 194 | 团队第一次的速率评估可能是错误的,因为无法事先知道团队的速率。然而,我们可以用初步估计来勾勒出大致的蓝图或者发布计划,用以说明在每轮迭代中会完成哪些工作,需要多少轮迭代周期。 195 | 196 | 在每轮迭代开始前,客户团队可以在中图修正计划。当迭代结束后,我们可以得知开发团队的实际速率,然后用它来代替估计速率进行估计。这意味着每一堆故事可能需要通过增加或者移除来进行调整。 197 | 198 | 有些故事可能比预期的简单很多,所以有时开发团队想在迭代中完成另外的故事。但有些故事比预期的难,这是就要把有些工作移到下一轮迭代或者下一个发布计划中去完成。 199 | 200 | ## 1.7. 规划发布和迭代 201 | 202 | **一个发布由一个或多轮迭代组成。** 203 | 204 | 发布规划指的是确定项目实际表和预期功能集合之间达到平衡。 205 | 206 | 迭代规划涉及选择迭代包含的故事。 207 | 208 | 客户团队和开发人员在发布和迭代规划中都要参与。 209 | 210 | 在进行发布规划时,客户团队**首先从排列目标优先级开始**。在排列优先级是,需要考虑下面几点。 211 | 212 | - 大部分用户和客户对特定特性的渴望程度 213 | - 小部分重要客户和用户对特定特性的渴望程度 214 | - 故事之间的关系。例如,“缩小(`zoom out`)”这个故事的优先级可能不高,但是它可能被看做高优先级的,因为它和高优先级的另一个故事“放大(`zoom in`)”互补。 215 | 216 | 在许多故事的优先级上,开发人员可能与客户团队意见相左。他们可能基于技术风险方面的考虑,或者由某个故事是其他故事的互补故事,而建议更改故事的优先级。客户团队应该倾听他们的观点,但是随后排列故事优先级时,应该坚持客户组织利益最大化的原则。 217 | 218 | 排列故事优先级时不能不考虑它们的成本。故事的成本由开发人员给出的,每个故事用故事点来估计,故事点表明了一个故事相对于其他故事的大小和复杂度。 219 | 220 | > 一个4个点的故事成本是一个估计2个点的故事成本的2倍。 221 | 222 | 为发布中的所有迭代分配故事后,发布计划便浮出水面了。当开发人员陈述他们所预计的速率,在每一轮迭代中他们认为可以完成多少故事点。然后客户团队把故事分配到迭代中,他们要确保每轮迭代中分配的故事点数不超过开发团队预期的速率。 223 | 224 | > 比如开发团队的预期速率是13,没有迭代可以完成多于13个故事点的故事。这意味着第二轮、第三轮迭代只能计划12个故事点。(很少有非常精确,这点差异不会造成什么影响) 225 | 226 | 除了在迭代中临时跳过一个大的故事而放入一个较小的故事以外,可以把大故事分成两个小故事。 227 | 228 | > 假设5个故事点的故事I可以作为故事Y(3个点)和故事Z(2个点)。现在故事Y包含故事I的最重要部分,就可以在当前迭代中引入故事Y。 229 | 230 | ## 1.8. 什么是验收测试? 231 | 232 | > 验收测试是用来验证实现的用户故事是否符合客户团队的期望。 233 | 234 | 当一轮迭代开始,开发人员开始编码,同时客户团队开始测试工作。测试工作可以报考从**故事中写下测试描述开始**,将测试放入到自动化测试工具中的所有工作。客户团队中应该包含一个专业的、熟练的测试人员,由他完成这些任务中偏技术的工作。 235 | 236 | 测试工作应该尽早的在迭代中编写(如果能大致猜到即将开始的迭代会产出什么,就可以在迭代开始前编写测试)。 237 | 238 | 早期编写测试是非常有用的,因为这样一来,客户团队的假设和预期就会更早与开放人员沟通。 239 | 240 | > 例如,写下故事“用户可以用信用卡为购物车中的物品付款”,然后可以写下这些简单的测试描述. 241 | 242 | - 用Visa信用卡、万事达信用卡来测试(通过) 243 | - 用公交卡测试(失败) 244 | - 用Visa借记卡测试(通过) 245 | - 用有效、无效和反面丢失卡ID号的信用卡测试 246 | - 用过期卡测试 247 | - 用不同购买金额测试(包括超出信用卡额度) 248 | 249 | 尽早把这些测试交给开发人员,客户团队不仅仅澄清了他们的预期,也同时提醒了开发人员可能会忘记的场景。 250 | 251 | ## 1.9. 为什么要变成用户故事? 252 | 253 | > 为什么要编写用户故事,并进行所有这些对话呢? 254 | 255 | 相比较其他方法,用户故事有比较多的优势,比如: 256 | 257 | - 用户故事强调对话交流而不是书面沟通 258 | - 用户故事可以同时被你和开发人员理解 259 | - 用户故事的大小适合做计划 260 | - 用户故事适用于迭代开发 261 | - 用户故事激励推迟考虑细节,直到你非常清楚地了解自己的真正需求 262 | 263 | 由于用户故事的重点从文档转移到对话,所有重要决策不会写在文档里,因为很可能没有人阅读哪些文档。取而代之的是,**在自动化测试中捕获用户故事的重要信息,频繁执行进行验证**。 264 | 265 | 除此之外,我们还要避免在文档中出现下面含义不清的语句: 266 | 267 | - 系统必须存储地址和办公电话或移动电话 268 | 269 | 上面的描述是什么意思呢?它可以理解为系统必须存储: 270 | 271 | - (地址和办公电话),或者移动电话 272 | - 地址和(办公电话或移动电话) 273 | 274 | 再次强调,由于用户故事不会有技术术语(它们是有客户团队编写的),所以开发人员和客户团队双方都能理解。 275 | 276 | 每个用户故事都代表一个独立的功能,即用户在一个单一环境中可能做的事情。这便使用户故事成为一个非常合适的计划工具。你能够估计在不同的发布中挪动故事顺序(优先级)的价值,这远远比估计去掉一个或多个“系统应该...”的陈述所产生的影响容易。 277 | 278 | 迭代过程是一个逐步求精的过程。开发团队首先开发系统中一小部分,知道它在某些(或许很多)方面是不完整的或者不完善的。然后在逐步加以相应的改进,直到产品让人满意。 279 | 280 | 通过每轮迭代中增加的更多细节,软件被逐步改进。用户故事和迭代开发可以紧密结合,因为故事也是可以迭代的。 281 | 282 | 对于最终需要但当前并不重要的特性,可以写下一个大的故事([史诗故事`Epic`](#史诗故事))。准备好将大故事加入系统之后,便可以提炼它,抛弃史诗故事继而使用更小的,更具体的故事代替它。 283 | 284 | > “故事集可以迭代”这一能力,恰恰可以佐证我们可以推迟考虑故事细节。 285 | 286 | 因为假如你可以今天写下用于占位的史诗故事,就没有必要再进一步写下系统这部分的用户故事,除非马上就要开发那些部分。 287 | 288 | 推迟细节很重要,因为这样一来,我们在不确定是否真正需要某个特性时,可以不花过多的时间来考虑它。使用故事,我们不必假装可以事先知道并写下所有东西,以客户团队和开发人员的讨论为基础,不断精炼我们的需求。 289 | 290 | ## 1.10. 小结 291 | 292 | - 故事卡包含对用户或客户有价值的功能和简短描述 293 | - 故事卡是故事可见部分,但客户团队和开发人员关于故事的对话更重要 294 | - 客户团队包括那些确保软件符合潜在用户需求的人,可以包括测试人员、产品经理、实际用户和交互设计师 295 | - 故事卡有客户团队编写,因为他们最了解如何表达需要实现的需求,也因为他们会在后期与开发人员共同确定故事细节并安排故事的优先级顺序 296 | - 按照故事对客户的价值来安排故事的优先级顺序 297 | - 将各个故事放入迭代,进行发布和迭代规则 298 | - 效率是开发人员可以在一轮迭代中完成的工作量 299 | - 如果故事太大以至于无法在一轮迭代中完成,可以考虑把它分成两个或更多的小故事 300 | - 验收测试用于验证实现的故事是否开发成符合客户团队的设想 301 | - 用户故事是很有意义的,因为它们强调口头交流,你和开发人员都可以理解,可用于进行迭代计划,子啊迭代开发过程中能很好的工作,而且它们鼓励推迟细节 302 | -------------------------------------------------------------------------------- /ch4/README.md: -------------------------------------------------------------------------------- 1 | # 4. 收集故事 2 | 3 |  4 | 5 | 你是怎么收集故事的? 6 | 7 | 接下来咱们聊聊**如何和用户一起工作**,如何通过**与他们沟通来发现故事**。 8 | 9 | 同时介绍各种方法的优点,怎样**提出恰当的问题**,从而**获得用户真正的需求*。 10 | 11 | ## 4.1. 用“拖网”来收集需求 12 | 13 | 一些需求相关的书中用到了像“引出”(`Kovitz 1999`; `Lauesen 2002`; `Wiegers 1999`)和“捕捉”(`Jacobson`, `Booch`和`Rumbaugh 1999`)这样的词来描述识别与确认需求的实践。 14 | 15 | 不过这样的术语给我们一种**错觉**:“需求本来已经存在了,我们只要让客户给我们解释需求,然后把它们锁到一个笼子里就可以了。”很多需求并不容易想到。同样,**用户并不知道所有需求**,所以不能单纯依靠引出(`elicitaion`)。 16 | 17 | 我们可以用另外一个词--**拖网**(`trawling`)这个词来描述收集需求的过程。怎么理解呢?**要像“拖网渔船捕捞鱼”那样收集需求**。 18 | 19 |  20 | 21 | 为什么要用这样的比喻呢?理由如下: 22 | 23 | - 首先,**不同大小的网用来捕获不同大小的需求**。第一遍,我们可以用大网眼的渔网捞一遍需求池,以此得到所有的大需求。通过大需求,形成对产品的整体感觉。接下来,用网眼小一些的渔网得到中等大小的需求,展示还不用顾及到那些小需求。在这个比喻中,**大小可以是对此产品的商业价值高低或必要性程度**等。 24 | 25 | - 其次,拖网表达了另一个含义:**需求会像🐟一样,会生长,也可能会死亡**。今天渔网可能会漏掉一个需求,因为这个需求对于系统来说不重要。但是,根据每轮迭代的反馈,系统会朝着事先不可预知的方向发展,有些需求会变得越来越重要。同样,有些曾经被认为重要的需求,重要性可能会降低,有时甚至降低到我们认为这些需求已经无效了。 26 | 27 | - 第三,正如在**某些区域拖网捕鱼不可能捕到所有的🐟,我们也不可能捕捉到所有的需求**。另外,在拖网捕捞需求的时候,也可能捞到一些废弃的货物或漂浮的残骸,他们使需求膨胀。 28 | 29 | - 最后,这个拖网捕捞需求的比喻还说明了一个重要的现实:**技能也是发现需求的一个要素**。一个熟练的需求分析人员(`requirements trawler`)知道要到哪里去找需求,而不熟练的需求分析人员只会用低效的方法或在错误的地方浪费时间。 30 | 31 | 那我们接下来看看有哪些更有效“捞”到用户故事的方法。 32 | 33 | ## 4.2. 够用就行,不是吗? 34 | 35 | **辨别传统规范过程和敏捷过程最简单的方法之一,是看它们收集需求的方式。** 36 | 37 | 传统规范过程的特征是它过分强调在项目早期正确的获取并写出所有的需求。与此不同的是,敏捷项目则承认没有一种理想的方法可以在一个单一阶段获取到所有的用户故事。 38 | 39 | 敏捷过程也承认用户故事有一个**时间维度**: **随着时间的推移以及先前迭代中加入产品的故事,一个故事的相关性(`relevance`)会有变化**。 40 | 41 | 然而,即使我们承认无法为一个项目编写出所有的故事,我们还是应该在早期尝试编写我们可以编写的故事,即使许多故事还只能停留在十分笼统的阶段。 42 | 43 | 使用故事的好处之一就是**可以用不同的详尽程度来编写**。我们可以写下“用户可以搜索故事”这样的故事,无论是作为一个占位符,亦或此时我们只是了解到这个程度,这个故事都是恰当的(`placeholder`)。我们可以在**今后将它演进为更小的、更有用的多个故事**。 44 | 45 | 因此,对应用程序的大部分功能编写故事是非常容易的,其工作量少于用其他方法收集故事。 46 | 47 | 这并不是鼓励大家在开始一个新项目时花3个月时间来编写用户故事。相反,它要求大家**展望未来的一个发布(大概3-6个月)的时间,故事发布的时间越往后,我们越不需要编写的那么详细**。 48 | 49 | > 例如,如果客户或用户说过他们“很可能在此次发布中想要报表功能”,那么简单的在一张卡片上写下“用户可以运行报表”。但是可以就此打住:先不用决定他们是否需要配置自己的报表,报表是否以HTML格式输出,或者是否可以保存报表。 50 | 51 | 在**项目启动之前,对应用程序的大小有一个大致的了解是很重要的**。通常在对项目拨款以及批准启动它之前,**有必要粗略的了解**一下项目的**成本**和它能够带来的**效益**。为了获知这些问题的答案,需要对项目包含的故事进行初步的深谋远虑。 52 | 53 | ## 4.3. 方法 54 | 55 | 因为**故事会随着项目的进展而演进**,所以我们需要一些**可以反复使用的方法来收集故事**。我们使用的技巧**必须是足够轻量的,并且不咄咄逼人,可以持续的、或多或少的应用于故事收集**。 56 | 57 | 接下来我们会聊一些是创建故事的方法。这些方法有很多是传统业务分析师所使用的。对于配备有业务分析师的项目,应该充分利用他们以“**拖网捕鱼**”的方式来收集故事。 58 | 59 | ### 4.3.1. 用户访谈 60 | 61 |  62 | 63 | **用户访谈是许多团队用来获取故事的默认方法**,很可能这也是你想使用的方法。 64 | 65 | 访谈成功的关键之一是**选择正确的受访者**。如同我们接下来要聊的“[与用户代理一起工作](../ch5/)”那样,有许多用户代理可以做访谈。但是显然,**只要有可能,就应该访谈真实用户。还应该访问担任不同角色的用户**。 66 | 67 | 有一次,一位用户走进我们办公室对我说:“你们的确开发了我所需要的应用,但它并不是我真正想要的”。这件事让我明白一点,**只询问用户“你们需要什么”是不够的,大部分用户还不善于理解,更难以表达他们真实需求**。 68 | 69 | > 我曾经和一个团队一起工作,他们要开发一个调查速递软件。每个调查会通过电话、电子邮件和交互式语音应答系统来进行。不同类型的用户会使用不同类型的调查。 70 | > 这些调查非常复杂:对于一组问题的回答将决定下一个问题是什么。用户需要输入调查的方法,他们向开发团队演示一些例子,建议用一种复杂的迷你型语言来确定问题。 71 | > 这种完全基于文本的方法,对于开发人员来说增加了不必要的复杂度。开发人员向用户展示了他们可以通过可视化的图标拖拽来创建调查,不同的图标代表了调查中的不同类型的问题。 72 | > 之后用户放弃了他们迷你型的语言,并和开发人员一起开发可视化的调查设计工具。 73 | 74 | 这说明了一点:**仅仅因为这些问题是用户提出来就认为只有用户才有资格提出解决方案,这种观念是不对的**。 75 | 76 | #### 开放式问题和背景无关问题 77 | 78 | **想要获取用户的本质需求,最好的技巧就是提问**。 79 | 80 | 我曾经与一个项目小组一起工作,他们在把应用程序开发成Web程序和开发成更传统的平台相关程序之间举棋不定。 81 | 82 | 基于浏览器的程序更容易部署,而且培训成本比较低;而与平台相关的客户端程序则更加健壮,两者如何选择?用户一定会喜欢浏览器的优势,但他们也重视特定平台客户端程序丰富的用户体验。 83 | 84 | 有人建议,询问一下目标用户的喜好。由于这个产品是对上一代产品的重新编写,所以市场部同意与目前的产品客户代表取得联系。询问每个接受调查的用户:“你们想在浏览器运行新的应用程序吗?” 85 | 86 | > 这个问题就像是你去最喜欢的餐厅,服务员问你是否想要免费餐。 87 | 88 | 设计问题的人犯了一个错误,他们询问了一个封闭式问题,没有提供足够的细节让对方更好的回答。这个问题假设了接受调查的用户知道浏览器方案和未明确说明的代替方案之间的优缺点。 89 | 90 | 这个提问最好的版本是: 91 | 92 | > 你想我们新的应用在浏览器里运行,而不是本地窗口程序吗?即使这意味着性能有所减弱,总体上用户体验会差一些,交互也少一些。 93 | 94 | 这个提问仍然有问题,因为它还是**封闭式**的。**调查对象只能回答简单的是或否,没有余地去回答其他的东西**。 95 | 96 | 询问开放式的问题要好很多,这可以让调查对象表达更深入的意见。比如:“为了让我们下一代产品运行在浏览器上,你愿意舍弃什么?”针对这个问题的用户可能回答有很多种。无论是哪种回答,对我们都更有意义。 97 | 98 | 同样重要的是要提**背景无关的问题**,这种提问**没有暗含什么答案或喜好**。比如,你不应该问:“你不会愿意为了软件在浏览器上运行,而牺牲性能和丰富的用户体验,对吗?”很明显,我么知道大部分会怎么回答。 99 | 100 | 类似的,不要问“搜索速度需要多快?”,而要问“需要怎样的性能?”或者“性能在应用程序中的某些部分更会重要吗?” 101 | 102 | 第一个提问不是一个背景无关的问题,因为它包含了有一个需求是关于搜索性能的。要么没有人这么问用户,一旦问了,他的回答可能是猜想出来。 103 | 104 | **某些时候,需要使用非常具体的问题**。当然,**最好是从背景无关的问题开始提问**,这样就有可能从用户那儿获得更多样化的回答。如果从非常具体的问题开始,则很可能漏掉很多故事。 105 | 106 | ### 4.3.2. 问卷调查 107 | 108 |  109 | 110 | 问卷调查就是一种有效的方法,**有助于收集已有故事的相关信息**。 111 | 112 | **若你有一个庞大的用户群,那么问卷就是收集有关故事优先级信息的好方法**。在需要得到大量用户关于某些具体问题的回答是,问卷是非常有用的。 113 | 114 | 然而,**问卷不适合作为拖网捕获新故事的主要方法**。使用静态的问卷不利于跟进后续问题。同样,不能通过谈话方式那样,很方便的立即对用户的有趣想法进行深入探讨。 115 | 116 | 举一个使用问卷的例子,你可以调查用户今天使用软件功能的频率,以及为什么有些功能用的比较少。这样就可以将那些使用频率比较高的故事优先级调高了。 117 | 118 | > 另一个例子,“什么新功能你最想看到”这样的问卷问题价值就比较低。如果给用户一些选择,就可能错过几个自己没有想到过的关键功能;如果让用户自己用自由格式提供回答,就很难归纳多个问题。 119 | 120 | 鉴于单向沟通的既有特点和时间滞后,我不建议在捕获故事时使用问卷调查。 121 | 122 | 假如想**从已有的广泛用户群收集信息**,而**愿意等待**一个或多轮迭代来分析收集到的信息,则**可以使用问卷**,但**不要把它作为捕获故事的主要方法**。 123 | 124 | ### 4.3.3. 观察 125 | 126 |  127 | 128 | **观察用户实际使用软件的情况**,这个方法非常不错。 129 | 130 | 每次我看到有人使用我的软件,我都会获得很多提高用户体验或生产力的想法。不幸的是,能观察用户使用情况的机会少之又少,除非为内部客户开发。 131 | 132 | 太多商业产品开发采用的方法都是**猜测用户需求**。 133 | 134 | 因此,如果有机会观察用户使用软件的情况,千万不要错过。这种机会可以让你**快速直接从用户那里获得反馈**,从而可以更早更频繁的发布软件。 135 | 136 | > 曾经有一个公司的用户是在呼叫中心的护士。他们负责回答来电咨询的医学问题。护士指出他们需要一个大文本框,在通话结束后,能够用它来记录通话结果。软件最初的版本中,在接电话时有一个覆盖屏幕的大文本框。然而,该版本发布后,每一个开发人员话一个天时间观察用户。他们发现用户在大文本框中输入的内容其实可以让系统来跟踪记录。通过观察,开发人员发现真正的需求是系统应记录用户在使用软件过程中所做的决定。 137 | > 138 | > 后来,开发人员用一个日志功能替换了文本框,该功能记录护士所有的搜索和选择的建议。这个才是真正的需求,记录所有给来电者的解答。 139 | 140 | 这个真正的需求由于最初护士对需求的描述而变得很不清晰,只有**通过观察才能发现**。 141 | 142 | ## 4.3.4. 故事编写工作坊 143 | 144 |  145 | 146 | **故事编写工作坊是开发人员、用户、产品和其他对编写故事有帮助的人共同参加的会议**。 147 | 148 | 在工作坊期间,参与人员编写尽可能多的故事,此时不对故事排优先级。 149 | 150 | 正确举办故事编写工作坊可以非常快速的编写大量故事。**良好的故事编写工作坊结合了头脑风暴的最佳要素和简单原型法**。可以把一个简单原型画在纸上,笔记本上,白板上,并画出软件内部高层之间的交互。在工作坊中,随着参与者对用户在使用软件过程中可能要做的事情进行头脑风暴,不断构建原型。这并不是像传统原型法或联合应用设计中,要确定实际界面和字段,只是为了**从概念上确定工作流**。 151 | 152 | ### 故事与简单原型 153 | 154 | 比如,下面就是通过故事编写工作坊整理的招聘网站的简单原型。 155 | 156 |  157 | 158 | 其中: 159 | 160 | - 每个方框代表着网站的一个新组件 161 | - 方框中带有加粗的文字是组件的标题 162 | - 标题下面是组件要做的和包含的列表 163 | - 方框间的箭头标示组件间的链接 164 | 165 | 对于一个网站,组件可能是一个新的页面或当前页面的一块区域。所以,每一个链接意味着弹出一个新页面或者在同一个页面上显示信息。 166 | 167 | > 比如,搜索工作可能是一个页面或者首页上的一块区域。 168 | 169 | 开始画原型前,首先要决定从哪种用户角色开始。需要用每个角色来重复这个过程,不论什么顺序。然后,画一个空的方框,告诉参与者这是软件的主界面,询问他们当前这个用户角色能在这个界面做什么。即使你现在还不知道主界面是什么,有什么用,这也没有关系。参与者会想出角色会做什么。对角色做的每一件事情,画一条指向一个新的方框,然后写一个故事。 170 | 171 | 比如上面的图,我们可以得出以下故事: 172 | 173 | - 求职者可以发布他的简历 174 | - 雇主可以发布工作 175 | - 求职者可以搜索工作 176 | - 求职者可以看到搜索条件的工作 177 | - 求职者可以看到指定工作的详细信息 178 | 179 | 这些故事都不需要界面是该如何设计。但是,**走一遍流程**可以帮助大家想出尽可能多的故事。 180 | 181 | 我发现**深度优先的方法最有效**: 182 | 183 | > 对于第一个组件,写下主要的细节,接着是与第一个组件相连的组件B,一样写下其主要的细节。然后是与B相连的组件,而不是回到第一个组件A,描述其他与A相连的组件。 184 | 185 | 广度优先的办法非常不容易理解,因为**很难记住自己刚才在哪条功能线上**。 186 | 187 | 另外,我们要尽快**扔掉简单原型** 188 | 189 | > 在画好简单原型后的几天内,一定要扔掉或擦掉它、 190 | > 原型并不是开发流程中的一个长期工件,因为长期留着可能会导致不必要的困惑。 191 | > 如果觉得在故事编写工作坊中还有发现所有故事,可以把原型保留几天,再看看是否还能写出一些漏掉的故事,然后再考虑扔掉它。 192 | 193 | 在画原型的过程中,**问一些有助于找到遗漏故事的问题**,示例如下: 194 | 195 | - 用户接下来最有可能做什么? 196 | - 用户会在这里犯什么错误? 197 | - 在这里,用户会有什么困惑? 198 | - 用户需要什么额外的信息? 199 | 200 | 在问这些问题是,考虑一下当时用户的角色。**许多答案都和用户当时角色有关**。 201 | 202 | #### 原则 203 | 204 | 在故事编写工作坊期间,我们有一些工作的**原则**需要大家都**了解并且遵守**: 205 | 206 | 1. 应该把重点放在数量上,而不是质量上。 207 | 2. 即使最后会用电子方式保存故事,但在工作坊里最好还是使用卡片。 208 | 3. 只需把想法记录下来就行。最初大家觉得不好的故事经过几个小时后可能会变得很棒,或者会激发我们相处其他故事。 209 | 4. 不要为每个故事都陷入长时间的讨论中。 210 | 5. 如果一个故事是多余或者能被更好的故事替换,就扔掉这个故事。 211 | 212 | #### 小技巧 213 | 214 | 另外,还有一些**小技巧**能够让工作坊进行的更加顺利。 215 | 216 | 1. 可以维护一个待办问题列表,将一些当前不是最重要的故事记录其中,留着以后再来解决。 217 | 2. 如果我们卡在某个点过不去,这时不妨看看竞争对手的产品或类似的产品。 218 | 3. 留意在工作坊中成员的贡献。有些参与者在大部分或者整个期间都保持沉默,可以在中间休息的时候和这个参与者谈谈,确定他并不是不适宜这个过程。要让参与者觉得大家只是在记录而不是评价故事,会更乐于参与。 219 | 220 | 最后,再次重申故事编写工作坊中的讨论应该在**较高层面**上。我们的**目的是在短时间内写出更多的故事**。这不是设计界面或解决问题的时候。 221 | 222 | ## 4.4. 职责 223 | 224 | ### 客户团队职责 225 | 226 | - 负责理解并使用多种技巧捕捞用户故事 227 | - 负责尽早写更多的故事 228 | - 作为软件用户的主要代表,负责和他们多沟通 229 | - 了解怎么使用开发式和背景无关的提问 230 | - 如果需要关于编写故事的帮助,负责安排并举办一场或多次故事编写工作坊 231 | - 负责确保捕捞故事过程中考虑所有用户角色 232 | 233 | ### 开发人员职责 234 | 235 | - 负责理解并使用各种技巧来捕捞用户故事 236 | - 负责知道怎么使用开放式和背景无关的提问 237 | 238 | ## 4.5. 小结 239 | 240 | - 能够引出及捕捉需求这一想法是错误的。它有两个问题的假设:用户知道所有的需求;需求一旦捕捉,就锁定,不在改变。 241 | - 拖网捕鱼的比喻是非常有用:它说明了需求由不同的大小,需求会随着时间的推移变化,需要一些技巧来发现需求。 242 | - 即使敏捷流程支持需求的后期涌现,依然需要对预期的发布进行展望并开始写下容易发现的故事。 243 | - 我们可以通过用户访谈、观察用户、问卷调查和举办故事编写工作坊来发现用户故事。 244 | - 使用多种方法比过度使用一种更能获得好的效果。 245 | - 通过开放式、与背景无关的提问更容易获得有用的答案,例如,“告诉我你想怎么搜索工作?”就胜于“你要通过职位名称来搜索工作吗?” 246 | -------------------------------------------------------------------------------- /ch5/README.md: -------------------------------------------------------------------------------- 1 | # 5. 与用户代理合作 2 | 3 |  4 | 5 | 对于一个项目来说,客户团队里包括一个或多个真实用户是极其重要的。虽然其他人可以猜想用户如何使用软件,但事实上往往还在于实际用户。 6 | 7 | 遗憾的是,我们很难有机会与实际用户一起工作。 8 | > 例如,我们在开发一个广泛通用的产品,用户遍及全国各地,但我们没法也不适宜把一个(或多个)用户带到现场,与我们一起编写故事。或者我们正在开发一个给公司内部用的软件,但被告知我们不能与用户一起讨论。 9 | 10 | 我们期望与尽可能多的用户接触,这些用户代表了产品的不同角度,当我们无法接触到他们时,我们就需要求助于**用户代理(`user proxy`)**,他们自己可能不是用户,但他们在项目里**代表着用户**。 11 | 12 | 选择**合适的用户代理对于用户的成功至关重要**。我们要考虑潜在用户代理的**背景和动机**。 13 | 14 | 有营销背景的用户代理识别故事的方法,不同于由领域专家担当的用户代理。重要的是要**认识到这些差异**。 15 | 16 | 接下来,我们会探讨有时会**代替实际用户、不同类型的用户代理**。 17 | 18 | ## 5.1. 用户的经理 19 | 20 |  21 | 22 | 在开发一个供内部使用的项目是,组织可能不愿意让你完全不受限制的接触一个或多个用户,却**可能让你接触用户的经理**。 23 | 24 | 如果用户的经理不是软件的实际用户,这其实就是**偷梁换柱**。即使用户的经理的确是软件的用户,但是他**使用软件的模式肯定也与典型用户不同**。 25 | 26 | 例如: 27 | 28 | > 在一个呼叫中心的应用程序项目中,开发团队获准接触呼叫中心的轮班主管。 29 | > 虽然轮班主管确实在使用这个软件,但他们在新版本中想要的功能主要集中于管理呼叫队列和在坐席之间的呼叫转移上。 30 | 31 | 这些功能**对于轮班主管手下的人**来说,**重要性**却很**低**,但**这些人才是该软件的主要用户**。如果开发人员不能接触更多典型的用户,他们就会**过分关注轮班主管需要的功能**,但这些功能**很少使用**。 32 | 33 | 有些时候,用户的经理会从中干预,并且出于**自负**,想在项目中充当用户的角色。他可能承认自己不是典型用户,但**他固执己见**,**认为自己比用户更知道他们需要什么**。 34 | 35 | 在这种情况下,务必小心,不要得罪用户的经理,但是为了项目的成功,在部分围绕他的同事,也要想办法接触终端用户。针对这种情况,我们会在后续的[“5.10 与用户代理合作时,做些什么?”](#510-与用户代理合作时做些什么)专门聊聊如何来处理。 36 | 37 | ### 5分钟不等于1分钟 38 | 39 | 我们下面用一个故事来说明一下,有时候用户的经理是错误信息的来源。 40 | 41 | > 有个内部项目的“用户”是副总裁,他从来没用过这个软件。在他和终端用户之间,还隔着经理层。 42 | > 43 | > 在为下一个迭代的故事安排优先级是,他希望开发人员专注于提高数据库查询的速度。开发团队也注意到了这个高优先级的故事,但他们有些困惑。 44 | > 45 | > 他们知道应用程序的性能十分重要,所以已经在软件里建立了一个监测的机制:每次执行数据库查询时,它的运行参数、执行查询花的视觉以及用户的名字都会保存在数据库里。每天至少会监测一次这种信息,没有迹象表明性能问题。可是,他们的“用户”仍旧告诉他们,有些查询需要花“多达5分钟”的时间。 46 | > 47 | > 在与副总裁会晤之后,开发小组研究了一下查询的历史记录。他们发现:有两个用户执行的查询操作只花了一分钟就完成了。这虽然比所期待的时间要长,但考虑到他们查询的数据、数据库的大小以及执行这种类型的查询操作并不频繁,这种性能是符合预期的。 48 | > 49 | > 但是用户已经将这件事情汇报给他们的经理了。之后经理有汇报给副总裁;可是为了让副总裁注意到这个问题,经理却故意说查询花了2分钟时间。然后副总裁又将这个问题反馈给开发人员,为了让开发人员足够重视这个问题,他将这个问题说成了花了“多达5分钟”的时间。 50 | 51 | 所以,只要有可能,就要通过与实际用户交流来求证我们从用户的经理那儿获得的信息。 52 | 53 | ## 5.2 开发经理 54 | 55 |  56 | 57 | **让开发经理担任客户代表,是最坏的选择之一,除非你们开发的软件就是给开发经理使用的。** 58 | 59 | 尽管开发经理可能没什么不好的,但他们**最想获得是荣誉**,他们的**目标很可能是互相冲突的**。例如: 60 | 61 | > 开发经理排列故事的优先级会不同于实际用户排列的,因为这样可以让**提早给别人介绍令人兴奋的新技术**。 62 | 63 | 此外,开发经理的**目标可能与企业目标不一致**:或许他的年终奖跟项目结束日期有关系,这有可能导致他希望提前完成项目。 64 | 65 | 最后,大部分的开发经理对正在开发的软件没有像用户那样的亲身经历,而且他们也不是领域专家。 66 | 67 | > 如果以后的用户代理是开发经理,而他恰恰有拥有领域知识,最好把他视为领域专家。 68 | 69 | 判断是否有合格的用户代理前,请阅读[“5.4 领域专家”](#54-领域专家)部分。 70 | 71 | ## 5.3 销售人员 72 | 73 |  74 | 75 | **让销售人员从当用户代理是危险的,这会让大家对正在开发的产品没有一个全面的蓝图。** 76 | 77 | 对于销售人员来说,**最重要的故事是哪些如果没有实现就会导致他“丢单”的故事**。 78 | 79 | 如果故事因为产品没有“撤销”(undo)功能而让他失去一位客户,完全可以打赌,他会马上把故事“撤销”的卡片调整到顶部。 80 | 81 | 虽然基于丢单具体原因的重要程度,编写一两个新故事也是值得的。但是,如果一家产品研发公司**过分关注每一笔丢失的订单,他们可能会失去战略方向**,产品的长期远景就会停滞不前了。 82 | 83 | ## 5.4 领域专家 84 | 85 |  86 | 87 | **领域专家,有时也称为主题专家,是非常重要的资源**。他们对软件应用领域的了解程度对软件的成败有直接影响。 88 | 89 | 有些领域相对其他领域更难理解。 90 | 91 | > 我之前给医生和护士写过软件,虽然有时候软件会很复杂,但我通常明白客户的真正需求。后来,我参与过为统计遗传学家开发软件。在这个领域里,充满了诸如表型(`phenotype`)、重组率(`centiorgan`)和单体率(`haplotype`)这些我以前闻所未闻的词汇,因此该领域变得更加难以把握。 92 | 93 | 这使每个开发人员需要更多的依赖领域专家,让他们**帮助我们了解我们正在开发的软件**。 94 | 95 | 尽管领域专家是很好的资源,但他们是否对你有帮助,取决于他们**是否目前或曾经使用过你们正在开发的这种软件**。 96 | 97 | > 例如,开发一个工资系统时,毫无疑问你想要一位注册会计师(`CPA`, `Certified Public Accountant`)来做领域专家。 98 | > 99 | > 但是,由于未来的用户可能是薪资办理员,而不是注册会计师,很可能你会在薪资办理员那里才能了解到更好的故事。 100 | 101 | **在建立领域模型、确定业务规则是,领域专家是理想的资源,但是最好从实际用户哪里了解工作流以及使用方面的问题。** 102 | 103 | 让领域专家来担任用户代理的另一潜在的问题是,最终开发出的软件可能**仅仅针对那些与领域专家有类似水平的用户**。 104 | 105 | **领域专家会倾向将项目指向适合他们自己的解决方案上进行**,但这往往**过于复杂**,对目标用户群体而言明显是错误的。 106 | 107 | ## 5.5 市场营销团队 108 | 109 |  110 | 111 | `Larry Constantine`和`Lucy Lockwood`在1999年指出,**了解市场的是营销团队,而不是用户**。这会导致**营销团队或者有营销背景的人会更关注产品特性的数量,而轻视特性的质量**。 112 | 113 | 在很多情况下,**营销团队可以为相关故事的优先级提供更高层次的指导意见,但他们往往不具备很好的洞察力,无法提供故事的具体细节**。 114 | 115 | > 在一个公司里,由市场营销团队担当用户代表,他们将用一个新的产品来替换公司现有的纸质产品。这个公司的销售记录非常成功,他们销售的纸质书包含了医院和保险公司之间商定的规则:如果医院遵循了那些规则,他们就可以向保险公司报销。 116 | > 117 | > 例如:“若(以及其他条件)病人的白细胞数量高于一定的界限时,医院才能做阑尾炎切除手术。” 118 | > 119 | > 市场营销团队没有兴趣通过与用户交谈来了解他们想让软件做什么。相反,他们辩解说他们确切知道用户的需求,开发团队可以在他们的指导下进行开发工作。 120 | > 121 | > 营销团队选择了将软件做成纸书的电子版。他们并没有利用软件先天的灵活性,而只是满足于将软件当成一本“自动化的书籍”。 122 | > 123 | > 很显然,用户对软件很失望。 124 | > 125 | 126 | 很不幸,如果他们让实际用户而不是营销团队来担任用户代理,那么公司很早就可能已经发现这个问题了。 127 | 128 | ## 5.6 以前的用户 129 | 130 |  131 | 132 | 如果以前的用户会在不久前还是用过你们的软件,那么由他来担任用户代理是非常好的。但是和其他用户代理一样,**应该谨慎考虑他的目标和动机是否与实际用户的完全一致**。 133 | 134 | ## 5.7 客户 135 | 136 |  137 | 138 | **客户是哪些做出购买决定的人,他们不一定是软件的用户。** 139 | 140 | **考虑客户的期望是很重要的,因为开支票买软件的人是他们,而不是用户**(当然,除非你的用户和客户是同一批人)。 141 | 142 | > 企业的桌面办公软件是一个可以充分说明客户与用户区别的例子。企业的IT人员可以决定公司所有的员工使用哪一款字处理程序。 143 | 144 | 这个例子中,IT人员是客户,公司所有员工是用户(包括IT人员,他们即是客户,也是用户)。对于像这样的产品,其功能一定要够用,用户才不会大声抱怨;但这些功能也一定要能够吸引客户,使其决定购买。 145 | 146 | > 例如,对于大部分桌面办公软件的用户而言,安全特性通常不是很重要。然而,对于那些做出购买决定的IT(客户)而言,安全性却极为重要。 147 | 148 | 我曾经和一个项目团队一起工作,他们设计过一个数据密集型应用程序。该程序的数据是从客户现有的其他系统载入的。开人人员需要定义一个文件格式,用于交换数据。 149 | 150 | > 在这个案例中,客户是公司的首席信息官(`CIO`); 151 | > 这个功能的用户是他公司的IT人员,他们需要编写数据抽取程序,将现有系统的数据转换成新系统指定的格式。 152 | > 153 | > 在问对文件格式有什么偏好是,客户(这位CIO)决定使用`XML`,对于他来说比非标准的逗号分隔文件(`CSV`)更有吸引力。交付软件时,用户(IT人员)完全不赞成--他们喜欢更简单、易于生成的`CSV`文件。 154 | 155 | 如果**开发人员从用户那儿直接获取故事,那么他们早就可以知道这一点了**,也不会浪费时间在`XML`格式上了。 156 | 157 | ## 5.8 培训师和技术支持 158 | 159 | ### 培训师 160 | 161 |  162 | 163 | 由培训师和技术支持人员来充当用户代理看似合乎逻辑的选择。他们整天和实际用户交谈,所有他们一定知道用户的需求。 164 | 165 | 不幸的是,如果用培训师充当用户代理,你的系统最终只能成为一个**容易培训**的系统。 166 | 167 | ### 技术支持 168 | 169 |  170 | 171 | 类似的,如果你让技术支持来充当用户代理,那么你的系统**最终只是使得支持工作变得较为容易**。 172 | 173 | > 例如,某个做技术支持的人可能会把高级功能的优先级排的较低,因为他预计会增加支持工作的工作量。 174 | 175 | **把易于培训和良好的可支持性作为目标时,他们做优先级排列时很可能与真实用户不同**。 176 | 177 | ## 5.9 业务分析师或系统分析师 178 | 179 |  180 | 181 | 许多业务和系统分析师是很好的用户代理,因为他们即懂得技术,又熟悉软件相关的领域知识。能平衡好这些背景且努力跟实际用户沟通的分析师,通常会是非常出色的用户代理。 182 | 183 | 有些分析师暴露出来的问题是,他们**遇到问题喜欢空想,而不是去做调查**。我曾经与太多分析师一起工作,他们相信自己可以坐在办公室里凭直觉就知道用户的视觉需求,而不是与用户交谈。 184 | 185 | 必须注意,**项目的分析师应该同用户讨论,他不能只根据自己的观点来决定**。 186 | 187 | 让分析师担任用户代理时,第二个问题是偶尔会有**分析师喜欢在项目前期花太多时间**。 188 | 189 | > 在两个小时的角色建模和故事编写坊足以填满未来四个月的发布计划时,有些分析师却喜欢在这些活动上花3周时间。 190 | 191 | ## 5.10 与用户代理合作时,做些什么? 192 | 193 | 虽然不太理想,但不和实际用户一起而和用户代理一起,还是可能开发出优秀软件的。在这种情况下,还是有很多方法可以用来促进成功。 194 | 195 | ### 能访问到用户但访问受限时 196 | 197 | 访问实际用户受阻且团队被告知要和用户代理一起工作,由用户代理来做出项目相关的所有决定时,团队就要和他们合作,但同时也要与用户简历便捷的联系。 198 | 199 | 最好的方法之一是**请求准许启动一个用户顾问团队**(`user task force`)。用户顾问团队可以由数量不限的实际用户组成,从几个到几十个人都可以。 200 | 201 | **顾问团队能够提出意见和建议,而用户代理依然是项目最终的决策者。** 202 | 203 | 大多数情况下,用户代理会同意那么做,特别是因为这让他有一个防护网从而**避免做出错误决定的时候**。 204 | 205 | 一旦建立起用户顾问团队,并且配置实际用户。它就**可以指导每天越来越多的关于项目的决策**。可以开一系列的会议来讨论软件的一小部分,然后**让顾问团队来识别、编写并且排列用户故事**。 206 | 207 | ### 实在不能接触到用户时 208 | 209 | 当实在不能接触到用户是,必须求助于用户代理,一种有价值的方法是**使用多个用户代理**。 210 | 211 | 这有助于**减少一种可能性,即开发的系统仅仅准确的满足一个人的需求**。使用多个用户代理时,要确保**利用不同类型的用户代理**。 212 | 213 | > 例如,将一个领域专家和有市场背景的人组合,而不是使用两个领域专家。 214 | 215 | 为此,要么指定两个用户代理,或者只指定一位用户代理,但鼓励他依靠其他非正式的用户代理。 216 | 217 | 如果正在开发和其他商业产品竞争的软件,可以使用竞争者的产品作为一些故事的来源。 218 | 219 | 不过,我们要引入竞争者的产品作为故事来源,需要考虑下面的问题: 220 | 221 | - 在软件评测里提到了同类产品的哪些功能? 222 | - 在线新闻组里讨论过哪些功能? 223 | - 这些功能成为讨论的焦点是否由于其使用起来过于复杂? 224 | - ...... 225 | 226 | ### 尽早发布产品 227 | 228 | 另一个可用的方法就是**尽早发布产品**。 229 | 230 | 技师发布被称为雏形版本或早期测试版本,及早将软件交付到用户手里,有助于辨别出用户代理与实际用户之间想法的不一致。 231 | 232 | 更妙的是,一旦软件交付到一个或多个早期用户手里,你就打开了一条与用户沟通的途径,并且可以利用它与用户讨论后续的功能。 233 | 234 | ## 5.11 可以自己来吗? 235 | 236 | 当你无法找到或者访问到实际用户时,要**避免思维陷入误区**:你知道用户的想法,所以不需要或可以忽略你的用户代理。 237 | 238 | 虽然每种类型的用户代理对比实际用户都有一些缺点,但大部分**开发人员冒充实际用户时,有更多缺点**。 239 | 240 | 总的来说,开发人员没有市场背景,他们不懂软件功能的相对价值,他们不像销售人员那样频繁接触客户,他们不是领域专家等等。 241 | 242 | ## 5.12 建立客户团队 243 | 244 | 首先,要记住,在任何时候,实际用户总是优于用户代理。只要可能,就要邀请实际用户加入客户团队。 245 | 246 | 然而,当实际用户不能加入客户团队是,就需要有一个或多个用户代理。应该将客户团队建成一个优势互补的团队,一位成员的优势能平衡另一位成员的弱势。 247 | 248 | 建立客户团队有三步。 249 | 250 | - 第一,**邀请真实用户加入**。如果**有不同类型的用户使用软件,试着邀请每种类型的用户**。例如,在一个健康应用程序中,我们的用户是护士。在我们客户团队中,有常规的护士、肿瘤专家、糖尿病专家等。 251 | 252 | - 第二,**在客户团队中确定一位“项目负责人”(`project champion`)或“一把手”(`first among equals`)**。在商业软件公司里,这个人**通常是产品经理**,但也可以是其他人。这个项目负责人**负责协调客户团队的协作**。客户团队的所有成员,应尽力做到传递一致性的信息。尽管可能有多个客户,但对于开发项目来说,必须只能由一个客户声音。 253 | 254 | - 第三,确定项目成功必须的关键因素。这点随着项目的不同而不同。例如,如果项目是现有产品的下一代版本,那么**成功的关键之一是如何让现有用户轻松的转移到新系统上**。**将具有相关知识、技能和经验的用户代理补充到客户团队中,是项目成功的关键因素**。在将现有用户转移到新系统的例子里,这可能意味着要在客户团队中加入一位培训师。 255 | 256 | ## 5.13 职责 257 | 258 | ### 客户团队职责 259 | 260 | - 如果你不是软件的用户,则要负责了解自己属于哪类用户代理。 261 | - 负责理解自己会将哪些偏见导入到项目中,如何克服这个问题,无论是依靠别人还是其他方法。 262 | 263 | ### 开发人员职责 264 | 265 | - 负责帮助组织机构为项目物色合适的客户。 266 | - 负责了解不同类型的用户代理怎么考虑你们正在开发的系统,他们的背景如何影响交互。 267 | 268 | ## 5.14 小结 269 | 270 | - 我们了解了不同类型的用户代理,讨论了编写用户故事时,为什么用户代理不如实际用户理想。 271 | - 除非用户的经理也是用户,否则他就不是合适的用户代理。 272 | - 开发经理会试图担任用户代理,因为他们已经参与到项目每天的细节中。然而,开发经理大多数不是正在开发的软件的客户,所以他们不是理想的用户代理。 273 | - 在产品公司里,客户经常来自市场团队。来自市场团队的人通常是不错的用户代理,但他们通常关注于软件的功能数目,而不是其质量,这点必须克服。 274 | - 在很多不同的客户(而这些客户同时也是用户)联系的销售人员可以是很好的开发项目客户。销售人员必须避免把重点放在那些可以重新赢得已失去订单的故事上。在所有的情况下,销售人员是与用户沟通的有效渠道。 275 | - 领域专家可以成为优秀的客户代表,但必须避免一点:在为用户编写故事时,将产品开发成只适合那些与他们有相同水平的人使用。 276 | - 客户,那些做出购买决定的人,如果他们能与用户密切的交流,那么他们能成为非常好的用户代理。显然,如果客户自己也是用户,那就是完美的组合。 277 | - 为了成为好的用户代理,培训师和技术支持人员必须避免仅仅关注产品中那些他们每天关心的方面。 278 | - 我们简短的给出一些与用户代理一起工作的方法,包括用户顾问团队的使用,使用多个用户代理,分析竞争产品,尽早发布产品来获取用户反馈。 279 | -------------------------------------------------------------------------------- /theme/css/variables.css: -------------------------------------------------------------------------------- 1 | 2 | /* Globals */ 3 | 4 | /* :root { 5 | --sidebar-width: 120px; 6 | --page-padding: 15px; 7 | --content-max-width: 80%; 8 | --menu-bar-height: 40px; 9 | --pagetoc-width: 15%; 10 | --pagetoc-fontsize: 14.5px; 11 | } */ 12 | 13 | :root { 14 | --sidebar-width: 300px; 15 | --page-padding: 15px; 16 | --content-max-width: 80%; 17 | --menu-bar-height: 40px; 18 | --pagetoc-width: 15%; 19 | --pagetoc-fontsize: 14.5px; 20 | } 21 | 22 | /* @media only screen and (max-width:1439px) { 23 | :root{ 24 | --content-max-width: 98%; 25 | } 26 | } 27 | 28 | @media only screen and (max-width:1439px) { 29 | :root{ 30 | --content-max-width: 98%; 31 | } 32 | } 33 | 34 | @media only screen and (max-width:1439px) { 35 | :root{ 36 | --content-max-width: 98%; 37 | } 38 | } 39 | 40 | @media only screen and (max-width:1439px) { 41 | :root{ 42 | --content-max-width: 98%; 43 | } 44 | } 45 | 46 | @media only screen and (max-width:1439px) { 47 | :root{ 48 | --content-max-width: 98%; 49 | } 50 | } 51 | 52 | @media only screen and (max-width:1439px) { 53 | :root{ 54 | --content-max-width: 98%; 55 | } 56 | } 57 | 58 | @media only screen and (max-width:1439px) { 59 | :root{ 60 | --content-max-width: 98%; 61 | } 62 | } 63 | 64 | @media only screen and (max-width:1439px) { 65 | :root{ 66 | --content-max-width: 98%; 67 | } 68 | } 69 | 70 | @media only screen and (max-width:1439px) { 71 | :root{ 72 | --content-max-width: 98%; 73 | } 74 | } 75 | 76 | @media only screen and (max-width:1439px) { 77 | :root{ 78 | --content-max-width: 98%; 79 | } 80 | } 81 | 82 | @media only screen and (max-width:1439px) { 83 | :root{ 84 | --content-max-width: 98%; 85 | } 86 | } 87 | 88 | @media only screen and (max-width:1439px) { 89 | :root{ 90 | --content-max-width: 98%; 91 | } 92 | } 93 | 94 | @media only screen and (max-width:1439px) { 95 | :root{ 96 | --content-max-width: 98%; 97 | } 98 | } 99 | 100 | @media only screen and (max-width:1439px) { 101 | :root{ 102 | --content-max-width: 98%; 103 | } 104 | } 105 | 106 | @media only screen and (max-width:1439px) { 107 | :root{ 108 | --content-max-width: 98%; 109 | } 110 | } 111 | 112 | @media only screen and (max-width:1439px) { 113 | :root{ 114 | --content-max-width: 98%; 115 | } 116 | } 117 | 118 | @media only screen and (max-width:1439px) { 119 | :root{ 120 | --content-max-width: 98%; 121 | } 122 | } 123 | 124 | @media only screen and (max-width:1439px) { 125 | :root{ 126 | --content-max-width: 98%; 127 | } 128 | } */ 129 | 130 | /* Themes */ 131 | 132 | .ayu { 133 | --bg: hsl(210, 25%, 8%); 134 | --fg: #c5c5c5; 135 | 136 | --sidebar-bg: #14191f; 137 | --sidebar-fg: #c8c9db; 138 | --sidebar-non-existant: #5c6773; 139 | --sidebar-active: #ffb454; 140 | --sidebar-spacer: #2d334f; 141 | 142 | --scrollbar: var(--sidebar-fg); 143 | 144 | --icons: #737480; 145 | --icons-hover: #b7b9cc; 146 | 147 | --links: #0096cf; 148 | 149 | --inline-code-color: #ffb454; 150 | 151 | --theme-popup-bg: #14191f; 152 | --theme-popup-border: #5c6773; 153 | --theme-hover: #191f26; 154 | 155 | --quote-bg: hsl(226, 15%, 17%); 156 | --quote-border: hsl(226, 15%, 22%); 157 | 158 | --table-border-color: hsl(210, 25%, 13%); 159 | --table-header-bg: hsl(210, 25%, 28%); 160 | --table-alternate-bg: hsl(210, 25%, 11%); 161 | 162 | --searchbar-border-color: #848484; 163 | --searchbar-bg: #424242; 164 | --searchbar-fg: #fff; 165 | --searchbar-shadow-color: #d4c89f; 166 | --searchresults-header-fg: #666; 167 | --searchresults-border-color: #888; 168 | --searchresults-li-bg: #252932; 169 | --search-mark-bg: #e3b171; 170 | } 171 | 172 | .coal { 173 | --bg: hsl(200, 7%, 8%); 174 | --fg: #98a3ad; 175 | 176 | --sidebar-bg: #292c2f; 177 | --sidebar-fg: #a1adb8; 178 | --sidebar-non-existant: #505254; 179 | --sidebar-active: #3473ad; 180 | --sidebar-spacer: #393939; 181 | 182 | --scrollbar: var(--sidebar-fg); 183 | 184 | --icons: #43484d; 185 | --icons-hover: #b3c0cc; 186 | 187 | --links: #2b79a2; 188 | 189 | --inline-code-color: #c5c8c6;; 190 | 191 | --theme-popup-bg: #141617; 192 | --theme-popup-border: #43484d; 193 | --theme-hover: #1f2124; 194 | 195 | --quote-bg: hsl(234, 21%, 18%); 196 | --quote-border: hsl(234, 21%, 23%); 197 | 198 | --table-border-color: hsl(200, 7%, 13%); 199 | --table-header-bg: hsl(200, 7%, 28%); 200 | --table-alternate-bg: hsl(200, 7%, 11%); 201 | 202 | --searchbar-border-color: #aaa; 203 | --searchbar-bg: #b7b7b7; 204 | --searchbar-fg: #000; 205 | --searchbar-shadow-color: #aaa; 206 | --searchresults-header-fg: #666; 207 | --searchresults-border-color: #98a3ad; 208 | --searchresults-li-bg: #2b2b2f; 209 | --search-mark-bg: #355c7d; 210 | } 211 | 212 | .light { 213 | --bg: hsl(0, 0%, 100%); 214 | --fg: hsl(0, 0%, 0%); 215 | 216 | --sidebar-bg: #fafafa; 217 | --sidebar-fg: hsl(0, 0%, 0%); 218 | --sidebar-non-existant: #aaaaaa; 219 | --sidebar-active: #1f1fff; 220 | --sidebar-spacer: #f4f4f4; 221 | 222 | --scrollbar: #8F8F8F; 223 | 224 | --icons: #747474; 225 | --icons-hover: #000000; 226 | 227 | --links: #1f1fff; 228 | 229 | --inline-code-color: #F42C4C; 230 | 231 | --theme-popup-bg: #fafafa; 232 | --theme-popup-border: #cccccc; 233 | --theme-hover: #e6e6e6; 234 | 235 | --quote-bg: hsl(197, 37%, 96%); 236 | --quote-border: hsl(197, 37%, 91%); 237 | 238 | --table-border-color: hsl(0, 0%, 95%); 239 | --table-header-bg: hsl(0, 0%, 80%); 240 | --table-alternate-bg: hsl(0, 0%, 97%); 241 | 242 | --searchbar-border-color: #aaa; 243 | --searchbar-bg: #fafafa; 244 | --searchbar-fg: #000; 245 | --searchbar-shadow-color: #aaa; 246 | --searchresults-header-fg: #666; 247 | --searchresults-border-color: #888; 248 | --searchresults-li-bg: #e4f2fe; 249 | --search-mark-bg: #a2cff5; 250 | } 251 | 252 | .navy { 253 | --bg: hsl(226, 23%, 11%); 254 | --fg: #bcbdd0; 255 | 256 | --sidebar-bg: #282d3f; 257 | --sidebar-fg: #c8c9db; 258 | --sidebar-non-existant: #505274; 259 | --sidebar-active: #2b79a2; 260 | --sidebar-spacer: #2d334f; 261 | 262 | --scrollbar: var(--sidebar-fg); 263 | 264 | --icons: #737480; 265 | --icons-hover: #b7b9cc; 266 | 267 | --links: #2b79a2; 268 | 269 | --inline-code-color: #c5c8c6;; 270 | 271 | --theme-popup-bg: #161923; 272 | --theme-popup-border: #737480; 273 | --theme-hover: #282e40; 274 | 275 | --quote-bg: hsl(226, 15%, 17%); 276 | --quote-border: hsl(226, 15%, 22%); 277 | 278 | --table-border-color: hsl(226, 23%, 16%); 279 | --table-header-bg: hsl(226, 23%, 31%); 280 | --table-alternate-bg: hsl(226, 23%, 14%); 281 | 282 | --searchbar-border-color: #aaa; 283 | --searchbar-bg: #aeaec6; 284 | --searchbar-fg: #000; 285 | --searchbar-shadow-color: #aaa; 286 | --searchresults-header-fg: #5f5f71; 287 | --searchresults-border-color: #5c5c68; 288 | --searchresults-li-bg: #242430; 289 | --search-mark-bg: #a2cff5; 290 | } 291 | 292 | .rust { 293 | --bg: hsl(60, 9%, 87%); 294 | --fg: #262625; 295 | 296 | --sidebar-bg: #3b2e2a; 297 | --sidebar-fg: #c8c9db; 298 | --sidebar-non-existant: #505254; 299 | --sidebar-active: #e69f67; 300 | --sidebar-spacer: #45373a; 301 | 302 | --scrollbar: var(--sidebar-fg); 303 | 304 | --icons: #737480; 305 | --icons-hover: #262625; 306 | 307 | --links: #2b79a2; 308 | 309 | --inline-code-color: #6e6b5e; 310 | 311 | --theme-popup-bg: #e1e1db; 312 | --theme-popup-border: #b38f6b; 313 | --theme-hover: #99908a; 314 | 315 | --quote-bg: hsl(60, 5%, 75%); 316 | --quote-border: hsl(60, 5%, 70%); 317 | 318 | --table-border-color: hsl(60, 9%, 82%); 319 | --table-header-bg: #b3a497; 320 | --table-alternate-bg: hsl(60, 9%, 84%); 321 | 322 | --searchbar-border-color: #aaa; 323 | --searchbar-bg: #fafafa; 324 | --searchbar-fg: #000; 325 | --searchbar-shadow-color: #aaa; 326 | --searchresults-header-fg: #666; 327 | --searchresults-border-color: #888; 328 | --searchresults-li-bg: #dec2a2; 329 | --search-mark-bg: #e69f67; 330 | } 331 | 332 | @media (prefers-color-scheme: dark) { 333 | .light.no-js { 334 | --bg: hsl(200, 7%, 8%); 335 | --fg: #98a3ad; 336 | 337 | --sidebar-bg: #292c2f; 338 | --sidebar-fg: #a1adb8; 339 | --sidebar-non-existant: #505254; 340 | --sidebar-active: #3473ad; 341 | --sidebar-spacer: #393939; 342 | 343 | --scrollbar: var(--sidebar-fg); 344 | 345 | --icons: #43484d; 346 | --icons-hover: #b3c0cc; 347 | 348 | --links: #2b79a2; 349 | 350 | --inline-code-color: #c5c8c6;; 351 | 352 | --theme-popup-bg: #141617; 353 | --theme-popup-border: #43484d; 354 | --theme-hover: #1f2124; 355 | 356 | --quote-bg: hsl(234, 21%, 18%); 357 | --quote-border: hsl(234, 21%, 23%); 358 | 359 | --table-border-color: hsl(200, 7%, 13%); 360 | --table-header-bg: hsl(200, 7%, 28%); 361 | --table-alternate-bg: hsl(200, 7%, 11%); 362 | 363 | --searchbar-border-color: #aaa; 364 | --searchbar-bg: #b7b7b7; 365 | --searchbar-fg: #000; 366 | --searchbar-shadow-color: #aaa; 367 | --searchresults-header-fg: #666; 368 | --searchresults-border-color: #98a3ad; 369 | --searchresults-li-bg: #2b2b2f; 370 | --search-mark-bg: #355c7d; 371 | } 372 | } -------------------------------------------------------------------------------- /theme/css/chrome.css: -------------------------------------------------------------------------------- 1 | /* CSS for UI elements (a.k.a. chrome) */ 2 | 3 | @import 'variables.css'; 4 | 5 | ::-webkit-scrollbar { 6 | background: var(--bg); 7 | } 8 | ::-webkit-scrollbar-thumb { 9 | background: var(--scrollbar); 10 | } 11 | html { 12 | scrollbar-color: var(--scrollbar) var(--bg); 13 | } 14 | #searchresults a, 15 | .content a:link, 16 | a:visited, 17 | a > .hljs { 18 | color: var(--links); 19 | } 20 | 21 | /* Menu Bar */ 22 | 23 | #menu-bar, 24 | #menu-bar-hover-placeholder { 25 | z-index: 101; 26 | margin: auto calc(0px - var(--page-padding)); 27 | } 28 | #menu-bar { 29 | position: relative; 30 | display: flex; 31 | flex-wrap: wrap; 32 | background-color: var(--bg); 33 | border-bottom-color: var(--bg); 34 | border-bottom-width: 1px; 35 | border-bottom-style: solid; 36 | } 37 | #menu-bar.sticky, 38 | .js #menu-bar-hover-placeholder:hover + #menu-bar, 39 | .js #menu-bar:hover, 40 | .js.sidebar-visible #menu-bar { 41 | position: -webkit-sticky; 42 | position: sticky; 43 | top: 0 !important; 44 | } 45 | #menu-bar-hover-placeholder { 46 | position: sticky; 47 | position: -webkit-sticky; 48 | top: 0; 49 | height: var(--menu-bar-height); 50 | } 51 | #menu-bar.bordered { 52 | border-bottom-color: var(--table-border-color); 53 | } 54 | #menu-bar i, #menu-bar .icon-button { 55 | position: relative; 56 | padding: 0 8px; 57 | z-index: 10; 58 | line-height: var(--menu-bar-height); 59 | cursor: pointer; 60 | transition: color 0.5s; 61 | } 62 | @media only screen and (max-width: 420px) { 63 | #menu-bar i, #menu-bar .icon-button { 64 | padding: 0 5px; 65 | } 66 | } 67 | 68 | .icon-button { 69 | border: none; 70 | background: none; 71 | padding: 0; 72 | color: inherit; 73 | } 74 | .icon-button i { 75 | margin: 0; 76 | } 77 | 78 | .right-buttons { 79 | margin: 0 15px; 80 | } 81 | .right-buttons a { 82 | text-decoration: none; 83 | } 84 | 85 | .left-buttons { 86 | display: flex; 87 | margin: 0 5px; 88 | } 89 | .no-js .left-buttons { 90 | display: none; 91 | } 92 | 93 | .menu-title { 94 | display: inline-block; 95 | font-weight: 200; 96 | font-size: 2.4rem; 97 | line-height: var(--menu-bar-height); 98 | text-align: center; 99 | margin: 0; 100 | flex: 1; 101 | white-space: nowrap; 102 | overflow: hidden; 103 | text-overflow: ellipsis; 104 | } 105 | .js .menu-title { 106 | cursor: pointer; 107 | } 108 | 109 | .menu-bar, 110 | .menu-bar:visited, 111 | .nav-chapters, 112 | .nav-chapters:visited, 113 | .mobile-nav-chapters, 114 | .mobile-nav-chapters:visited, 115 | .menu-bar .icon-button, 116 | .menu-bar a i { 117 | color: var(--icons); 118 | } 119 | 120 | .menu-bar i:hover, 121 | .menu-bar .icon-button:hover, 122 | .nav-chapters:hover, 123 | .mobile-nav-chapters i:hover { 124 | color: var(--icons-hover); 125 | } 126 | 127 | /* Nav Icons */ 128 | 129 | .nav-chapters { 130 | font-size: 2.5em; 131 | text-align: center; 132 | text-decoration: none; 133 | 134 | position: fixed; 135 | top: 0; 136 | bottom: 0; 137 | margin: 0; 138 | max-width: auto; 139 | min-width: auto; 140 | 141 | display: flex; 142 | justify-content: center; 143 | align-content: center; 144 | flex-direction: column; 145 | 146 | transition: color 0.5s, background-color 0.5s; 147 | } 148 | 149 | .nav-chapters:hover { 150 | text-decoration: none; 151 | background-color: var(--theme-hover); 152 | transition: background-color 0.15s, color 0.15s; 153 | } 154 | 155 | .nav-wrapper { 156 | margin-top: 50px; 157 | display: none; 158 | } 159 | 160 | .mobile-nav-chapters { 161 | font-size: 2.5em; 162 | text-align: center; 163 | text-decoration: none; 164 | width: 90px; 165 | border-radius: 5px; 166 | background-color: var(--sidebar-bg); 167 | } 168 | 169 | .previous { 170 | float: left; 171 | } 172 | 173 | .next { 174 | float: right; 175 | right: var(--page-padding); 176 | } 177 | 178 | @media only screen and (max-width: 1080px) { 179 | .nav-wide-wrapper { display: none; } 180 | .nav-wrapper { display: block; } 181 | } 182 | 183 | @media only screen and (max-width: 1380px) { 184 | .sidebar-visible .nav-wide-wrapper { display: none; } 185 | .sidebar-visible .nav-wrapper { display: block; } 186 | } 187 | 188 | /* Inline code */ 189 | 190 | :not(pre) > .hljs { 191 | display: inline; 192 | padding: 0.1em 0.3em; 193 | border-radius: 3px; 194 | } 195 | 196 | :not(pre):not(a) > .hljs { 197 | color: var(--inline-code-color); 198 | overflow-x: initial; 199 | } 200 | 201 | a:hover > .hljs { 202 | text-decoration: underline; 203 | } 204 | 205 | pre { 206 | position: relative; 207 | } 208 | pre > .buttons { 209 | position: absolute; 210 | z-index: 100; 211 | right: 5px; 212 | top: 5px; 213 | 214 | color: var(--sidebar-fg); 215 | cursor: pointer; 216 | } 217 | pre > .buttons :hover { 218 | color: var(--sidebar-active); 219 | } 220 | pre > .buttons i { 221 | margin-left: 8px; 222 | } 223 | pre > .buttons button { 224 | color: inherit; 225 | background: transparent; 226 | border: none; 227 | cursor: inherit; 228 | } 229 | pre > .result { 230 | margin-top: 10px; 231 | } 232 | 233 | /* Search */ 234 | 235 | #searchresults a { 236 | text-decoration: none; 237 | } 238 | 239 | mark { 240 | border-radius: 2px; 241 | padding: 0 3px 1px 3px; 242 | margin: 0 -3px -1px -3px; 243 | background-color: var(--search-mark-bg); 244 | transition: background-color 300ms linear; 245 | cursor: pointer; 246 | } 247 | 248 | mark.fade-out { 249 | background-color: rgba(0,0,0,0) !important; 250 | cursor: auto; 251 | } 252 | 253 | .searchbar-outer { 254 | margin-left: auto; 255 | margin-right: auto; 256 | max-width: var(--content-max-width); 257 | } 258 | 259 | #searchbar { 260 | width: 100%; 261 | margin: 5px auto 0px auto; 262 | padding: 10px 16px; 263 | transition: box-shadow 300ms ease-in-out; 264 | border: 1px solid var(--searchbar-border-color); 265 | border-radius: 3px; 266 | background-color: var(--searchbar-bg); 267 | color: var(--searchbar-fg); 268 | } 269 | #searchbar:focus, 270 | #searchbar.active { 271 | box-shadow: 0 0 3px var(--searchbar-shadow-color); 272 | } 273 | 274 | .searchresults-header { 275 | font-weight: bold; 276 | font-size: 1em; 277 | padding: 18px 0 0 5px; 278 | color: var(--searchresults-header-fg); 279 | } 280 | 281 | .searchresults-outer { 282 | margin-left: auto; 283 | margin-right: auto; 284 | max-width: var(--content-max-width); 285 | border-bottom: 1px dashed var(--searchresults-border-color); 286 | } 287 | 288 | ul#searchresults { 289 | list-style: none; 290 | padding-left: 20px; 291 | } 292 | ul#searchresults li { 293 | margin: 10px 0px; 294 | padding: 2px; 295 | border-radius: 2px; 296 | } 297 | ul#searchresults li.focus { 298 | background-color: var(--searchresults-li-bg); 299 | } 300 | ul#searchresults span.teaser { 301 | display: block; 302 | clear: both; 303 | margin: 5px 0 0 20px; 304 | font-size: 0.8em; 305 | } 306 | ul#searchresults span.teaser em { 307 | font-weight: bold; 308 | font-style: normal; 309 | } 310 | 311 | /* Sidebar */ 312 | 313 | .sidebar { 314 | position: fixed; 315 | left: 0; 316 | top: 0; 317 | bottom: 0; 318 | width: var(--sidebar-width); 319 | font-size: 1em; 320 | box-sizing: border-box; 321 | -webkit-overflow-scrolling: touch; 322 | overscroll-behavior-y: contain; 323 | background-color: var(--sidebar-bg); 324 | color: var(--sidebar-fg); 325 | } 326 | .sidebar-resizing { 327 | -moz-user-select: none; 328 | -webkit-user-select: none; 329 | -ms-user-select: none; 330 | user-select: none; 331 | } 332 | .js:not(.sidebar-resizing) .sidebar { 333 | transition: transform 0.3s; /* Animation: slide away */ 334 | } 335 | .sidebar code { 336 | line-height: 2em; 337 | } 338 | .sidebar .sidebar-scrollbox { 339 | overflow-y: auto; 340 | position: absolute; 341 | top: 0; 342 | bottom: 0; 343 | left: 0; 344 | right: 0; 345 | padding: 10px 10px; 346 | } 347 | .sidebar .sidebar-resize-handle { 348 | position: absolute; 349 | cursor: col-resize; 350 | width: 0; 351 | right: 0; 352 | top: 0; 353 | bottom: 0; 354 | } 355 | .js .sidebar .sidebar-resize-handle { 356 | cursor: col-resize; 357 | width: 5px; 358 | } 359 | .sidebar-hidden .sidebar { 360 | transform: translateX(calc(0px - var(--sidebar-width))); 361 | } 362 | .sidebar::-webkit-scrollbar { 363 | background: var(--sidebar-bg); 364 | } 365 | .sidebar::-webkit-scrollbar-thumb { 366 | background: var(--scrollbar); 367 | } 368 | 369 | .sidebar-visible .page-wrapper { 370 | transform: translateX(var(--sidebar-width)); 371 | } 372 | @media only screen and (min-width: 620px) { 373 | .sidebar-visible .page-wrapper { 374 | transform: none; 375 | margin-left: var(--sidebar-width); 376 | } 377 | } 378 | 379 | .chapter { 380 | list-style: none outside none; 381 | padding-left: 0; 382 | line-height: 2em; 383 | } 384 | 385 | .chapter ol { 386 | width: 100%; 387 | } 388 | 389 | .chapter li { 390 | display: flex; 391 | color: var(--sidebar-non-existant); 392 | } 393 | .chapter li a { 394 | display: block; 395 | padding: 0; 396 | text-decoration: none; 397 | color: var(--sidebar-fg); 398 | } 399 | 400 | .chapter li a:hover { 401 | color: var(--sidebar-active); 402 | } 403 | 404 | .chapter li a.active { 405 | color: var(--sidebar-active); 406 | } 407 | 408 | .chapter li > a.toggle { 409 | cursor: pointer; 410 | display: block; 411 | margin-left: auto; 412 | padding: 0 10px; 413 | user-select: none; 414 | opacity: 0.68; 415 | } 416 | 417 | .chapter li > a.toggle div { 418 | transition: transform 0.5s; 419 | } 420 | 421 | /* collapse the section */ 422 | .chapter li:not(.expanded) + li > ol { 423 | display: none; 424 | } 425 | 426 | .chapter li.chapter-item { 427 | line-height: 1.5em; 428 | margin-top: 0.6em; 429 | } 430 | 431 | .chapter li.expanded > a.toggle div { 432 | transform: rotate(90deg); 433 | } 434 | 435 | .spacer { 436 | width: 100%; 437 | height: 3px; 438 | margin: 5px 0px; 439 | } 440 | .chapter .spacer { 441 | background-color: var(--sidebar-spacer); 442 | } 443 | 444 | @media (-moz-touch-enabled: 1), (pointer: coarse) { 445 | .chapter li a { padding: 5px 0; } 446 | .spacer { margin: 10px 0; } 447 | } 448 | 449 | .section { 450 | list-style: none outside none; 451 | padding-left: 20px; 452 | line-height: 1.5em; 453 | } 454 | 455 | /* Theme Menu Popup */ 456 | 457 | .theme-popup { 458 | position: absolute; 459 | left: 10px; 460 | top: var(--menu-bar-height); 461 | z-index: 1000; 462 | border-radius: 4px; 463 | font-size: 0.7em; 464 | color: var(--fg); 465 | background: var(--theme-popup-bg); 466 | border: 1px solid var(--theme-popup-border); 467 | margin: 0; 468 | padding: 0; 469 | list-style: none; 470 | display: none; 471 | } 472 | .theme-popup .default { 473 | color: var(--icons); 474 | } 475 | .theme-popup .theme { 476 | width: 100%; 477 | border: 0; 478 | margin: 0; 479 | padding: 2px 10px; 480 | line-height: 25px; 481 | white-space: nowrap; 482 | text-align: left; 483 | cursor: pointer; 484 | color: inherit; 485 | background: inherit; 486 | font-size: inherit; 487 | } 488 | .theme-popup .theme:hover { 489 | background-color: var(--theme-hover); 490 | } 491 | .theme-popup .theme:hover:first-child, 492 | .theme-popup .theme:hover:last-child { 493 | border-top-left-radius: inherit; 494 | border-top-right-radius: inherit; 495 | } -------------------------------------------------------------------------------- /theme/index.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |