Company Ghost Story 公司鬼故事 6

Java

Observer vs. Inheritor

1
2
3
4
5
6
7
8
9
Tool tool = new Tool() {
@Override
protected void fireSomething(T t) {
super.fireSomething(t);
updateProcess(t);
}
}
void updateProcess(T t) {...}

當觀察者模式 (observer pattern) 的介面應優先考量 attach/addListener,不應該以繼承的方式覆寫 update/fireListener/notify 等函數。

WhatIf?

1
2
3
4
5
if (isA()) {
// something
} else if (isA() && isB()) {
// dead code
}

第二個 else-if 並不會執行到。

Equals vs. ==

1
2
3
if (a == b && a.equals(b)) {...}
if (a == b || a.equals(b)) {...}

對 Java 而言,有 Objects.equals 可以替代呼叫,又或者在 boolean equals(Object) 中包含 == 才對。

String.format

1
String.format("Move %s", fromObj, toObj);

對上參數數量是很重要的。

Mapper Function

1
2
3
4
5
6
7
8
static T a2b(String key) {
Map<String, T> mapper = new HashMap<>();
mapper.put("...", ...);
mapper.put("...", ...);
mapper.put("...", ...);
mapper.put("...", ...);
return mapper.get(key);
}

手動做 if-else 比較快的,建立 hash 可不是這麼簡單,記憶體宣告等因素需要考慮。而它也不屬於只宣告一次的靜態變數,因此並不適合這樣處理。

Comparable

1
2
3
4
5
@Override
int compareTo(T other) {
int[] c = {fa.compareTo(other.fa), fb.compareTo(other.fb) ...};
return firstNotZero(c);
}

if-else 不應該這樣被偷懶的,整個效能都爛了。

Argument vs. Method Name

1
2
3
4
5
void doExpand(boolean isExpand) {
if (!isExpand)
doCollapse();
...
}

好的命名決定品質。

Switch

1
2
3
4
5
6
7
8
static void enableAListener() {}
static void removeAListener() {}
static void addBListener() {}
static void stopBListener() {}
static void setCListener() {}
static void disableCListener() {
sListeners.remove(sAListener);
}

有一種被玩弄的感覺 …

Read More +

Company Ghost Story 公司鬼故事 5

Java

Duplicate Field

1
2
3
4
5
6
7
8
class C {
final A a;
final A b;
C(A a) {
this.a = a;
this.b = a; // why?
}
}

為什麼要做出兩個相同意義的欄位?

Strange Method Name

1
2
3
4
5
class UIComponent {
public void setDisable(boolean d);
public boolean isDisable();
public boolean isDisabled();
}

英文不好的我,一度慘死。

Strange Method Name II

1
2
3
4
5
class UIComponent {
public void setEnabled(boolean e);
public boolean isEnabled();
public boolean getEnabled();
}

拜託決定一種風格做一件事情。

Inefficient Listener

1
2
3
4
5
6
7
void init(Component c) {
c.addFocusListener(...);
c.addChangeListener(...);
c.addSelectListener(...);
c.setSelect(...); // trigger listener ...
c.setFocus(...); // trigger listener ...
}

初始化的優先順序很重要,多次觸發有可能發生,造成重複處理。

Override Method

1
2
3
4
5
6
7
8
9
@Override
public void uninstall() {
uninstallMe();
}
private uninstallMe() { // why not write into uninstall()?
// something ...
super.uninstall();
}

函數這麼簡單,為什麼 super.method 寫在原本的函數外部。出現不同於 @Override 的函數中實在少見。

Empty Listener

1
2
3
4
5
6
c.addWindowListener(new WindowAdapter() {
@Override
public void windowClosed(WindowEvent e) {
super.windowClosed(e);
}
});

不要註冊一堆空操作。

Override Empty

1
2
3
4
@Override
public void install() {
// please add comment here if remove parent processing
}

這不知道在寫什麼,也沒叫 super.install(),該觸發事件的都沒觸發,真的沒事嗎?應該是寫錯了吧

Read More +

Company Ghost Story 公司鬼故事 4

SVN

Commit File

1
2
3
4
5
6
7
$ svn status
.DS_Store
import.csv$
export.txt~
test_result.txt
golden.txt
? test_result.log
  • 別建立隱藏檔案格式,以小數點 . 開頭的在 Linux 可是隱藏檔案的意思
  • 別上傳 .DS_Store 那個是 Mac 平台上的隱藏檔案,並不需要
  • 別上傳 xxx.csv$那個是 Microsoft 平台上的 lock 檔案,並不需要
  • 別上傳 xxx.txt~ 那個是 emacs, nano, vim 等製造的備份檔案,並不需要
  • 別上傳測試結果,如 test_result.txt,正常來講會覆寫,傳上去結果沒產生怎麼辦,比對還會過呢。
  • 別上傳空資料夾

