2021年3月26日

gRPC 是什麼?以 Golang 進行示範與說明

gRPC 說明影片 @ BESG

:::tip source code
對應的程式碼可檢視 besg-grpc 的 repository。
:::

gRPC 是什麼:以 Golang 說明與實作

RPC 的全名是 remote procedure call,主要是作為電腦和電腦間溝通使用。A 電腦可以呼叫 B 電腦執行某些程式,B 電腦會將結果回傳給 A 電腦,A 電腦在收到回應後會再繼續處理其他任務。RPC 的好處在於,雖然 A 電腦是發送請求去請 B 電腦做事,但其呼叫的方式,就很像是 A 電腦直接在呼叫自己內部的函式一般。
gRPC 也是基於這樣的概念,讓想要呼叫 server 處理請求的 client,在使用這支 API 時就好像是呼叫自己內部的函式一樣簡單自然。從功能面來說,gRPC 就像 Web 常用的 Restful API 一樣,都是在處理請求和回應,並且進行資料交換,但 gRPC 還多了其他的功能和特色。
gRPC 是由 Google 開發的開源框架,它快速有效、奠基在 HTTP/2 上提供低延遲(low latency),支援串流,更容易做到權限驗證(authentication)。在下面的文章中,將會對於 gRPC 能提供的特色有更多說明。

Protocol Buffers 是什麼

在學習 gRPC 時,需要同時了解什麼是 Protocol Buffers。在傳統的 Restful API 中,最常使用的資料交換格式通常是 JSON;但到了 gRPC 中,資料交換的格式則是使用名為 Protocol Buffers 的規範/語言。
Protocol Buffers vs JSON
也就是說,當我們想要使用 gRPC 的服務來交換資料前,必須先把資料「格式」和「方法」都定義清楚。
:::tip
使用 gRPC 前,不只需要先把資料交換的格式定義清楚,同時也需要把資料交換的方法定義清楚。
:::
這裡要稍微釐清一點很重要的是,Protocol Buffers 可以獨立使用,不一定要搭配 gRPC;但使用 gRPC 一定要搭配 Protocol Buffers

實作將 Protocol Buffers 編譯成在 Golang 中可使用的檔案

對應的程式碼可檢視 besg-grpc repository 中的 proto 資料夾。

STEP 1:撰寫 Protocol Buffers 檔案

  • 使用 message 定義資料交換的格式
  • 使用 service 定義呼叫 API 的方法名稱
syntax = "proto3";  // 定義要使用的 protocol buffer 版本

package calculator;  // for name space
option go_package = "proto/calculator";  // generated code 的 full Go import path

message CalculatorRequest {
  int64 a = 1;
  int64 b = 2;
}

message CalculatorResponse {
  int64 result = 1;
}

service CalculatorService {
  rpc Sum(CalculatorRequest) returns (CalculatorResponse) {};
}

STEP 2:安裝編譯 Protocol Buffer 所需的套件

此部份可參考 編譯 Protocol Buffers(Compiling) 段落。

安裝 compiler

# 安裝 compiler,安裝完後就會有 protoc CLI 工具
$ brew install protobuf
$ protoc --version  # Ensure compiler version is 3+

# 安裝 protoc-gen-go 後可以將 proto buffer 編譯成 Golang 可使用的檔案
$ go get github.com/golang/protobuf/protoc-gen-go

# 安裝 grpc-go 後,可以在 Golang 中使用 gRPC
$ go get -u google.golang.org/grpc

STEP 3:編譯 Protocol Buffer 檔案

進到放有 .proto 檔的資料夾後,在終端機輸入下述指令:
$ protoc *.proto --go_out=plugins=grpc:. --go_opt=paths=source_relative
在成功編譯好後,應該會看到同樣的資料夾位置出現 *.pb.go 的檔案,這就是編譯好後可以在 Golang 中使用 Protocol Buffer 和 gRPC 的檔案。

實作 gRPC Server

對應的程式碼可檢視 besg-grpc repository 中的 server 資料夾。

STEP 1:建立 gRPC server

type Server struct {}

