顯示具有 設計方式 標籤的文章。 顯示所有文章
顯示具有 設計方式 標籤的文章。 顯示所有文章

2018年1月1日 星期一

Refactor Redux-go

如標題所示,這次我要談論的是redux-go重構
在幾次的測試撰寫過程中,我發現舊版的redux-go有一些小問題
首先是Action物件建構不易,且無關的資訊不斷出現
何謂建構不易?我們必須用
redux.SendAction("type")
這樣彆扭的寫法來建構只有typeAction
然而需要Args時,卻要使用
&redux.Action{
    Type: "type",
    Args: map[string]interface{} {
        // ...
    },
}
這樣複雜的寫法,直接使用struct非常容易出錯,如果客戶端直接插值給Args,結果只會是panic
讓介面容易被使用,不容易被誤用
而且redux這個模組資訊完全是非必要的,所以我在v0.5.0將整個Action部份移到redux/action模組
這樣一來程式就變成了
import "github.com/dannypsnl/redux/action"

action.New("type")
非必要的資訊已被移除,現在我們可以專注於action
下一步是讓新增Args的介面容易使用,所以我在v0.5.1新增func (*Action) Arg(string, interface{})方法進入action模組
於是新增Args變成
action.New("type").
    Arg("key", value).
    Arg("key2", value)
這樣的流暢呼叫式

第二個部份是Store,大抵和Action問題一樣,所以亦將其移入store模組
所以客戶端程式就變成了
import "github.com/dannypsnl/redux/store"
import "github.com/dannypsnl/redux/action"

func main() {
    store := store.New(reducer...)
    store.Dispatch(
        action.New("type").
            Arg("key", value).
            Arg("key2", value))
}
注意省略概念外的程式

第三是DispatchC方法的部份(屬於Store)
原本我發現在效能較高的電腦上,序列式的計算subscribed function比共時化的程式更快
於是分成兩個API
但是經過檢討,我認為這只會造成困惑,且未來多核心能力上升,Go亦可能優化其排程器使實作更加高效,比起讓客戶端負擔測試程式效能的成本,直接採取共時版本的實作更加適合,因此採取這個作法(v0.6.0)

經過這次重構,我重新認識了Go的模組組織概念,還有誰來負擔決策成本的問題,是非常有趣的體驗

2017年12月23日 星期六

Type driven development -- by C++

Let's start from some code. And seems will be only code in this article.
// Compile: clang++ main.cc
#include <iostream>

template <int x, int y> class Matrix {
  // We don't care how to implement at here
public:
  std::string print() { return std::string("Matrix"); }
};

template <int x, int y> Matrix<x, y> Add(Matrix<x, y> a, Matrix<x, y> b) {
  return a; // Just help compile can run it.
}

int main() {
  std::cout << Add(Matrix<2, 2>(), Matrix<2, 2>()).print() << std::endl;
  // This line never pass, interesting part.
  std::cout << Add(Matrix<3, 2>(), Matrix<2, 3>()).print() << std::endl;
}
Ok, some code be there, why I want to talk about these code?
Few weeks ago, I study Idris and it's core concept: Type-Driven-Development.
But what is TDD(T is Not test at here)?

Matrix can show this concept clearly. Because we need some meta to make sure we are adding correctness Matrix together.
We don't want something like [0 0] + [1 0 3] can work, because Matrix can't be that.
So what will we do at first? Every programmer will check it(I thought, hope I am correct). And most of them will check it at: runtime. But runtime checking is danger. If I could, I always trying compile-time checking, because the chance that can be find out by editor is very big, almost 100%. But how to do that?

In C++, template help we checking at compile-time.
And almost no other language can template integer as template parameter. In Java, we have generic only. And a lot language only have generic too.
But maybe some people can't understand idris, so let's use C++.

The point is: when we need Matrix add. Only those Matrix with correct X, Y can add together.
With template check, second Add always can't pass compile.
Hope you already got the point of TDD.
That is  define type for certain usage, you can get the help from Type System.
It can limit error into a narrow part.
Thanks for read.

