improve bitter code

想法

周五的时候去了趟图书馆,回来的时候已经2点了,又是组织参加每周的代码评审了。 这项活动已经参加了很多次了,但还是经常会遇到相同的问题,即使每天codediff, 也很难避免问题一次次的重复出现。代码基还是不可避免的膨胀,需求变化还是那么的频繁, 人员流动还是那么快,新人还是有那么多不成熟的编码风格。

在这个开发团队里边,也没少唠叨编码规范,codediff也是经常再做,代码评审还是有弄, 但最终的效果,体现在代码上,还远远没有那么乐观。我认为这有好些原因:一方面是历史原因, 代码基庞大,复杂,每天对着这些代码,连好代码是怎样的都没有见过,还能真的无师自通? 另外一方面是各种质量改进活动参与度都不是很高,毕竟目标是完成需求,时间久了热情一点点磨灭, 对这个就更不重视了。

相信很多人看过郑大的代码之丑,里边很多素材就出自我所在的项目组,所以这种代码我也看见不少了, 正因为如此,我觉得我也能写一些相关的内容。在下班坐地铁回来的时候, 我列了一些相关主题,更确信了还是有很多内容可以做文章的。大多数题材来自 项目的代码,自己的想法思路则是来源于平时的实践,工作经历,还有开源代码和各种相关的书籍。

计划

整个系列围绕bitter code来说,毕竟维护现有代码是非常重要的工作。这么个系列名称灵感来源于 代码之丑系列和一本叫bitter java的书籍。虽然我觉得代码之丑的名称不是很好,但内容形式会和 这个系列类似,只是会多加一些上下文和背景。文章内容仍然以代码为主线,写出我对代码的改进意见, 至于发布周期,大约是一个星期争取不少于两篇!!

期待吧!!!

"improve bitter code"系列文章:

hello erlang, part 4

练习重点

列表解析和高阶函数是erlang的基本用法之一,熟练掌握还是很有必要的。 作为一个阶段的学习总结,按照书上的一个练习题,尝试实现列表模块中的一些方法。

后续学习计划

再找时间总结一下位匹配用法,然后就开始客户端服务器编程模型, 网络和分布式方面的内容。

相关内容虽然都看过一下,但是没有敲过代码,感觉心里没底。还是那个硬道理:纸上得来终觉浅呀。

简单的测试用例

作为另外一个章节的内容,单元测试。我顺便实践一下erlang的单元测试框架eunit, 里边使用了宏来作为assert语句(使用eclipse插件可以有很好的提示), 总的来说,xunit的基本风格都差不多,注意import eunit的包,方法名采用_test结束。 执行的时候调用模块的test方法就可以了。我简单写了一些测试用例:

-module(myliststest).
-include_lib("eunit/include/eunit.hrl").
-import_all(mylists).

%% EUnit TestCase

%% all/2
all_empty_test() ->
?assert(mylists:all(fun(E) -> E > 10 end, [])).
all_test() ->
?assertNot(mylists:all(fun(E) -> E > 10 end, [11,12,10])).

%% any/2
any_empty_test() ->
?assertNot(mylists:any(fun(E) -> E > 10 end, [])).
any_test() ->
?assert(mylists:any(fun(E) -> E > 10 end, [11,12,10])).

%% append/1
append_1_test() ->
?assertEqual([1,2,3,a,b,4,5,6], mylists:append([[1, 2, 3], [a, b], [4, 5, 6]])).

%% append/2
append_2_test() ->
?assertEqual("abcdef", mylists:append("abc", "def")).

%% delete/2
delete_test() ->
?assertEqual([1,2,4], mylists:delete(3, [1,2,3,4])).

%% dropwhile/2
dropwhile_test() ->
?assertEqual([1,2], mylists:dropwhile(fun(E) -> E >= 3 end, [1,2,3,4])).

%% duplicate/2
duplicate_test() ->
?assertEqual([x, x], mylists:duplicate(2, x)).

%% filter/2
filter_test() ->
?assertEqual([3,4], mylists:filter(fun(E) -> E >= 3 end, [1,2,3,4])).

%% reverse/1
reverse_test() ->
?assertEqual([4,3,2,1], mylists:reverse([1,2,3,4])).

%% min/1
min_test() ->
?assertEqual(2, mylists:min([3,4,2,5])).

%% zip/2
zip_2_test() ->
?assertEqual([{1,3},{2,4}], mylists:zip([1,2], [3,4,5])).