func main() {
 fmt.Println("starting gRPC server...")

 lis, err := net.Listen("tcp", "localhost:50051")
 if err != nil {
  log.Fatalf("failed to listen: %v \n", err)
 }

 grpcServer := grpc.NewServer()
 calculatorPB.RegisterCalculatorServiceServer(grpcServer, &Server{})

 if err := grpcServer.Serve(lis); err != nil {
  log.Fatalf("failed to serve: %v \n", err)
 }
}

STEP 2:實作 Protocol Buffer 中的 service

func (*Server) Sum(ctx context.Context, req *calculatorPB.CalculatorRequest) (*calculatorPB.CalculatorResponse, error) {
 fmt.Printf("Sum function is invoked with %v \n", req)

 a := req.GetA()
 b := req.GetB()

 res := &calculatorPB.CalculatorResponse{
  Result: a + b,
 }

 return res, nil
}

STEP 3:啟動 server

在終端機中輸入:
$ go run server/server.go
即可啟動 gRPC server。

補充:使用 Bloom RPC 進行測試

在只有 server 的情況下,可以使用BloomRPC 這套工具來模擬 Client 對 gRPC server 發送請求,功能就類似在 Restful 中使用的 Postman。
使用時只需要匯入 proto 檔後,即可看到對應可呼叫的方法和可帶入的參數,能這麼方便也是因為在 protocol buffer 中已經把傳輸的資料格式和能對應呼叫的方法都定好的緣故。
Bloom RPC

建立 gRPC Client

完整程式碼可檢視 besg-grpc repository 中的 client 資料夾。

STEP 1:與 gRPC server 建立連線

func main() {
 conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
 if err != nil {
  log.Fatalf("failed to dial: %v", err)
 }

 defer conn.Close()

 client := calculatorPB.NewCalculatorServiceClient(conn)

 doUnary(client)
}

STEP 2:使用 Protocol Buffers 中定義好的 Service

func doUnary(client calculatorPB.CalculatorServiceClient) {
 fmt.Println("Staring to do a Unary RPC")
 req := &calculatorPB.CalculatorRequest{
  A: 3,
  B: 10,
 }

 res, err := client.Sum(context.Background(), req)
 if err != nil {
  log.Fatalf("error while calling CalculatorService: %v \n", err)
 }

 log.Printf("Response from CalculatorService: %v", res.Result)
}

STEP 3:向 server 發送請求

在終端機中輸入:
$ go run client/client.go
即可執行 client.go 並向剛剛起動好的 server 發送請求。

gRPC 解決了什麼

gRPC 和 REST API 的比較

簡單來說,gRPC 在效能上比起 REST API 好非常多:
項目 gRPC Restful API
資料傳輸格式(Payload) Protocol Buffer - 更快且更小 JSON, XML, formData - 較慢且較大
通訊協定 HTTP/2 HTTP
傳輸方式 支援一般的「請求-回應」、伺服器端串流、Client 端串流、與雙向串流(streaming) 僅能透過 Client 發送請求、Server 給予回應
API 方法命名 沒有限制,一般會直接描述該方法要做的事,例如 createUser, getUser。不需要思考路由命名。 使用動詞(GET, POST, PUT, PATCH, DELETE)搭配資源來命名。需要根據不同的行為來定義不同的路由。
Client 呼叫 API 的方式 就像呼叫一般的函式 透過特定的 Endpoint,給予符合的資料型別
Server 建立 API 的方式 根據文件(Protocol Buffer)實作功能,不需要額外檢查資料型別與方法正確性。 根據文件(Swagger)實作功能,但須額外檢查資料型別。
根據文件產生程式碼 gRPC OpenAPI / Swagger
此外,gRPC 的 server,預設就是非同步的,因此不會阻塞任何進來的請求,並可以平行處理多個請求。gRPC Client 則可以選擇要用同步(阻塞)或非同步的方式處理。

使用 Protocol Buffers 的好處

  • 節省網路傳輸量:速度更快、檔案更小
  • 節省 CPU 消耗:Parse JSON 本身是 CPU intensive 的任務;Parse Protocol Buffer(binary format)因為更接近底層機器表徵資料的方式,消耗的 CPU 資源較低
  • 跨程式語言:Protocol Buffer 可以根據不同的程式語言編譯出不同的檔案
  • 可以寫註解、型別清楚明確