2017年12月3日 星期日

Redux in Go

就在昨天我完成了Go版本redux的主要功能
發文要附圖(誤),是連結: redux
首先redux是什麼呢?redux的起源與react有關,facebook推出一種叫flux的資料流架構,而如今redux憑借著簡單優雅的架構成為了主流的實作,它受到elm這隻程式語言的啟發而對flux做出修正
那麼flux是什麼呢?或者說,它的重點是什麼?flux試圖解決JavaScript程式長久以來面臨的問題:究竟是誰改變了狀態?
事實上這不只是JavaScript會面臨的問題,是所有具有多個改變狀態的原因的程式需要面對的問題
追蹤狀態的改變是如此的複雜,flux點出的關鍵在於我們不知道狀態存放的地點以及誰可以接觸到它,既然了解了問題點所在,我們就能問題提出解法
好吧!到了這裡,相信大家都已經了解flux存在的意義,那麼它面臨了什麼樣的新問題呢?
問題在於實作,flux由action dispatcher store view組成(但是view通常並非其實作所關注的,而是一種抽象的概念,指涉使用它的展示層)
store儲存資料,dispatcher分發事件,view觀察store中的資料對自己做出更新
使用者觸發action,dispatch就會觸發已註冊的那些callback,最終達到update store的效果
實際操作起來的問題是dispatcher會變得非常多,每次應用就會出現類似的程式
而這顯然與程式工程師們習慣不符,我們就是『懶』
那麼如何解決這樣的問題呢?redux參考函數式語言的一些概念,提出了
reducer(previousState, action) => newState
這樣的等式,更有趣的是你不能提供一個store初始狀態給redux,而是要給予一個個reducer組成store,每個reducer各自擁有initial state,store的初始狀態變由此決定
除此之外,redux也沒有dispatcher,它只有dispatch函數,所以我們不用管理事件會分發給誰,因為所有reducer都會收到
接著是subscribe函數,這個函數讓呼叫方的函數可以在dispatch時自動被執行,唯一的限制是裡面不能再度呼叫dispatch,這會無限遞迴
好了,最後我們進入正題,這個Go版的redux究竟是怎麼實作的呢?
首先是NewStore函數,它是我們這個版本的createStore,命名是依照Go的慣例
其回傳一個Store指標(我們當然不想要複製整個Store,其成本難以想像,JS的物件則是本來就不會複製(預設))
接受reducer型別作為參數
type reducer func(interface{}, Action) interface{}
reducer定義非常簡單,就是我們前面看到的reducer(previousState, action) => newState的樣子
Store定義如下
type Store struct {
        GetState map[string]interface{}
        reducers []reducer
        subscribes []func()
        atSubscribe bool
        mu sync.Mutex
}
reducers, subscribes無須解釋,GetState即是我們所有state存放的位置,這樣取名是為了呼叫時的清晰度
(其實想改掉了,這樣會被客戶端修改,其實失去了保證性)
mu在共時程式之中保證Dispatch會安全完成
atSubscribe則是保證Subscribe中不得呼叫Dispatch
func NewStore(r reducer, reducers ...reducer) *Store {
        s := &Store{
                GetState:    make(map[string]interface{}),
                atSubscribe: false,
        }
        s.newReducer(r)
        for _, r := range reducers {
                s.newReducer(r)
        }
        return s
}
將參數分成兩部份是因為這樣就不需要自己檢查參數(...散列可以為空之特性),而是由編譯器做出保證
除去上述的特殊設計,非常容易看出程式的邏輯,我們把一個個reducer綁上我們可愛的store
到這裡我們先看看實際案例
import "github.com/dannypsnl/redux"

func counter(state interface{}, action redux.Action) interface{} {
        // Initial State
        if state == nil {
                return 0
        }
        switch action.Type {
        case "INC":
                return state.(int)+1
        case "DEC":
                return state.(int)-1
        default:
                return state
        }
}