Commit Log

1
2
3
4
5
$ svn log golden.txt
------------------------------------------------------------------------
r32 | product_bot | 2003-01-13 00:43:13 -0600 (Mon, 13 Jan 2003) | 1 line
Update golden

拜託用自己的帳號上傳,不小心用到 product_bot 這種全自動的帳號,不曉得是誰修改的內容。

Read More +

Company Ghost Story 公司鬼故事 3

Java

Setter/Getter

1
2
3
4
5
6
7
class Parser {
X getX() {}
boolean x() {}
boolean readX() {}
void parseX() {}
void X() {}
}

Java 特性預設都是透過 setter/getter 的概念來完成成員變數的存取。為了擴充使用不建議直接存取變數。

這毫無設計的函數是發生甚麼事情?名字這麼像是想害死誰啊,而且還都是不同邏輯。

1
2
3
4
5
6
class Parser {
X eX() {}
boolean aX() {}
boolean sX() {}
void pX() {}
}

不是這樣子說完,就用奇怪的前綴解決。把整個動詞完整描述出來會讓你打字很痛苦嗎?

Inner/Anonymous Class

現在我們用一個指令去搜尋建立的 class files。

1
2
3
4
5
$ find ./bin -name "*\$[0-9]*.class"
xxxx$1$1.class
xxxx$64$1.class
...
xxxx$64$64.class

居然發現了非常非常多的匿名類別,而且還是爆炸性的嵌套。這意味者當改變一個 class 檔案時,編譯會一次可能產生上百個檔案。

1
2
3
4
5
6
boolean getToken() {
return new Token {
@Override
public String toString() { return "here we are"; }
}
}

這也是常常在調適函數時,不知道為什麼會有 IDE 崩潰的情況發生。其實都是上述的寫法氾濫,連帶的內存洩漏問題也很嚴重。事實上,大部分的匿名類別都可以額外地用功能去描述類別,請重新宣告並且命名好。

Long Name Method

1
2
3
void addXXXsToAdjacentYYYsSetSoItWillNotAAABBBWhereMMM();
void setXInDatabaseAAABBBField();

這種命名函數比惡名昭彰的匈牙利還兇殘,直接把文件描述寫在函數上,這時候又願意打字了。

請找個稍微中立一點描述方法,等到哪天換了連帶功能,所有相關函數都要重新做過。

Create Context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
boolean read() {
try {
readPartA();
externalCall();
} finally {
setCacheEnabled(true);
}
}
boolean readPartA() {
setCacheEnabled(false); // why not place it in read()?
...
}
// in external.xxx
java::setCacheEnabled(false); // what are you doing?
...
return true;

建立 context 的習慣是要寫在同一個檔案中,而且盡可能寫在同一個函數內。

跳來跳去,有時候只有下文,有時候只有上文,不覺得很可怕嗎?萬一中間執行失敗,直接萬劫不復。

Create Context II

1
2
3
4
5
boolean read() {
db.setDbCheckEnabled(false);
...
// where's your setDbCheckEnabled(true);, can I trust you more?
}

直接把所有重要檢查通通關閉,卻忘了開起來,直接出逃啦。

Create Context III

1
2
3
4
5
6
7
8
9
10
boolean read() {
try {
db.setDbCheckAEnabled(false);
db.setDbCheckBEnabled(false);
...
} finally {
db.setDbCheckAEnabled(true); // setDbCheckBEnabled() first, please
db.setDbCheckBEnabled(true); // setDbCheckAEnabled() last, please
}
}

大部分都不會錯,但建議採用堆疊的順序撰寫。因為有些保護機制會造成重複工作。

Create Context IV

1
2
3
4
5
6
7
8
9
10
11
12
boolean read() {
try {
db.setDbCheckAEnabled(false);
db.setDbCheckBEnabled(false);
...
db.setDbCheckBEnabled(true); // I don't know.jpg
...
} finally {
db.setDbCheckBEnabled(true);
db.setDbCheckAEnabled(true);
}
}

我不知道.jpg。

Regression Test

Create Test Folder

1
2
3
4
5
6
$ tree Regression/FunctionA
├─testBBBB
│ └─testCCCC
│ └─testDDDD
│ └─test.sh
...

為什麼需要前綴 test?請給我一個理由。

Create Test Folder II