:::tip
節省網路傳輸量和 CPU 消耗在行動裝置上的影響可能更重要。
:::

跨程式語言的好處

透過 Protocol Buffer 定義好資料的傳輸欄位(message)和呼叫的方法(service)後,gRPC 即可在不同程式語言上運行,這非常適合微服務(micro-services)的應用情境,只要雙方一起定義好 schema 後,就可以用不同的程式語言進行開發。

使用 HTTP/2 的好處

傳統的 HTTP/1.1 在每個 TCP 連線中只允許向 server 發送單一個請求,但當網頁載入時,往往會需要向同一個伺服器發送多個請求(例如、圖檔、CSS、靜態檔、JS 等),因此為了要避開這樣的限制、加快載入的速度,瀏覽器會實作多個平行的(parallel) TPC 連線(每個瀏覽器實作不同,因此數量的上限也不同),以處理同時向伺服器發出的多個請求。
在 HTTP/2 中則可在同一個 TCP 連線中進行多個請求和回應,並且可以由 server 主動推送資源給 client,而並非一定要透過 client 主動請求;此外支援 HTTP Header 的壓縮,減少資料傳數量;HTTP/2 也是使用 binary 的方式在傳輸資料。
HTTP2

gRPC 的四種類型

  • Unary:類似傳統 API,client 發送 request 而 server 回傳 response
  • Server Streaming:透過 HTTP/2,client 發送一次 request,而 server 可以回傳多次資料
  • Client Streaming:client 發送多次資料,直到告知 server 資料傳完後,server 再給予 response
  • Bi Directional Streaming:兩邊都用串流的方式傳送資料
gRPC
service GreetService {
  // Unary
  rpc Greet(GreetRequest) returns (GreetResponse) {};

  // Streaming Server
  rpc GreetManyTimes(GreetManyTimesRequest) returns (stream GreetManyTimesResponse) {};

  // Streaming Client
  rpc LongGreet(stream LongGreetRequest) returns (LongGreetResponse) {};

  // Bi-directional Streaming
  rpc GreetEveryone(stream GreetEveryoneRequest) returns (stream GreetEveryoneResponse) {};
}

gRPC 的缺點

  • Protocol Buffer 不像 JSON 是 Human Readable。
  • 需要額外的學習時間和導入成本。
  • 瀏覽器原生目前還不支援,須透過套件 grpc-web 來處理。

其他

推薦工具

  • BloomRPC:方便用來模擬 Client 對 gRPC server 發送請求,功能就類似在 Restful 中使用的 Postman。

錯誤排除

protoc-gen-go: program not found or is not executable

# 需要把 $GOPATH/bin 加到 .zshrc/.bashrc 等
$ echo 'export PATH=$PATH:$GOPATH/bin' >> $HOME/.zshrc

參考資料

2021年3月8日

[React] 讓父層可以取得子層的 DOM 元素:ForwardRef 的使用

imgur

目的

有些時候父層的元件希望能夠取得子層的 DOM 元素(例如,buttoninput),以便能夠在父層控制子層 DOM 元素的 focus, selection 或 animation 的效果。這時就可以使用 Ref forwarding 來讓父層取得子層 DOM 元素,以便控制和操作它。
:::note
通常需要被 forwardRef 的子層元件會是封裝好的元件(例如,套件),其他使用它的開發者無法直接修改,因此才需要透過 forwardRef 把控制權交給父層元件,讓其他開發者可以直接控制。
:::

forwardRef 基本使用

舉例來說,我們建立一個 AwesomeInput 元件:
const AwesomeInput = (props) => <input type="text" />;
接著我們在父層 <App> 元件中使用 <AwesomeInput /> 元件:
const App = () => {
  return <AwesomeInput />;
};
這時候如果想要在 App 元件得到 AwesomeInput 中 <input /> 的 value 或者是對它進行 focus 的動作,就需要透過 forwardRef 把這個 <input /> 傳到父層以供使用。
要讓 <App /> 可以操作到 <AwesomeInput /> 中的 <input /> 元素,需要:

STEP 1:在父層元件建立 ref

  • 先在父層元件透過 useRefcreateRef 建立一個 ref,這裡取名作 awesomeInputRef
  • 把建立好的 awesomeInputRef 透過 ref 屬性傳到 <AwesomeInput /> 元件內
const App = () => {
  const awesomeInputRef = React.useRef(null);

  return <AwesomeInput ref={awesomeInputRef} />;
};

STEP 2:在 AwesomeInput 使用 forwardRef

接著在 <AwesomeInput /> 元件中,可以使用 React.forwardRef() 來把 <AwesomeInput /> 內的 <input /> 傳出去:
  • 一般的 React Component 是不會取得 ref 的屬性,需要被 React.forwardRef() 包起來的才會有 ref 屬性:
const AwesomeInput = React.forwardRef((props, ref) => {
  return <input type="text" ref={ref} />;
});

STEP 3:在 App 可以使用 AwesomeInput 中的 input DOM 元素

這時候,就可以直接在 App 中操作 AwesomeInput 中的 <input /> 元素,舉例來說,我們希望做到 autoFocus 的效果,就可以在 <App /> 元件中,透過 awesomeInputRef 取得裡面的 <input /> 元素:
const App = () => {
  const awesomeInputRef = React.useRef(null);

  // App mounted 的時候讓 AwesomeInput 中的 input 元素 focus
  React.useEffect(() => {
    console.log(awesomeInputRef.current); // <input type="text">...</input>
    awesomeInputRef.current.focus(); // 對 AwesomeInput 中的 <input /> 進行操作
  }, []);

  return <AwesomeInput ref={awesomeInputRef} />;
};

在 HOC 中使用 forwardRef

現在假設建立一個名為 logPropsHOC 的 Higher Order Component(HOC),單純是把元件的 props 有改變時 console 出來的作用:
const logPropsHOC = (WrappedComponent) => {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props: ', prevProps);
      console.log('new props: ', this.props);
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  }

  return LogProps;
};
這時候,如果我們把上面寫過的 AwesomeInput 元件用此 HOC 包起來後再使用:
const AwesomeInput = React.forwardRef((props, ref) => {
  return <input type="text" ref={ref} />;
});

const AwesomeInputWithLogProps = logPropsHOC(AwesomeInput);
接著在 App 中使用 AwesomeInputWithLogProps
const App = () => {
  const awesomeInputRef = React.useRef(null);

  React.useEffect(() => {
    console.log(awesomeInputRef.current); // <input type="text">...</input>
    // awesomeInputRef.current.focus(); // 對 AwesomeInput 中的 <input /> 進行操作
  }, []);

  return <AwesomeInputWithLogProps ref={awesomeInputRef} />;
};
這時候會發現 awesomeInputRef.current 的對象變成是該 HOC 元件,也就是 LogProps,而不像原本一樣能夠指稱到 AwesomeInput 元件中的 <input /> 元素。
為什麼會這樣呢?這是因為,雖然我們有在 LogProps 中有試著用:
return <WrappedComponent {...this.props} />;
把所有的 props 都帶回到原本的元件中,但因為 ref 並不是 prop,所以在 props 中並沒有 ref 的內容,進而不會傳遞到 AwesomeInput 元素中
即使在 AwesomeInput 元件包了 HOC 後,為了要讓 App 元件還是能夠取得 AwesomeInput 元件中的 <input />,我們需要在 HOC 中一樣先透過 React.forwardRef 取出 ref 後,再把它傳遞到被 HOC 包覆起來的 WrappedComponent 中,具體來說可以這樣做:

STEP 1:將 HOC 回傳的元件也用 React.forwardRef

首先將 HOC 回傳的元件也用 React.forwardRef 取出 ref,接著再把這個 ref 傳到 LogProps 元件內:
const logPropsHOC = (WrappedComponent) => {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props: ', prevProps);
      console.log('new props: ', this.props);
    }

    render() {
      return <WrappedComponent {...this.props} />;
    }
  }

  return React.forwardRef((props, ref) => <LogProps {...props} forwardedRef={ref} />);
};
:::caution
要留意的是 ref 是 React 元件的關鍵字,這裡如果單純只是要把 ref 當成 props 往下傳的話,不能用 ref 當名稱,因此這裡使用 forwardedRef 作為 props 的名稱。
:::