%% zip/3
zip_3_test() ->
?assertEqual([{1,3,5},{2,4,7}], mylists:zip3([1,2], [3,4,5], [5,7])).

%% zipwith/3
zipwith_test() ->
?assertEqual([5,7,9], mylists:zipwith(fun(X, Y) -> X+Y end, [1,2,3], [4,5,6])).

%% zipwith/4
zipwith3_test() ->
?assertEqual([[a,x,1],[b,y,2],[c,z,3]], mylists:zipwith3(fun(X, Y, Z) -> [X,Y,Z] end, [a,b,c], [x,y,z], [1,2,3])).

示例代码

根据上面的测试用例,我写了实现代码,如下所示。注意的时候,我偷懒使用了export_all, 但是min/2好像默认有一个已经被引入了,所以需要去除这个模块。就export_all本身来说, 也不是推荐方式,还是应该严格区分public api和local api。

-module(mylists).
-compile(export_all).
-compile({no_auto_import,[min/2]}).

%% all/2
all(_P, []) ->
true;
all(P, [H|T]) ->
case P(H) of
true -> all(P, T);
false -> false
end.

%% any/2
any(_P, []) ->
false;
any(P, [H|T]) ->
case P(H) of
true -> true;
false -> any(P, T)
end.

%% append/1
append(LL) ->
[X || L <- LL, X <- L].

%% append/2
append(LA, LB) ->
LA ++ LB.

%% delete/2
delete(E, L) ->
[X || X <- L, E /= X].

%% dropwhile/2
dropwhile(P, L) ->
[X || X <- L, not P(X)].

%% duplicate/2
duplicate(N, E) ->
duplicate(N, E, []).
duplicate(0, _E, L) ->
L;
duplicate(N, E, L) when N > 0 ->
duplicate(N-1, E, [E|L]).

%% filter/2
filter(P, L) ->
[X || X <- L, P(X)].

%% reverse/1
reverse(L) ->
reverse(L, []).
reverse([], L) ->
L;
reverse([H|T], L) ->
reverse(T, [H|L]).

%% min/1
min([H|T]) ->
min(T, H).
min([], M) ->
M;
min([H|T], M) ->
if
H < M -> min(T, H);
true -> min(T, M)
end.

%% zip/2
zip(LA, LB) ->
reverse(zip(LA, LB, [])).
zip([], _LB, L) ->
L;
zip(_LA, [], L) ->
L;
zip([HA|TA], [HB|TB], L) ->
zip(TA, TB, [{HA,HB}|L]).

%% zip/3
zip3(LA, LB, LC) ->
reverse(zip3(LA, LB, LC, [])).
zip3([], _LB, _LC, L) ->
L;
zip3(_LA, [], _LC, L) ->
L;
zip3(_LA, _LB, [], L) ->
L;
zip3([HA|TA], [HB|TB], [HC|TC], L) ->
zip3(TA, TB, TC, [{HA,HB,HC}|L]).

%% zipwith/3
zipwith(P, LA, LB) ->
[P(X,Y) || {X,Y} <- zip(LA, LB)].

%% zipwith/4
zipwith3(P, LA, LB, LC) ->
[P(X,Y,Z) || {X,Y,Z} <- zip3(LA, LB, LC)].

hello erlang, part 3

练习重点

并发编程是erlang的重要话题,所以这次先来热身,熟悉一下erlang中进程的基本用法。

练习1:进程环

编写一个程序,它生成N个进程并相连形成一个环,一旦启动,这些进程会环绕发送M个消息。 然后当收到消息的时候正常终止。调用ring:start(M, N, Message)来启动环。

思路及编程示例

首先,为了形成环状,就是让最后一个进程能够给第一个进程发送消息, 调用spawn生成进程的时候,需要把第一个pid一直传递到最后一个。所以需要有个start的方法, 但需要多加个第一个进程id的参数,关注参数N=1作为临界点。

还有,需要等待消息,然后给下一个进程发送消息,之后继续等待消息,直到M=0。 所以需要一个等待消息的方法,同时需要关注参数M=0作为临界点。

最后,整个环,所有的进程一开始都在等待,需要有个消息来启动环, 可以在第一个进程启动下一个进程之前,往自身邮箱里边发送一条消息, 这样第一个进程进入等待消息的时候,就能接收到消息,从而启动环。

我的代码示例如下:

-module(ring).

%% Exported Functions
-export([start/3]).
-export([start/4]).

%% API Functions
start(M, N, Message) ->
self() ! {self(), start},
start(M, N, Message, self()).

start(M, 1, Message, Rid) ->
waitmessage(M, 1, Message, Rid);