func main() {
        store := redux.NewStore(counter)
        store.Subscribe(func() {
                fmt.Printf("Now state is %v\n", store.GetState["counter"])
        })
        store.Dispatch(redux.SendAction("INC"))
}
我們居然可以GetState["counter"]!?
這就是用在newReducer中的魔法了
func (s *Store) newReducer(r reducer) {
        s.GetState[getReducerName(r)] = r(nil, Action{})
        s.reducers = append(s.reducers, r)
}
應該很容易能看出,我們找出reducer的參考名稱,並用此名稱作為鍵值,用nil調用reducer(Action在這裡不重要,想一下Go版reducer的定義中必須在nil時回傳initial state)
然後將reducer放入我們的大殺器reducers中(!?)
getReducerName實作如下
func getReducerName(r reducer) string {
        fullName := runtime.FuncForPC(reflect.ValueOf(r).Pointer()).Name()
        return fullName[strings.LastIndexByte(fullName, '.')+1:]
}
我們先用runtime API取得指標指向的函數,再存取其名稱,這時我們會得到完整的名稱(套件.參考名稱)
所以去除套件部份之後就是我們想要的部份了
做成helper函數的原因是之後更新state時也需要這個函數,DRY,很好

接著我們看Dispatch的實作
func (s *Store) Dispatch(act *Action) {
    s.mu.Lock()
    if s.atSubscribe {
        panic(`you're trying to invoke Dispatch inside the subscribed function`)
    }
    for _, r := range s.reducers {
        funcName := getReducerName(r)
        s.GetState[funcName] = r(s.GetState[funcName], *act)
    }
    // we call subscribed function after state updated.
    s.atSubscribe = true
    for _, subscribtor := range s.subscribes {
        subscribtor()
    }
    s.atSubscribe = false
    s.mu.Unlock()
}
可以看到if atSubscribe程式就會崩潰,這樣能夠阻止想做蠢事的正常呼叫(但是你阻止不了硬要用recover卻不處理這個問題的人)(目前這部份有bug,事實上我們會先遇上deadlock而不是panic,雖然結果正確但是失去錯誤訊息的提示)
我們對操作上鎖
所以程式能夠安全的在共時程式中執行,而狀態更新上鎖也算是正常的設計
然後我們執行那些subscribe進來的函數,注意註冊的函數不能有參數,因為我也不知道要傳什麼參數給你,那很不合理對吧
而最後Subscribe的實作索然無味,就只是將我們可愛的函式們放入註冊函數集合中
func (s *Store) Subscribe(subscribetor func()) {
    s.subscribes = append(s.subscribes, subscribetor)
}

最後總結是為了Go的一些特性我們得要做出取捨,例如原版中,只有一個reducer的情況只需要使用getState就能得到狀態,但是我統一使用函數名稱做存取,因為Go不允許多載函數,而動態參數列亦不太適合(我們得在參數數量超過1時panic,0時檢查reducer的數量,都很麻煩)

另外state型別為interface{}的部份,這並不會造成任何問題,因為state只在reducer中被使用,因此強制轉型並不會造成問題

謝謝觀看,歡迎提出改進意見

2017年4月21日 星期五

ATM 06

重構程式碼,就是找出不良的寫法並不斷改進的行為,每一步如果都有測試把關,就能更安全的進行下一步,而不用自己找出問題點。當然,這一切的前提是測試必須簡單到只有一個可能的影響點,這樣才能快速的找出出問題的部分

我們要進行的第一個重構是對控制域的重構
>>  當屬性應當被保護時(不被外界存取),我們就應該給予私有等級的權限
將Account的ID password都改為private,在將balance也改為private的時候,我們遇上了問題,我們不能直接取用Account實體的balance了,為了讓操作最小化,所以我們給予一個getter,就像這樣
public double balance() {
    return balance;
}
所以我們就能像下面這樣存取
double balance = ac.balance();