STEP 2:從 props 中取出 forwardRef 並帶到下層的 ref

接著就可以從 props 中取出 forwardedRef 後,透過 ref 把它帶進去:
const logPropsHOC = (WrappedComponent) => {
  class LogProps extends React.Component {
    componentDidUpdate(prevProps) {
      console.log('old props: ', prevProps);
      console.log('new props: ', this.props);
    }

    render() {
      const { forwardedRef, ...rest } = this.props;
      return <WrappedComponent ref={forwardedRef} {...rest} />;
    }
  }

  return React.forwardRef((props, ref) => <LogProps {...props} ref={ref} />);
};
:::tip
留意 Component 使用 ref 時,是要使用 ref 的功能,還是只是要把父層的 ref 當層 props 往下傳遞,如果是要把 ref 當成 props 往下傳遞,就不能使用 ref 當作屬性名稱,而要換名字,例如 forwardedRef={ref}
:::

針對 Class Component 使用 forwardRef

針對 Class Component 使用 forwardRef 的方式和 Function Component 非常相似。首先將原本的 AwesomeInput 元件改成 class component:
class AwesomeInput extends React.Component {
  render() {
    return <input type="text" />;
  }
}

STEP 1:透過 props 接受父層傳進來的 ref

由於要把 ref 當成 props 傳遞時,props 的名稱不能直接使用 ref,因此改名為 forwardedRef,並從 props 中取出:
class AwesomeInput extends React.Component {
  render() {
    const { forwardedRef } = this.props;
    return <input type="text" ref={forwardedRef} />;
  }
}
:::note
Class Component 雖然能直接使用在元素的使用上使用 ref,但它會指稱到的是該 class 的 instance 而非 <input />
:::

STEP 2:使用 React.forwardRef 取出父層傳進來的 Ref,並以 props 往下傳遞

接著要使用 React.forwardRef 先取出父層傳入的 ref,並改名為 forwardedRef 後以 props 傳入 AwesomeInput 元件中:
const AwesomeInputWithForwardRef = React.forwardRef((props, ref) => {
  // 把父層的 ref 透過 props 往下傳
  return <AwesomeInput forwardedRef={ref} {...props} />;
});

STEP 3:在父層中可直接取用到 AwesomeInput 元件中的 input 元素

App 元素並不需要做什麼更改,即可取得 <AwesomeInput /> 元件中的 <input /> 元素:
const App = () => {
  const awesomeInputRef = React.useRef(null);

  React.useEffect(() => {
    console.log(awesomeInputRef.current); // <input type="text">...</input>
    awesomeInputRef.current.focus(); // 對 AwesomeInput 中的 <input /> 進行操作
  }, []);

  return <AwesomeInputWithForwardRef ref={awesomeInputRef} />;
};

幾種不同的變化型

若有需要在父層取得子層的 ref,React 在官方文件中的 Exposing DOM Refs to Parent Components 建議使用 React.forwardRef 的做法,但若你使用的是 React 16.2 以前的版本,或需要更彈性的用法,則直接把 ref 當成 props 直接傳到子層也是可行的,但 props 的名稱不能用 ref,需另外取名。

Function Component:使用 React.forwardRef

  • 直接在 JSX 的 Function Component 中使用 ref
const AwesomeInput = React.forwardRef((props, ref) => {
  return <input type="text" ref={ref} />;
});

const App = () => {
  const awesomeInputRef = React.useRef(null);

  React.useEffect(() => {
    console.log(awesomeInputRef.current); // <input type="text">...</input>
    awesomeInputRef.current.focus(); // 對 AwesomeInput 中的 <input /> 進行操作
  }, []);

  // 直接在 JSX 的 Function Component 中使用 ref
  return <AwesomeInput ref={awesomeInputRef} />;
};

