JavaScript 是一個動態弱型別
的語言,
Golang 是一個靜態強型別
的語言,
但在學習的過程中,我常常驚訝…
Golang 這做法真 JavaScript
從JavaScript學習到Golang 並不太像學一門完全新
的語言,
而是像學習一門解決 JavaScript 眾多雷的語言。
用 JavaScript 地雷的例子讓你了解到 Golang 為什麼這樣設計
、此設計的精神為何
,使學習 Golang 更加得順暢。
JavaScript 之父 Brendan Eich
JavaScript 是一個 Object Oriented Programming(OOP) 與 Functional Programming(FP) 混合的 Hybrid 語言,目標就是簡單、易用、彈性
網頁只能瀏覽的時代
Brendan Eich 借鑒了Scheme這門 FP 語言可傳遞 function 的First Class
概念來設計了 JavaScript,可輕鬆傳遞處理 function 使code變得更簡單更短
document.querySelector('input').addEventListener('input', e => {
alert(e.target.value)
})
Java 大紅大紫的年代,網景希望JavaScript要有些像Java
,但 Java class 又太複雜,
public class MainActivity extends ActionBarActivity implements View.OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
public void onClick(View v) {
Toast.makeText(this, "clicked", Toast.LENGTH_SHORT).show();
}
}
所以 Brendan Eich 借鑑Self這門沒有 Class 的原型 OOP 語言,設計出有以下幾種特性的 JavaScript OOP:
原型鏈
指向原本的屬性result.toJSON()
最後被送出時到底為什麼爆炸了?
const controller = function (req, res, next) => {
try {
result = a(req)
result = b(result)
result = c(result)
result = d(result)
result = e(result)
result = f(result)
result = g(result)
result = h(result)
result = i(result)
res.json(result.toJSON())
} catch (err) {
res.status(500).send('Get error')
}
}
原來…e function
裡的一行 code 改了toJSON()
的行為
動態增減屬性在雜亂的 code 中是有害的
res.json()
就是要吃object,其他string或int都不要
在雜亂的 code 中無法規範input/output的介面是難過的
function callAPI(caller) {
caller.get("https://api");
}
const caller = {
header: 'header',
get(URL) {
console.log(`${URL} with ${this.header}`)
}
}
callAPI(caller)
// 印出 "https://api with header"
function callAPI(caller) {
caller.get("https://api");
}
const caller = {
header: 'header',
get(URL) {
console.log(`${URL} with ${this.header}`)
}
}
caller.header = 123
caller.get = undefined
callAPI(caller)
而如果是 Golang,他會怎麼做呢?
package main
import "fmt"
func callAPI(caller Caller) {
caller.Get("https://api")
}
type Caller struct {
header string
}
func (c Caller) Get(URL string) {
fmt.Println(URL + " with " + c.header)
}
func main() {
callAPI(Caller{header: "header"})
}
嗯?怎麼好像新增的 code 不多
Golang struct 就像 JavaScript 被規範成員的 object,
而 struct 中的 function 則是用 Golang 的receiver function
來實作,並且無法動態修改。
多型是純 JavaScript 開發者較少聽到的詞
沒有runtime
,沒有傷害
callAPI(caller)
callAPI(callerB)
callAPI(callerC)
但 Golang 呢?
限制了callAPI()
只能傳入Caller struct
,這時就無法傳入CallerB struct
、CallerC struct
,
package main
import "fmt"
func callAPI(caller Caller) {
caller.Get("https://api")
}
type Caller struct {
header string
}
func (c Caller) Get(URL string) {
fmt.Println(URL + " with " + c.header)
}
func main() {
callAPI(Caller{header: "header"})
}
把 struct 所擁有的共同 function 定義出來,只要傳入的 struct 有此 function 就可以傳入,
package main
import "fmt"
type GetHandler interface {
Get(string)
}
func callAPI(getHandler GetHandler) {
getHandler.Get("https://api")
}
type Caller struct {
header string
}
func (c Caller) Get(URL string) {
fmt.Println(URL + " with " + c.header)
}
type CallerB struct {
header string
}
func (c CallerB) Get(URL string) {
fmt.Println(URL + " with " + c.header)
}
func main() {
callAPI(Caller{header: "header"})
callAPI(CallerB{header: "header"})
}
Golang 跟 JavaScript 一樣都在追求
簡單、易用、彈性
First Class
的特性interface Caller {
public void call();
}
interface OtherCaller {
public void call();
}
class ACaller implements Caller {
public ACaller() {
}
public void call() {
System.out.printf("Call API A");
}
}
class BCaller implements Caller {
public BCaller() {
}
public void call() {
System.out.printf("Call API B");
}
}
class CCaller implements OtherCaller {
public CCaller() {
}
public void call() {
System.out.printf("Call API C");
}
}
public class Main {
public static void main(String[] args) {
doRequest(new ACaller());
doRequest(new BCaller());
// 爆炸!雖然行為相同的介面不同!
doRequest(new CCaller());
}
public static void doRequest(Caller caller) {
caller.call();
}
}
在 Uncle Bob 的 Clean Architecture 一書中,
他認為 OOP 中的封裝
、繼承
、多型
中,
多型
是最具代表性與實戰效果,
封裝
、繼承
事實上不限定於在 OOP 語言出現前就可以做到。
Java class 在實現上述三個特性很方便,
但因為 class 嚴謹的規範造成如上功能實現的麻煩,
而 Golang 將多型
設定為整體 OOP 重點考量之一,
而不侷限在利用 class封裝
、繼承
的思維中。
如果將整個系統呼叫每層的 interface 定義出來,並將每層以此 interface 注入至下一層,那系統將不再被底層綁架
將
GetHandler
的 interface 定義出來,並將每個 Caller 以此 interface 注入至callAPI()
,只不過在系統架構上我們習慣把這些 code 稱為層
。
package main
import "fmt"
type GetHandler interface {
Get(string)
}
func callAPI(getHandler GetHandler) {
getHandler.Get("https://api")
}
type Caller struct {
header string
}
func (c Caller) Get(URL string) {
fmt.Println(URL + " with " + c.header)
}
type CallerB struct {
header string
}
func (c CallerB) Get(URL string) {
fmt.Println(URL + " with " + c.header)
}
func main() {
callAPI(Caller{header: "header"})
callAPI(CallerB{header: "header"})
}
凡從外部進來的事物,都可以放在
Repository
層
業務邏輯放置 Usecase 層。而 Controller/Delivery 只負責交代如何把這些 Usecase 帶給 View 層。
要更換任何 View 層只需要重寫 Delivery 層,業務邏輯的 Usecase 是不需要改的
某次要從 Restful API 更換成 Websocket 介面時,我直接
一切依賴於domain的高獨立性的架構
package domain
import "context"
// Digimon ...
type Digimon struct {
ID string `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
}
// DigimonRepository ...
type DigimonRepository interface {
GetByID(ctx context.Context, id string) (*Digimon, error)
Store(ctx context.Context, d *Digimon) error
UpdateStatus(ctx context.Context, d *Digimon) error
}
// DigimonUsecase ..
type DigimonUsecase interface {
GetByID(ctx context.Context, id string) (*Digimon, error)
Store(ctx context.Context, d *Digimon) error
UpdateStatus(ctx context.Context, d *Digimon) error
}
// ... 其他程式碼
type postgresqlDigimonRepository struct {
db *sql.DB
}
// NewpostgresqlDigimonRepository ...
func NewpostgresqlDigimonRepository(db *sql.DB) domain.DigimonRepository {
return &postgresqlDigimonRepository{db}
}
func (p *postgresqlDigimonRepository) GetByID(ctx context.Context, id string) (*domain.Digimon, error) {
row := p.db.QueryRow("SELECT id, name, status FROM digimons WHERE id =$1", id)
d := &domain.Digimon{}
if err := row.Scan(&d.ID, &d.Name, &d.Status); err != nil {
logrus.Error(err)
return nil, err
}
return d, nil
}
// ... 其他程式碼
// ... 其他程式碼
type digimonUsecase struct {
digimonRepo domain.DigimonRepository
}
// NewDigimonUsecase ...
func NewDigimonUsecase(digimonRepo domain.DigimonRepository) domain.DigimonUsecase {
return &digimonUsecase{
digimonRepo: digimonRepo,
}
}
func (du *digimonUsecase) GetByID(ctx context.Context, id string) (*domain.Digimon, error) {
aDigimon, err := du.digimonRepo.GetByID(ctx, id)
if err != nil {
logrus.Error(err)
return nil, err
}
return aDigimon, nil
}
// ... 其他程式碼
Clean Architecture 的分層重點:
// ... 其他程式碼
// DigimonHandler ...
type DigimonHandler struct {
DigimonUsecase domain.DigimonUsecase
DietUsecase domain.DietUsecase
}
// NewDigimonHandler ...
func NewDigimonHandler(e *gin.Engine, digimonUsecase domain.DigimonUsecase, dietUsecase domain.DietUsecase) {
handler := &DigimonHandler{
DigimonUsecase: digimonUsecase,
DietUsecase: dietUsecase,
}
e.GET("/api/v1/digimons/:digimonID", handler.GetDigimonByDigimonID)
e.POST("/api/v1/digimons", handler.PostToCreateDigimon)
e.POST("/api/v1/digimons/:digimonID/foster", handler.PostToFosterDigimon)
}
// PostToCreateDigimon ...
func (d *DigimonHandler) PostToCreateDigimon(c *gin.Context) {
var body swagger.DigimonInfoRequest
if err := c.BindJSON(&body); err != nil {
logrus.Error(err)
c.JSON(500, &swagger.ModelError{
Code: 3000,
Message: "Internal error. Parsing failed",
})
return
}
aDigimon := domain.Digimon{
Name: body.Name,
}
if err := d.DigimonUsecase.Store(c, &aDigimon); err != nil {
logrus.Error(err)
c.JSON(500, &swagger.ModelError{
Code: 3000,
Message: "Internal error. Store failed",
})
return
}
c.JSON(200, swagger.DigimonInfo{
Id: aDigimon.ID,
Name: aDigimon.Name,
Status: aDigimon.Status,
})
}
// ... 其他程式碼
main.go
跑起來吧!// ... 其他程式碼
func main() {
logrus.Info("HTTP server started")
restfulHost := viper.GetString("RESTFUL_HOST")
restfulPort := viper.GetString("RESTFUL_PORT")
dbHost := viper.GetString("DB_HOST")
dbDatabase := viper.GetString("DB_DATABASE")
dbUser := viper.GetString("DB_USER")
dbPassword := viper.GetString("DB_PASSWORD")
db, err := sql.Open(
"postgres",
fmt.Sprintf("host=%s user=%s password=%s dbname=%s sslmode=disable", dbHost, dbUser, dbPassword, dbDatabase),
)
if err != nil {
logrus.Fatal(err)
}
if err = db.Ping(); err != nil {
logrus.Fatal(err)
}
r := gin.Default()
digimonRepo := _digmonRepo.NewpostgresqlDigimonRepository(db)
dietRepo := _dietRepo.NewPostgresqlDietRepository(db)
digimonUsecase := _digimonUsecase.NewDigimonUsecase(digimonRepo)
dietUsecase := _dietUsecase.NewDietUsecase(dietRepo)
_digimonHandlerHttpDelivery.NewDigimonHandler(r, digimonUsecase, dietUsecase)
logrus.Fatal(r.Run(restfulHost + ":" + restfulPort))
}
// ... 其他程式碼