start(M, N, Message, Rid) ->
Pid = spawn(ring, start, [M, N-1, Message, Rid]),
waitmessage(M, N, Message, Pid).

waitmessage(0, _N, _Message, _Pid) ->
receive
{Pd, Msg} ->
io:format("pid ~p receive message ~p from ~p~n", [self(), Msg, Pd])
ok
end;

waitmessage(M, N, Message, Pid) ->
receive
{Pd, Msg} ->
io:format("pid ~p receive message ~p from ~p~n", [self(), Msg, Pd]),
Pid ! {self(), Message},
waitmessage(M-1, N, Message, Pid)
end.

通过简单的测试,如果去掉io:format的话,效率会提高不少。IO操作消耗的时间还是不可小视呀。 另外,测试大数量进程的时候,内存也用得比较厉害,毕竟这个练习更像是一个同步操作。

hello erlang, part 2

练习重点

  1. 依然是熟悉erlang匹配模式的编程方式,对于用习惯了判断,循环操作的人来说,这个变化很大
  2. 熟悉erlang列表操作
  3. 为后面学习高阶函数fun,还有更多高级数据类型打好基础

练习1:压缩有序列表

类似搜索里边的倒排索引,需要对词对应的文档列表进行存放。 在这里,我们将一个有序列表转换成另外一种形式,如

%% 原始形式
[1,1,2,4,5,6,6,98,100,101,102,102].
%% 最终形式
[{1,2},{4,6},{98,98},{100,102}].

示例代码

multi(L) -> lists:reverse(multi_local(L, [])).
multi_local([], R) -> R;
multi_local([H|L], R) ->
{LL, E} = multi_r(L, H),
multi_local(LL, [{H, E}|R]).

%% 获取L里边里边和H同属一组的最后一个数,同时返回剩余的列表
%% 如果L以H开头,表示出现重复数
%% 如果L以H+1开头,表示出现序列数
multi_r(L, H) ->
N = H + 1,
case L of
[H|LL] -> multi_r(LL, H);
[N|LL] -> multi_r(LL, N);
[] -> {[], H};
_ -> {L, H}
end.

效率问题

这里和上次不一样的是,这里没有使用lists:append方法,而是在最后使用了reverse.

[1] ++ [2, 3].
lists:append([1], [2, 3]).
lists:reverse([3|[2, 1]]).

上面三种方式得到的结果是一样的。前面两种,每增加一个元素,都会遍历左边的列表,所以在递归里边处理的话, 效率并不好。把元素添加到表头,在递归完成后进行反转,效果就会好些。

练习2:马踏棋盘

作为一个阶段的学习总结,对马踏棋盘的算法思考用erlang实现。

代码比较悲剧,我选择了array,用了一些fun函数,从这么个例子就可以看出,自己还没掌握好erlang惯用法。 感觉数据结构没有选好,并且用了命令语言的一些编程思路,有些东西或许可以用列表解析来弄, 等再掌握多一些东西,再回头来体会一把!

-module(horse).

%% Exported Functions
-export([walkboard/0]).

%% API Functions
walkboard() ->
%% lists:foreach(fun(I) -> lists:foreach(fun(J) -> walkboard({I, J}) end, lists:seq(0,7)) end, lists:seq(0,7)).
walkboard({2,3}).

walkboard({I,J}) ->
{ARR, PATH, POSS} = init(),
catch walk(0, setvalue({I, J}, 1, ARR), array:set(0, {I,J}, PATH), POSS).

walk(63, _, PATH, _) -> throw(PATH);
walk(MARK, ARR, PATH, POSS) ->
{IP, JP} = array:get(MARK, PATH),
WC = fun({I,J,_}) ->
walk(MARK+1, setvalue({I+IP,J+JP}, 1, ARR), array:set(MARK+1, {I+IP,J+JP}, PATH), POSS)
end,
lists:foreach(WC, filter(MARK, PATH, POSS, ARR)).

%%
%% Local Functions
%%
init() ->
ARR = array:new(8, {default,array:new(8, {default,0})}),
PATH = array:new(64),
POSS = [{-2, 1}, {2, 1}, {1, 2}, {-1, 2}, {2, -1}, {-2, -1}, {-1, -2}, {1, -2}],
{ARR, PATH, POSS}.