Function Component:直接將 ref 當成 props 往下傳

  • 在 JSX 的 Function Component 中,不用 ref 當屬性名稱,換成其他名稱後以 props 的方式往下傳(例如,forwardedRef
const AwesomeInput = (props) => {
  // 取用的是 forwardedRef
  return <input type="text" ref={props.forwardedRef} />;
};

const App = () => {
  const awesomeInputRef = React.useRef(null);

  React.useEffect(() => {
    console.log(awesomeInputRef.current); // <input type="text">...</input>
    awesomeInputRef.current.focus(); // 對 AwesomeInput 中的 <input /> 進行操作
  }, []);

  // 將 ref 當成 props(不使用 ref 作為屬性名稱)往下傳
  return <AwesomeInput forwardedRef={awesomeInputRef} />;
};

Class Component:直接使用 ref 往下傳

錯誤寫法

  • 雖然 class component 可以直接接收 ref 做為參數,但它會指稱到的是該 Component 的 instance 而不是 Component 內的 DOM 元素
// 錯誤寫法:ref 會是 AwesomeInput 元件,而不是內部的 input 元素
class AwesomeInput extends React.Component {
  render() {
    const { ref } = this.props;
    return <input type="text" ref={ref} />;
  }
}

const App = () => {
  const awesomeInputRef = React.useRef(null);

  React.useEffect(() => {
    // 這裡指稱到的會是 `<AwesomeInput />`
    console.log(awesomeInputRef.current); // <AwesomeInput />
  }, []);

  // 直接透過 ref 往下傳
  return <AwesomeInput ref={awesomeInputRef} />;
};

使用 forwardRef

見上方段落「針對 Class Component 使用 forwardRef」的說明。

可行寫法

// 在 AwesomeInput 建立另一個 ref 以指稱到 input 元素
class AwesomeInput extends React.Component {
  innerRef = React.createRef();

  render() {
    return <input type="text" ref={this.innerRef} />;
  }
}

const App = () => {
  const awesomeInputRef = React.useRef(null);

  React.useEffect(() => {
    // 先指稱到 AwesomeInput 元件,再進去裡面找到 input 元素
    console.log(awesomeInputRef.current.innerRef.current);
  }, []);

  return <AwesomeInput ref={awesomeInputRef} />;
};

其他參考

2021年1月30日

敏捷開發:你我心中,都有一個屬於自己的 MVP

agile-scrum
由於自己沒有真的很理解過敏捷開發或 Scrum 到底是怎麼一回事,但公司的新專案又開始想要嘗試用這種方式來做 MVP,基於凡事問網友的心態,在 ALPHACamp 社群以及 Twitter 上得到了很多有價值的回饋,很感謝大家分享意見讓我知道。
我的問題大概是這樣的:
「想要請問大家所謂的敏捷開發?
團隊認為敏捷開發就是要快,不用管太多細節,先推出去收到市場回饋再來修改。但很多工程面的東西像是 DB 的設計,API 的規格的改動,添加幾個欄位還好,但若是動到比較大的架構改起來都很痛。
是真的想要了解大家在碰到這種情況是怎麼處理?因為我沒有深入理解過敏捷開發,不清楚在實際執行上的細節,只知道每次都說就是要快,先不考慮其他情況。
是不是我們沒有掌握到敏捷開發的核心?還是具體來說有那些東西是可以很「敏捷」?哪些東西不適合嗎?
先謝謝有經驗到大家」
下面則整理大家的想法和意見。
:::caution
本人並沒有任何實際了解過敏捷或 scrum 的學習經驗,因此若有任何錯誤或想法,都歡迎提出並一起討論。
:::

敏捷的重點在於快速應對市場需求變化的能力

首先,敏捷的重點在於快速應對市場需求變化的能力,同時也驗證自己對於產品能否達到市場需求的概念(Proof of Concept, POC),減少過多的時間或人力成本在開發某個專案,做到非常完整,但推出去之後才被市場打臉。
這裡直接 quote Jack Sung 所說的:
Jack Sung:「敏捷開發並不是短期的速度快,畢竟開發還是需要跑完基本的流程,而是面對市場變化的反應速度比起瀑布流還快,就像 POC(Proof of Concept),敏捷開發是更務實的去驗證想法,而非一個美好的想像去花費很高的時間跟成本去開發一個未經驗證的產品,等到真的推上線才被市場賞兩巴掌。」
:::info
至於什麼叫「完整」或「非常完整」真的是非常藝術的問題,有的人認為東西可以 work 就可以推出,有的人認爲至少要通過自己這一關後才算可以,而設計、工程、一直到商業端每個人的想法也都不同。也許業務認為有東西可以賣就好,設計認為的會是這個產品拿出去可以見人嗎?工程思考的可能是產品夠不夠穩定,有沒有漏洞,資料庫後續如何修改等等。
:::

敏捷的目的是要提早發現問題:對象是市場而非開發團隊

之所以要敏捷是因為市場的變化太快速,因此當有產品沒有收到預期的回饋時,適時且立即的停止、停損或轉換方向是敏捷的重點與核心。
然而,這並不表示要壓縮開發時程或開發品質,如同 Yan 所說的,整個開發流程應該還是嚴謹可靠的,而不是隨便快就好的。
Yan:「敏捷開發不單單是針對開發團隊而是整個軟體工程,從訪談>需求>規格>設計>開發>測試>發布,整個流程應該還是嚴謹的,只是它縮小整個目標進行對目標進行快速迭代,中途有錯應該立即修改或終止,這是 scrum 所謂的「快速試錯」,而已經成型產品遇到要整個砍掉重來,長遠來說如果是必要,我也認同需要去做,根據我的經驗以開發來說敏捷並不會減少「開發時間」。」

敏捷開發不會比較快速:敏捷 != 快

如同上面所述,敏捷的重點是為了因應市場的快速變化,避免到後來才發現一場空。但這個敏捷並不是快的意思,該有的開發流程還是不能省,Rafeni 分享的這張圖我覺得很到位:
Minumum Viable Product
這裡直接引用 ALPHACamp 助教 Yan 所說:
Yan:「目標是造車,縮小目標往往會被誤認為先造輪子,但其實應該是「可用的」滑板車,因為只造輪子根本收不到市場/客戶回饋,根本無法知道產品方向對不對;壓縮開發時間=壓縮開發品質,各種開發模式都是透過流程改善來減少時間成本,而非去壓榨單位去達到節省目的。」
每次開發出來的「可用的產品」,到底是要驗證什麼概念,是否能實際收到市場的回饋都是要考慮的問題。至於給人使用的「產品」和有功能就好的「東西」之間的界線要如何定義,又是另一門藝術了。

盡可能在開發時做到解耦、模組化

據 Jack Sung 所說,一般來說走敏捷開發對工程團隊來講是痛苦的,太多東西是無法預期或事先規劃的,因此打掉重練是常有的事,能做的是盡可能在開發時做到解耦、模組化,以減少這個痛。
agile-scrum
畢竟以上圖來說,腳踏車和滑板車已經是完全不同的概念了,要做到擴充性和預留彈性幾乎就是不可能的事,所以要認知到打掉重練可能是必經的過程

打掉重練是常有的事,但⋯再想想⋯

雖然說打掉重練可能是常有的事,但如果總是在經歷「整個」打掉重練,而不是部分功能或模組打掉,或是整個系統架構的調整,這時候就需要再多想想了。
如同前幾個段落中的金字塔圖(MVP)所示,在敏捷開發中並非省略掉開發流程來達到敏捷。Rafeni 提到,不應該打著敏捷開發就跳過了規劃、跳過了去思考產品是設計給誰用的、是要幫誰解決問題。 Jacob 則提到,PM 和 RD 溝通不良,或當問題都沒有先想清楚就馬上進到開發,產品設計沒有做好使用者研究時,常常才是導致架構需要不斷大改。
Rafeni:「因為起點對了,過程與終點才可能對。否則錯誤的流程也是會導致錯誤的結果的。」
也就是說,如果你覺得常常是整個系統在進行大架構的改動,甚至是經常感受到隕石擊落般的痛快時,那麼不要乖乖的抱著「打掉重練是正常的想法」,而是需要好好的再想想

參考資料