1
2
3
4
$ tree Regression/FunctionA
├─DoNotThrowNoSuchElementException
│ └─test.sh
...

如果是預期丟出錯誤的測試就算了,但錯誤的實例也是會變動的,這樣的資料夾名字不具有太深遠的意義。

Read More +

Company Ghost Story 公司鬼故事 2

C/C++

Overprotection

1
2
3
4
5
bool find_intersect(list<shape> a, tree b, shape c) {
if (b == null)
b = new tree(a);
return b.find_intersect(c);
}

過度保護,造成最後沒人知道 a 和 b 的關係。結果有時候 b 不是由 a 建立的,整串都不知道在做什麼。

Evil Comma

1
2
if (str[0] == '-');
str = str.substr(1);

排版已經告訴你可能發生了問題,但看起來還是救不了下面那一行。似乎沒有發生意外,但是非常恐怖。

Export

1
2
3
4
string x = "";
for (int i = 0; i < n; i++)
x = x + pts[i].str();
// x += pts[i].str();

組成不是大問題,串起來就崩潰 $O(n^2)$。請善用 in-place 的串接。

Test Script

1
2
3
4
// testXXX.cpp
int main() {
... // 100,000+ lines
}
1
g++ testXXX.cpp -O3

浪費生命從編譯時間著手,拜託開 -O0 就好,這樣就近乎線性時間的編譯速度。開到 -O3 會有平方層級的編譯時間,$(10^5)^2$ 雖然也不是很多,但突然從幾秒變成幾個小時是有可能的。

Algorithm

List vs Hash

List 比 Hash 快,不用計算雜湊值,直接比對更快。

數量一大是不是要平行去找了啊,小伙子。

Math

Degree vs Radian

1
2
3
double rotate90(double deg) {
return Math.toRadian(deg + 90) % 90;
}

不會弧度記算沒關係,晚點我們再檢查一下還有哪一段混著算吧。希望都沒事。

Git

linter

不要 format code,因為 git blame 不好看。

1
2
3
4
5
6
7
8
9
for (int i = 0; i < n; i++) {
try
{
Data rowData = data.getRow(i);
}
catch (Exception e)
{
}
}

心靜自然涼,再多的 try-catch 我也可以接受。沒事,我不會受到傷害。

Windows

Set Env Variable

1
2
1. 我的電腦 > 內容 > 進階內容設定 > 環境變數
2. 重新開機 // ?????

在大部分情況是不用重新開機,重新開啟 shell 即可。

Jenkins

Build Artifacts

每一次建置結果都保存下來,用定時刪除。

大部分的建置結果只需要保留進 25 次就差不多,如果每一次建置就要耗掉 1 GB 的硬碟空間,那在一天內上傳了好幾次,就會看到 1 TB 硬碟直接死去。

請不要說買硬碟解決問題,這並沒有解決根本問題。

Build Pipeline

充分運用硬體資源,全部平行。

有些工作是需要整台伺服器的計算資源,如果安排兩個以上的工作時,會造成另一個工作結果不穩定。而有些工作會需要大量的硬碟 IO,兩個以上甚至會造成卡死的狀態。

請不要說買更多的機器或 CPU 解決問題,這只要開一個有平行工作,另一個工作沒設計好就會自動失敗。

Read More +

Company Ghost Story 公司鬼故事 1

以下出現的問題,都是在公司遇到的事件,以類比的方式來描述遇到的狀況。

Java

Unique

1
2
3
4
5
6
class DbObj {
@Override
public boolean equals(Object obj) {
return this.compareTo((DbObj) obj) == 0;
}
}

對於唯一存在的物件,別用 compareTo() 來實作 equals(),它們可以直接用 == 判定。而且大部分的 compareTo 是比較慢的線性實作。如下述的物件

1
2
3
4
class MyString {
@Override
public boolean equals(Object obj) {...}; // possible O(n)
}

結果就是變得超慢。

toString in Comparison

1
2
3
4
5
6
class CompositeClass implements Comparable<CompositeClass> {
@Override
public int compareTo(CompositeClass other) {
return toString().compareTo(other.toString()); // ???????
}
}

千萬別這麼幹,當我們對類別增加成員時,同時修改 toString() 後,運行結果也不同。這也會帶來嚴重的效能問題,試想著排序的時候,產生一大堆的字串用於比較,那麼垃圾回收就會佔有大部分的時間。

toString() in Key

當需要對 enum 或者其他相關物件進行反序列時,避開 toString(),因為這個函數很容易被其他人覆寫。