filter(MARK, PATH, POSS, ARR) ->
{I, J} = array:get(MARK, PATH),
Fun1 = fun({IP, JP}) ->
{IXP, JXP} = {I+IP, J+JP},
case valid({IXP, JXP}, ARR) of
false -> {IP, JP, -1};
true ->
%% 计算空点数
CAL = fun({IP2, JP2}, Sum) ->
case valid({IXP+IP2, JXP+JP2}, ARR) of
true -> 1 + Sum;
false -> Sum
end
end,
{IP, JP, lists:foldl(CAL, 0, POSS)}
end
end,
%% 最后进行排序
T = lists:map(Fun1, POSS),
S = lists:filter(fun({_, _, Z}) -> Z == 0 end, T),
LEN = length(S),
if
LEN > 1 -> [];
LEN > 0 -> S;
true -> SS = lists:filter(fun({_, _, Z}) -> Z >= 0 end, T),
lists:sort(fun({_, _, Z1}, {_, _, Z2}) -> Z1 < Z2 end, SS)
end.


valid({I,J}, ARR) ->
I >= 0 andalso I<8 andalso J>=0 andalso J<8 andalso getvalue({I,J}, ARR) == 0.

getvalue({I,J}, ARR) ->
array:get(J, array:get(I, ARR)).

setvalue({I,J}, VAL, ARR) ->
array:set(I, array:set(J, VAL, array:get(I, ARR)), ARR).

hello erlang, part 1

练习重点

  1. erlang是单赋值的,就好像都经过java里边的final修饰过
  2. erlang没有内置的循环结构,这个和大多数语言是不一样的
  3. erlang是函数式语言,需要理解模式匹配编程方式
  4. erlang经常会遇到递归, 并且支持尾递归函数调用

练习1:数据库demo

作为学习erlang里边的模式匹配,熟悉语法。 编写一个数据库模块db.erl,它能够存储、检索、删除元素。我们需要实现的接口有:

db:new()                   %% Db.
db:destroy(Db) %% ok.
db:write(Key, Element, Db) %% NewDb.
db:delete(Key, Db) %% NewDb.
db:read(Key, Db) %% {ok, Element} | {error, instance}.
db:match(Element, Db) %% [Key1, ..., KeyN].

实现代码

-module(db).

%% Exported Functions
-export([new/0, write/3, read/2, match/2, delete/2, destroy/1]).

%% API Functions
new() -> [].
destroy(_) -> ok.
write(Key, Element, DB) -> [{Key,Element}|DB].
delete(Key, DB) -> delete_local(Key, DB, []).
read(Key, DB) ->
case DB of
[{Key, Element}|_] -> {oKey, Element};
[_|ODB] -> read(Key, ODB);
_ -> {error, instance}
end.
match(Element, DB) -> match_local(Element, DB, []).

%% Local Functions
match_local(Element, DB, LIST) ->
case DB of
[{Key, Element}|ODB] -> match_local(Element, ODB, [Key|LIST]);
[_|ODB] -> match_local(Element, ODB, LIST);
_ -> LIST
end.

delete_local(Key, DB, NDB) ->
case DB of
[{Key, _}|ODB] -> delete_local(Key, ODB, NDB);
[H|ODB] -> delete_local(Key, ODB, [H|NDB]);
_ -> NDB
end.

当然,case语句同样可以用如下代码替代:其中第三行的Key因为没有使用就换成_, 不然会提示警告variable ‘Key’ is unused。

delete_local(Key, [{key, _}|ODB], NDB) -> delete_local(Key, ODB, NDB);
delete_local(Key, [H|ODB], NDB) -> delete_local(Key, ODB, [H|NDB]);
delete_local(_, _, NDB) -> NDB;

练习2:排序算法

下面练习一下快速排序和归并排序,再熟悉一下模式匹配和递归的写法。

-module(sort).
-export([quicksort/1, mergesort/1]).

%% 快速排序
quicksort([H|Tail]) ->
lists:append([quicksort(lists:filter(fun(X) -> X < H end, Tail)),
[H],
quicksort(lists:filter(fun(X) -> X >= H end, Tail))]);
quicksort(L) -> L.

%% 归并排序
mergesort(A) ->
case length(A) of
0 -> [];
1 -> A;
LEN -> M = length(A) div 2,
merge(mergesort(lists:sublist(A, 1, M)),
mergesort(lists:sublist(A, M + 1, LEN - M)), [])
end.
merge(A, [], L) -> lists:append([L, A]);
merge([], B, L) -> lists:append([L, B]);
merge([HA|TA], [HB|TB], L) ->
if
HA > HB -> merge([HA|TA], TB, lists:append([L, [HB]]));
true -> merge(TA, [HB|TB], lists:append([L, [HA]]))
end.