接著執行測試,失敗!!!
因為ID還有password都有同樣的問題,所以都給予getter方法

現在測試通過,運行正常,所以我們要找尋下一個部分進行改善
在程式設計中,有一個原則叫Don't repeat yourself,意思是不要寫雖有不同卻類似的程式碼
在Account類別身上,我們找到了這個問題,其建構式如下
Account(String ID, String password) {
    this.ID = ID;
    this.password = password;
    this.balance = 0;
}
Account(String ID, String password, double balance) {
    this.ID = ID;
    this.password = password;
    this.balance = balance;
}
唯一的差別是,當沒有指定存款金額時,將其值設為0,那麼事實上我們可以這樣做
Account(String ID, String password) {
    this(ID, password, 0);
}
其中一個建構式委託另一個建構式來處理即可(並且附上註解,說明原由)
ps. 有些語言對此問題的解法是讓你可以指定預設值,實現上更優雅簡單

因為Account目前是獨立的,所以將它移到專屬檔案存放
而它的測試也不再需要透過User實例測試它,所以讓我們改掉這個情形
public void depositTest() {
    Account ac = new Account("1", "1");
    ac.userDeposit(10000);

    Assert.assertEquals(10000, ac.balance(), 0.0000001);
}
這是其中一個,其他的測試函式也照做就行

那麼,我們謹遵DRY原則,所以我把ac變成了一個靜態(static)物件,好讓下面的程式不必重複宣告
將各個測試函數中的ac移除之後,很不幸的是只有第一個測試程式成功了,道理很簡單,因為現在ac的副作用隨著共用同一個實例而被展現出來
我們改一改之後,程式可以運作了,然而它卻依賴著測試程式的執行順序!!!
一旦我們試圖個別執行測試,它們就不能正常運作
所以我們只好依靠JUnit提供的功能處理這件事
private Account ac;
@Before
public void setup() {
    ac = new Account("1", "1", 50000);
}

@After
public void next() {
    ac = null;
}
非常直觀的是,Before在每個測試執行前被執行,After恰恰相反
現在測試程式既能夠獨立執行也能一次執行全部

突然,有使用者抱怨------他明明還有50000元,為何提領50000元卻得到餘額不足的結果呢?
我們深入追查才發現,原來我們出了個烏龍
if (amountOfWithdraw < balance)
這裡應該是等於時也可以,所以我們馬上修正它,並在測試案例中加上了這個特殊情形
@Test
public void withdrawSomeTest() {
    double excepted = 0;
    try {
        ac.userWithdraw(50000);
    } catch (BalanceException e) {
        excepted = 50000;
    } finally {
        Assert.assertEquals(excepted, ac.balance(), 0.0000001);
    }

}
這下問題解決了

再來我把BalanceException換成了比較貼切的OutOfMoneyException
Account.XXX的amountOfXXX都改成amount(由於他們所處的位置變數不多,採簡單的命名就行了)
接下來都是名稱變換,並不重要

真正的問題在,從使用案例中,我認為User作為實例的意義已經不存在了,它現在只是純粹的使用者介面部分,那麼我們就開始著手改變其架構吧!
public class DataBase {
    List<Account> Accounts = new ArrayList<>();

    DataBase() {
        Accounts.add(new Account("1", "1", 50000));
    }

    ResultAccount find(String userID, String userPassword) {
        ResultAccount answer = new ResultAccount();
        for (Account ac : Accounts) {
            if (ac.ID().equals(userID) && ac.password().equals(userPassword)) {
                answer.location = Accounts.indexOf(ac);
                answer.found = true;
                return answer;
            }
        }
        answer.found = false;
        return answer;
    }
}
先是DataBase改為存放帳戶訊息,接著將測試中的存取方式全部改掉,這時候,測試的威力就出現了,這次沒有測試失敗,因此我們可以相信這次改動應該是完全正確的!
我們不必做猜測
接著另Accounts成為private,因為我們可不希望帳戶資料可以在外面被隨意修改,對吧!
更重要的是,我們可以實現檢查邏輯,避免重複的ID存在,下面是我們提供的新介面方法,以因應適才所言的部分
public void add(Account ac) {
    Accounts.add(ac);
}
這個方法還沒辦法檢查是否重複增加成員,所以我們改成下面那樣
public void add(Account ac) {
    if(find(ac.ID())) {
        return;
    }
    Accounts.add(ac);
}