1
2
3
4
5
6
7
8
enum SomeType {
@Override
public String toString() {
return super.toString() + "...";
}
}
SomeType.valueOf(type.toString()); // ?????

有人會說這個函數不該被覆寫,但能操作就可能出事。

It is not in C++

1
2
3
boolean sameLocation(Point a, Point b) {
return a == b; // ?????, Objects.equals(a, b)
}

抱歉,您使用的是 Java,並沒有 operator== 的語法,所有的 == 都是比較相同物件,並不會實作對應的 equals()

Numeric Comparison

1
2
static final Comparator<Point> compareX =
(a, b) -> a.getX() - b.getX(); // X, Long.compare(a.getX(), b.getX());

這種容易發生 overflow/underflow 的寫法,並不建議模仿 C/C++ 的習慣,請多用內建的函數比較。

this in Constructor

1
2
3
4
5
6
7
8
9
10
public Point {
Point(long x, long y) {
mX = x;
mY = y;
}
Point() {
mX = 0; // X, this(0, 0);
mY = 0;
}
}

請盡量使用建構子,各自獨立容易漏掉一些共同的檢查。

Initialization

1
2
3
4
5
6
Point copy() {
Point pt = new Point();
pt.setX(getX());
pt.setY(getY());
return pt; // new Point(getX(), getY());
}

能使用建構子完成的事情就盡量使用,分次使用可能會造成過多額外的檢查或調整,效能就會往下掉。

Find the Minimum/Maximum Element

1
2
3
4
5
6
7
8
Point getMax(List<Point> testList) {
Collections.sort(testList);
return testList.get(testList.size() - 1); // Collections.max(testList);
}
Point getMin(List<Point> testList) {
return testList.stream().sorted().getFirst().orElse(null);
// testList.stream().min().orElse(null);
}

排序很簡單,但也不能亂寫。

排序複雜度大部分為 $O(n \log n)$,事實上找最小值只需要 $O(n)$。當 $n = 10^6$ 時,效率就可能差到 20 倍。

Computed Getter

1
2
3
4
5
6
7
8
static final Comparator<CPoint> compateLocation =
(CPoint ca, CPoint cb) -> {
long p0x = ca.getCPt().getX();
long p1x = cb.getCPt().getX();
long p0y = ca.getCPt().getY();
long p1y = cb.getCPt().getY();
... // X, return ca.getCPt().compareTo(cb.getCPt());
}

請不要這麼寫,排版好看行數很多並不是好藉口,這呼叫了好幾次 getCPt(),如果牽涉到好幾步複雜運算,整個效率就慢上了好幾倍。

Computed If-Else

1
2
3
4
5
if (getA() != null &&
getA().getB() != null &&
getA().getB().getC() != null) {
...
}

寫在同一個 if-statement 很方便,卻造成效能嚴重退化。不如蠢一點的寫法,或者透過 Optional<> 來描述回傳型態。

1
2
3
4
5
6
7
8
9
A a = getA();
if (a != null) {
B b = a.getB();
...
}
getA().ifPresent(a -> {
a.getB().ifPresent(...)
});

List Getter

1
2
3
4
5
6
7
8
LinkedList<Long> X;
void doSomething(List<Long> X) {
for (int i = 0; i < X.size(); i++) {
long x = X.get(i);
...
}
}

別鬧了,這會出大事。效能殺手就是你。LinkedListget(index) 可是 $O(n)$ 的。

Immutable Methods

1
2
3
4
void parse(String elem) {
elem.trim(); // ??????, elem = elem.trim();
...
}

記得把回傳值接起來,請不要陡增效能問題。

Method Reference

1
2
3
4
5
6
7
8
9
10
Map<Long, List<Point>> map;
void add(Point a) {
List<Point> list =
map.computeIfAbsent(a.mX, key -> new LinkedList<>()); // X, Point::prepare
}
static List<Point> prepare(Long key) {
return new LinkedList<>();
}

盡量使用 method reference,防止 memory leak,也降低 lambda metafactory desugar 的花費。

Boxing

1
2
3
4
5
6
7
Double getRotate() { // X, double
double r = ...;
return r;
}
boolean getMirror() {...};
Boolean isMirror = getMirror(); // X, boolean

既然有預設值也確保不會發生 null,請不要過度包裝。拆拆裝裝的狀況會造成垃圾過多。

Exception is NOT a Return Value

1
2
3
4
5
6
7
8
public String getFileExt(File f) {
try {
String token = parseExt(f);
return "." + token;
} catch (NullPointerException e) {
return null;
}
}

請別接收這麼奇怪的 runtime exception。在 Java 中,exception 很昂貴的,造價就是整個 call stack。

