0%

【C#】Yield Return (二)

/images/20190315/0.jpg

最近經過大師的指導後,對於 Yield Return 又或是迭代器有更深的感受,先來看一段以前常常會寫的程式

1
2
3
4
5
6
7
8
9
10
11
12
13
void Main()
{
var users = GetUser(idList);
//過濾黑名單
var notInBlackList = users.Where(x=> !x.InBlackList);

//往下做其他邏輯
}

public List<User> GetUser(List<int> ids)
{
//透過 ID 取得會員的詳細資料
}

如果 Id 一次有一百萬筆,為了避免擊沉 Database 做了分批查詢也是很合理的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void Main()
{
var users = GetUser(idList);

//過濾黑名單
var notInBlackList = users.Where(x=> !x.InBlackList);

//往下做其他邏輯
}

public List<User> GetUser(List<int> ids)
{
var Result = new List<User>();
// 每批搜尋 1000筆
var batchCount = 1000;

//計算出搜尋次數後分批查詢
for (int i = 0; i < 搜尋次數; i++)
{
Result.AddRange(
UserRepository.Get(ids.Skip(i * batchCount).Take(batchCount)));
}
}

當然也可以從呼叫端分批處理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void Main()
{
// 每批搜尋 1000筆
var batchCount = 1000;
for (int i = 0; i < 搜尋次數; i++)
{
var users = GetUser(idList);

//過濾黑名單
var notInBlackList = users.Where(x=> !x.InBlackList);

//往下做其他邏輯
}
}

public List<User> GetUser(List<int> ids)
{
//透過 ID 取得會員的詳細資料
}

如果需要再更複雜一些,要依據使用者的設定濾掉一些人,最終要將處理過程都填回 DB ,以便追蹤那些人是因為那些條件被過濾掉的 (不然被客戶抱怨沒收到通知,不知道往哪找),這時候程式就會變成這樣….

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void Main()
{
// 每批搜尋 1000筆
var batchCount = 1000;

var temp = new List<User>()
for (int i = 0; i < 搜尋次數; i++)
{
var users = GetUser(idList);

var notInBlackList = users.Where(x=> !x.InBlackList);

var wantToReceiveThisMessage = notInBlackList.Where(x => x.BlockMessageType != ThisMessageType);

//統計誰在黑名單被過濾掉
//統計誰不想收到這種類型的 DB

//可能批次更新是 5000 筆最有效率
temp.Add(InBlackList);
temp.Add(DontWantToReceiveThisMessage);

if (temp.Count > 5000)
{
// Insert Log
LogRepository.Add(temp);
temp.Clear();
}
}
}

public List<User> GetUser(List<int> ids)
{
//透過 ID 取得會員的詳細資料
}

條件可以無限增長上去,程式也會變得越來越複雜,當然這時候可以採取一些物件導向的設計將功能職責拆開,不過這不是這篇的重點所以跳過。

更符合語意的寫法

如果有用過 FluentAssertionNsubstitute 等套件,應該會發現它 API 設計得非常好讀好懂

1
2
3
customer.Active.Should().BeTrue(because, becauseArgs);

theObject.Should().NotBeSameAs(otherObject);

讓程式不再是一段一段的,而是一看程式就能理解這邊在做什麼,而且也因為如此讓每個方法的職責更清楚單一,所以我們可以將上述的程式改成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
void Main()
{
//從使用端傳來一大批 Ids List
Ids.GetUser().NotInBlackList().WantToReceiveThisMessage(ThisMessageType).Record();
}

public static IEnumerable<User> GetUser(this IEnumerable<int> ids)
{
var temp = new List<int>();
foreach (var id in ids)
{
temp.Add(id);
// 假設每 1000 筆是有最好的搜尋效率
if (temp.Count == 1000)
{
foreach (var user in UserRepository.Get(temp))
{
yield return user;
}
temp.Clear();
}
}

if (temp.Count > 0 )
{
foreach (var user in UserRepository.Get(temp))
{
yield return user;
}
}
yield break;
}

public static IEnumerable<User> NotInBlackList(this IEnumerable<User> users)
{
foreach (var user in users)
{
if (user.InBlackList == false)
{
yield return user;
}
}
yield break;
}

public static IEnumerable<User> NotInBlackList(this IEnumerable<User> users, MessageType type)
{
foreach (var user in users)
{
if (user.BlockMessageType != type)
{
yield return user;
}
}
yield break;
}

public static IEnumerable<User> Record(this IEnumerable<User> users)
{
var temp = new List<User>();
foreach (var user in users)
{
temp.Add(user);
if (temp.Count == 5000)
{
// Insert Log
LogRepository.Add(temp);
temp.Clear();
}
yield return user;
}

if (temp.Count > 5000)
{
LogRepository.Add(temp);
}

yield break;
}

如果還需要紀錄每個過程的分別過濾的狀態,只需要在方法開個統計的物件參數,在每筆過程中做紀錄即可,不只讓整個程式可讀性更高、內聚更強,之後如果不想要紀錄或不想要過濾黑名單,也可以很快的調整完成

1
Ids.GetUser().WantToReceiveThisMessage(ThisMessageType);