boolean find(String ID) {
    for (Account ac : Accounts) {
        if (ac.ID().equals(ID)) {
            return true;
        }
    }
    return false;
}
現在我們可以看出是否已經有這個ID了,重複的帳號將不會被加入

我們依樣畫葫蘆的試圖重現get
public Account get(int index) {
    return Accounts.get(index);
}
然而這次卻失敗了,我們只得到一個NullPointerException,為什麼呢?就留給你自己去探查

更重要的是,無論對這段程式如何修改都沒有意義,因為架構已全然不同,我們的User已經失去跟Account的直接關聯
而且在這裡,我們要調用的是User實例,所以應該把get移除,並且改成
for (Method m : user.getClass().getDeclaredMethods()) {
    IfChoose c = m.getAnnotation(IfChoose.class);
    if (choose == c.value()) {
        double balance = (double) m.invoke(user);

        System.out.println("剩餘 " + balance + "元");

        break;
    }
}
這樣調用,user宣告成Main的static物件

然而,現在的User身上不應該出現Account實體,所以我們把它移除,既然已經沒有field了,那麼採用預設建構式就可以了
原本的find(String, String)複雜無比,讓我們採用更簡單的方式
int find(String userID, String userPassword) {
    int i = -1;
    for (Account ac : Accounts) {
        if (ac.ID().equals(userID) && ac.password().equals(userPassword)) {
            i = Accounts.indexOf(ac);
            return i;
        }
    }
    return i;
}
而使用方式自然要跟進
int index = db.find(userID, userPassword);
if (index != -1) {
    // ...
}
可見我當時做了過度設計,為不存在的需求設計了多餘的類別
而測試在修改之後也成功通過了

我們根據新架構提出了新的方式
double depositFrom(int i, int depositAmount) {
    return Accounts.get(i).userDeposit(depositAmount);
}

double withdrawFrom(int i, int depositAmount) throws OutOfMoneyException {
    return Accounts.get(i).userWithdraw(depositAmount);
}

double getBalanceFrom(int i) {
    return Accounts.get(i).balance();
}
因為在find的時候,我們知道了究竟是在使用哪一個帳號,但是我們沒辦法透過User實例直接調用了
@IfChoose(value = 1)
double deposit(int index) {
    System.out.println("請輸入存款金額:");
    int depositAmount = Main.sc.nextInt();

    return Main.db.depositFrom(index, depositAmount);
}
於是我們只能將User.deposit()改成這樣,不過利用反射調用deposit的部分,得加上這個新參數
double balance = (double) m.invoke(user, index);
其他部分也如此改造就行了

在成功運作之後我們要想怎麼改進,顯然,我們這樣搞不但重複多,現在登入哪一個帳戶我們都不大清楚,而傳遞的風險還有可能被不知情的程式員改動這個值,雖然我們可以用final來確保值不被改變,然而我們並不能確保這個放置在外的程式不會被改變,因此最保險的方式是封裝它
boolean find(String userID, String userPassword) {
    for (Account ac : Accounts) {
        if (ac.ID().equals(userID) && ac.password().equals(userPassword)) {
            now = Accounts.indexOf(ac);
            return true;
        }
    }
    return false;
}

double depositFrom(int depositAmount) {
    int i = now;
    now = -1;
    return Accounts.get(i).userDeposit(depositAmount);
}

double withdrawFrom(int depositAmount) throws OutOfMoneyException {
    int i = now;
    now = -1;
    return Accounts.get(i).userWithdraw(depositAmount);
}