也別這樣追蹤物件的建立,大量物件會造成內存不足。

1
2
3
class Point {
private Throwable trace = new Throwable();
}

Lightweight Exit

1
2
3
4
5
void process(Point p) {
List<Long> array = new ArrayList<>(10000); // ?????
if (p == null)
return;
}

別急著建立一堆用不著垃圾,有可能在後續判斷中直接返回。盡量縮小變數的生命週期,快用到的時候再準備計算資源。

If-Elif-Else

1
2
3
4
5
6
7
8
9
if (token.equals("a"))
{
}
else if (token.equals("b"))
{
}
if (token.equals("c"))
{
}

別說排版不重要,那一定是沒看過有人不小心漏了什麼。在大部分情況不太會出事,只是多判斷了幾次造成效能退化。

Dead instanceof

1
2
3
4
5
6
7
8
9
10
public abstract class Shape;
public class Poly extends Shape;
public class Rect extends Poly;
void write(Shape shape) {
if (shape instanceof Poly) {
} else if (shape instanceof Rect) {
// ?????, unsearchable
}
}

凡事有先後,請注意繼承關係。

Observer Pattern

1
2
private List<ObsListener> list = new LinkedList<>();
private Set<ObsListener> list = new HashSet<>();

當訂閱者數量不少,且容易進進出出,請不要用線性的 LinkedList。當你需要嚴格地再現 BUG,就不要使用不穩定順序的 HashSet,請使用 LinkedHashSet 是一種保守的選擇。

Remove Observer

1
2
3
4
5
6
class CreateUI extends Dialog {
public CreateUI() {
db.addListener(mChangeListener);
}
// where is your db.removeListener
}

別忘記移除掛載到資料庫的觀察者,這樣往往覆覆操作,越來越慢真的不能怪人。

Opposite Behavior

1
2
assert pathA.endsWith(pathB) ==
pathB.isEndOf(pathA); // fail ??????

英文函數命名上,操作對稱就該對稱。英文不好要先說,邏輯不好也先說一聲,大家可以幫忙的。

Global Garbage

1
2
3
4
5
6
7
static List<DbObj> tmp;
List<DbObj> getXXX() {
tmp = new LinkedList<>();
parallel...
return tmp;
}

別擠壓到下一個人的生存空間,要是沒有使用 getXXX,會看到一堆資料庫物件被卡在全區變數裡頭。

Useless Argument

1
2
3
4
class Path {
boolean startsWith(Path p);
boolean startsWith(Path p, Object a); // new one
}

別因為不想改動原本的,建立一個一模一樣名字的函數,而且第二個參數並沒有使用到,overloading 不是這樣子設計的。

Count

1
2
3
4
int getPointCount() {
return mPoints.parallelStream().count();
// return mPoints.size();
}

可以覺得慢,但不要總是開平行解決事情。這種計數問題,通常都有相關的方法可以呼叫。如果沒有,請聯絡相關人士。

Tooltip

1
2
3
String tooltip = ""; // should StringBuilder
for (DbObj t : db.getObjects(XXX.class))
tooltip = tooltip + t.toString();

沒人說這樣不好,除了串接消耗 $O(n^2)$,一般也不會把所有東西拉出來。

Convert Set to Array

1
2
3
4
5
6
List<Point> toArray(Set<Point> set) {
List<Point> arr = new ArrayList<>();
for (Point e : set)
arr.add(e);
return arr; // return new ArrayList<>(set);
}

建構子更方便,別這麼辛苦,效能至少慢了兩倍。

演算法還是有它的極限在的,常數的確不是很重要,但是數量一大的時候,均攤造成額外操作造成的垃圾,在 Java 中會變得相當明顯。

Error Message

1
2
3
4
5
6
7
8
9
10
11
Integer parseInteger(String x) {
String errMsg = x + "is an invalid integer format...";
errMsg = attachCurrentLineMessage(errMsg);
try {
Integer n = Integer.parseInt(x);
return n;
} catch (Exception e) {
System.err.println(errMsg);
return null;
}
}

人家還沒出錯,先別急著準備錯誤訊息。提前準備的字串複雜度比處理的複雜度還高,這絕不允許。

New Option

1
2
3
4
5
static void import(boolean a);
static void import(boolean a, boolean b);
static void import(boolean a, boolean b, int c);
static void import(boolean a, boolean b, int c, String d);
...

別再為了新參數往上疊,請用更容易看出參數意義的 builder 的寫法。

越來越多的參數,造成容易寫錯傳參的順序的悲劇,這時候 BUG

Read More +