double getBalanceFrom() {
    int i = now;
    now = -1;
    return Accounts.get(i).balance();
}
將他們改成這樣,測試對應上,測試通過
接著改變調用的方式,然後User的兩個方法接收index的意義已不存在,所以移除

接著增加測試
@Test
public void getBalance() {
    boolean exist = db.find("1", "1");
    double balance = db.getBalanceFrom();

    Assert.assertEquals(50000, balance, 0.0000001);
}
這讓我們知道now是不是真的指向我們預期的那個帳戶

其他的方法也一樣加上測試

接著執行main,看起來跟我們想的一樣

這篇就先在這裡結束
下一篇我們新增一些功能,See you next time

2017年4月20日 星期四

ATM 05

完整的ATM非常獨特,事實上,它需要能夠不斷運行
再者我們一直沒有對使用者回應操作完畢的狀態,因此我們將盡速模擬完成
while (true) {
    System.out.println("請輸入帳號");
    String userID = sc.nextLine();
    System.out.println("請輸入密碼");
    String userPassword = sc.nextLine();

    ResultAccount account = DataBase.find(userID, userPassword);
    if (account.found) {
        // ...
    } else {
        System.out.println("帳號不存在或是密碼輸入錯誤");
    }

}
為了讓ATM能不斷運行而加上了無窮迴圈
再者我們必須在找不到帳戶時讓使用者明白他們遇上了什麼問題
再來讓DataBase實例化
static DataBase db = new DataBase();
當然,所有存取方式都得改變
double balance = (double) m.invoke(db.Users.get(account.location));
System.out.println("剩餘 " + balance + "元");
接著我們讓這些方法回傳現在剩餘多少錢,然後告知使用者

再來因為Account實在沒有存在User中的必要,所以我暫時將它移出

接著我們發現一個問題,我們把流程混入了應當無副作用的Account Method: userWithdraw中
所以我們把流程移到高一層次的User.withdraw之中,定義如下
double userWithdraw(int amountOfWithdraw) throws BalanceException {
    if (amountOfWithdraw < balance) {
        balance -= amountOfWithdraw;
        log.AddWithdrawLog(amountOfWithdraw);
        return balance;
    } else {
        throw new BalanceException();
    }
}
使用則是
try {
    return ac.userWithdraw(amountOfWithdraw);
} catch (BalanceException e) {
    System.out.println("操作失敗,餘額不足");
} finally {
    return -1;
}
很不幸的,由於語言的限制,我們必須回傳一個值
然後我們舒爽的按下執行測試,很不錯,又出錯了呢...
為什麼呢?它說我們必須為拋出例外的程式加上try區塊,否則我們得拋出例外,我們得處理這個事實
public void withdrawTest() {
    User u = new User("", "", 50000);
    try {
        u.ac.userWithdraw(10000);
    } catch (BalanceException e) {
        // 我們不需要在這處理錯誤
    } finally {
        Assert.assertEquals(40000, u.ac.balance, 0.0000001);
    }

}
變成這樣了
而且我們還要處理一個事實,就是在測試中要測試提領失敗的情況(但不是在上面的測試)
public void withdrawFailTest() {
    User u = new User("", "", 50000);
    double excepted = -10000;
    try {
        u.ac.userWithdraw(60000);
    } catch (BalanceException e) {
        excepted = 50000;
    } finally {
        Assert.assertEquals(excepted, u.ac.balance, 0.0000001);
    }

}
我們得說清楚,這個測試是指提款失敗時

接著,我們實際執行了一下,嗚!我提50000元之後,它說
操作失敗,餘額不足
剩餘 -1.0元
...好像不大對呢(廢話,一整個都不對啊)
double balance = ac.balance;
        
try {
    balance = ac.userWithdraw(amountOfWithdraw);
    return balance;
} catch (BalanceException e) {
    System.out.println("操作失敗,餘額不足");
} finally {
    return balance;
}
因此我們得寫成這樣,多了一個區域變數

至此,整體邏輯看起來都還行了

下一篇就來整理程式碼吧