Resolve cycle#
之前用 astroid 寫的 tool pydependency,
將 dependency graph 匯出成 .dot 或是一些 .json 格式後,
就有幾個方便的 tool 可以來分析了。
以下的故事基本上都是在分析 junyi-academy 的 code,但大部分的壞味道,從 khan-academy 時代就有了。如果只看 dependency graph 沒感覺的話,也可以載 source code 來看看。
graph tools#
graphviz#
graphviz 為老牌的 graph 視覺化工具。 pylint 的內含的工具 pyreverse 就是拿 graphviz 來畫 UML 的圖的。好處就是簡單好上手,畫出第一張圖:
echo "digraph { hello->world }" | dot -Tpng -o dot.png
強大之處在於其 layout 演算法,但是如果 cycle 很多,畫出來就會全部結成一團, 也不知道能怎麼調整。
python 部分套件可以安裝 pygraphviz 或 pydot,讀寫 dot 會方便些。
pygraphviz 包裝原生 graphviz library 的 C api 到 python 中,所以對於 agraph
的操作是直接對應到 C 的 data structure 的。
而 pydot 對於 graph 的操作都還是在 python 當中。直到要找 graphviz tool 時, 才利用其中的 parser 作資料的轉換。
兩個在 layout/draw 時,都還是直接用 subprocess.Popen 呼叫 graphviz 的
command line tools。pygraphviz 也沒有因為直接呼叫 C,而直接用像 gvToolTred
這樣的 function。還是多了一層 serialize、deserialize 及 pipe。
在 tred 或是 layout (只需要多標上 position?) 這樣的功能上,感覺滿慘的。
tred 是一個 graphviz 的 transitive reduction tool。
比方說,一張圖長這樣:
會被 tred 轉成
如果沒有 cycle 的話,圖會在不影響 dependency 先後順序的情況下變得乾淨一點。 比較好做一些觀察。
不過如果有 cycle 的話,結果不會是唯一的。
例如 A->B->C; A->C; B->A 與 B->A; A->B->C; A->C 的結果就會不一樣。
猜測就是 graphviz 用 dfs 爬的順序來隨便決定是哪一個,先走到 B 就會把 B->C
拿掉,而先走到 A 的話,就會把 A->C 拿掉。
中間還踩到一個 pygraphviz 1.3.1 的 issue ,在有 cycle 時,會出現 warning,然後因為 data type 的關係,就丟出 exception 了...
networkx#
networkx 為 python 的 network (graph) 分析工具。可以把任何在 python 中有 hash 的 object 串接起來變成 graph。有內建很多演算法可以用。
視覺化的部分可接到 matplotlib 來呈現。也有 graphviz 的 plugin,可轉成 graphviz 來用 graphviz 的強大 layout engine。
graphviz 的轉接要多安裝 pygraphviz 或 pydot 然後透過 nx.nx_agraph 或是
nx.nx_pydot 來接到 graphviz。
兩個 module 都是在用到時才會嘗試 import,因此應該裝一個即可, 轉換的過程基本上就是讀舊的然後用新的 api 生成另一個 type 的 graph,因為兩邊 spec 不全相同,因此會有少一些東西,因此建議都在 networkx 上來做操作, 直到要 layout、draw 時再來考慮轉換。
cytoscape.js#
cytoscape 是一個生物資訊軟體,cytoscape.js 是它 js 版本。 在 js 中直接對 graph 做操作,並用 html5 canvas 來呈現視覺化效果。
比起 d3,可以不用管很多細節的部分,比較著重於 node、edge 的操作。 整個 API 很 jquery style,用類似的 selector 概念來選擇 node 上 style, 非常易於使用,調整一些簡單的 style 與做一些簡單的互動非常方便。
第一次用這來畫出 dependency graph 時真是嚇到了,看起來太複雜,不太有用。
這是用 concentric layout,並把 out degree 高的放在中間,
唯一有用的資訊只有中間是 main 與 api.v1,兩個的 out degree 最大。
如果改成 breadthfirst,由 main 做 root,可能是因為 cycle 太多,
計算是那一層的沒有辦法很準。全部排起來只有 6 層,中間穿插的情況很嚴重,也都看不清楚。
cose layout 會跑很久,畫起來就是一大坨,也是非常不好看。 需要找一些其他的指標來呈現才行。
Reduce nodes#
先簡化一下圖,要考慮的只有 cycle,先把不在 cycle 中的 node 拿掉。 無腦暴力做法,把所有 indegree、outdegree 為零的 node 拿掉。 這些並不可能是 cycle 的一部份。拿掉之後又可能會有一些 node 沒有 indegree、outdegree 就這樣拿到這些 node 都不見為止。
接著利用 graphviz dot 並設定 rankdir="LR" 來排,再將座標一起匯出成給 cytoscape
的 json 格式。
import networkx as nx
g = nx.nx_agraph.read_dot(sys.argv[1])
g = nx.DiGraph(g)
...
a = nx.nx_agraph.to_agraph(graph)
a.layout(prog='dot')
eles = []
for n in agraph.nodes_iter():
node_data = {'id': str(n)}
pos = tuple(map(float, n.attr['pos'].split(',')))
pos = (pos[0] / 4, pos[1] / 2)
eles.append({'group': 'nodes', 'data': node_data, 'position': {'x': pos[0], 'y': pos[1]}})
for e in agraph.edges_iter():
eles.append({'group': 'edges', 'data': {'source': e[0], 'target': e[1]}})
再把從右到左的 link 標成紅色的,並手動的整理一下,
package "badges"#
分幾個群來看,只看 badges 內部的話,其實滿好的,可以直接分成大概 4 層
models_badgesdatabase model 的定義。badges定義所有 badges 的 abstract base class。topic_badges、exercise_badges,一樣是 abstract class 的定義。- 其他除了
util_badges外的 module,真正的定義都放在這邊。 util_badges將所有實現的 badge modules 都 import 進來,應該為整個 package 對外的單一窗口。cy_all_modules.$(':selected').unselect(); (els => { els.select(); console.log(els.indegree()); els.incomers('node').each(e=>console.log(e.id())); })(cy_all_modules.$id('badges.util_badges'));可得知有 12 個 module 會連到
util_badges,
整個 badges 的矛盾來自於 class_time 與 video_models 的矛盾。
add_entry()->commit_log_summary_coaches()->ClassDailyActivitySummary->VideoLog
這條路徑是 cycle 的主因。但很類似的
attempt_problem()->commit_log_summary_coaches()->ClassDailyActivitySummary->ProblemLog
則沒有問題。
在 exercise 部分的處理,bussiness layer 與 data access layer 兩塊是拆切開來的;
而 video 部分則是沒有區分 bussiness 與 data access layer,全部分在同一個 module
當中,這樣自然當中間的 bussiness logic 想要拆切出去時,必須要考慮在這時分家。
不然就會造成 circular dependency 了。這隻的 caller 也都只有 api 介面及 unittest,
不要動太大的話,將 add_entry 拆切出去即可移除 cycle。
看起來好像只是小惡,沒什麼關係是吧?為什麼要做這樣的 refactoring?不是還可以動嗎? python 不是支援 circular import 嗎?
如果在 code 中,只寫了:
import badges.util_badges
就會噴出 exception
File "./badges/util_badges.py", line 16, in <module>
import badges
File "./badges/badges.py", line 4, in <module>
import models_badges
File "./badges/models_badges.py", line 9, in <module>
import util
File "./util.py", line 1, in <module>
import auth.cookies
File "./auth/cookies.py", line 5, in <module>
import user_models
File "./user_models.py", line 39, in <module>
import goals.models
File "./goals/models.py", line 9, in <module>
import exercise_models
File "./exercise_models.py", line 39, in <module>
from exercises import file_contents, stacks
File "./exercises/stacks.py", line 5, in <module>
import custom_stack_model
File "./custom_stack_model.py", line 11, in <module>
import topic_models
File "./topic_models.py", line 31, in <module>
import autocomplete
File "./autocomplete.py", line 3, in <module>
import video_models
File "./video_models.py", line 34, in <module>
import classtime
File "./classtime.py", line 12, in <module>
import profiles.activity_graph as activity_graph
File "./profiles/activity_graph.py", line 7, in <module>
from badges import models_badges, util_badges
ImportError: cannot import name models_badges
但是如果是:
import utils
import badges.util_badges
就會很神奇的沒事了。其實不管是先 import util、user_models、topic_models、
classtime 都會沒事。原因跟 from ... import 的寫法有關,
詳細可以參考之前的筆記 python import
。
因此,如果不想要發生:
- 移除 unused import 後,發現跑不起來。
- 改用 explicit relative import 後,發現跑不起來。
- 改用 absolute import 後,發現跑不起來。
- 修改了 import 順序後,發現跑不起來。
我想還是好好設計不要有 cycle 可能會比較簡單一點。這些聽起來都非常容易的發生, 等到發生時才來 refactoring 應該是更麻煩的事。
package "gandalf"#
gandalf 內部看起來也沒問題。gandalf.bridge 是一個開放給
bussiness layer 的有點像 AB-test 控管的東西入口,應該是這個 package 中,
唯一對外開放的 interface。
而 package 出口 gandalf.config,則指向了 user_models,乍看之下由
bussiness layer 指向 data access layer 好像不是什麼問題,但就一樣壞在
user_models 中,包含了 bussiness layer 與 data access layer 的責任,
因此又會指向其他的 bussiness layer,然後就回到 gandalf.bridge 這個入口了。
user_models 與 topic_models 看起來就是大部分問題中心,
在應該沒有簡單的搬移就可以馬上解決掉的好事。需要觀察更多跟他們相關的責任,
才知道要怎麼拆切會比較好。因此先來釐清 gandalf 的需求:
- get information about current user
- check current user is administrator or not
整個 package 特性應該跟 authorization 類型的 model 滿像的,之後跟其他同類型的一起討論。
"auth" modules#
不像上面兩個 package,auth 部分抽出來看就已經很混亂了。再加上 user_models、
nicknames、phantom_users 一起看的話,這根本是一個大坑。
發生 cycle 的部分,python 已經無法容忍,需要出一些怪招,才能繼續維持著這個恐怖平衡。
api/auth/auth_models.py
中,開始將 from ... import ... 寫在 file 的最後面,避免因 cycle 而導致 import
失敗。這樣寫取決於兩個 module import 及定義的順序,以及 function 中的 global
是在 run time 才 dereference 等特性。
如果什麼都不看,從頭想的話,應該會架構成這樣:
中間因為希望能加入不同的 authentication provider,因此需要一個 extension 的機制, 做個 reverse dependency ,希望 extension 的邏輯能與其他東西分離,方便擴充。 不過同樣的 presentation layer 也可能需要建立一個 extension 機制才行。 到底要打穿到什麼程度,extension 的機制需要存在於每一個 layer 嗎? 這就需要看中間的邏輯跟 auth 嵌多深了。
事與願違,api.auth.auth_models 中的 OAuthMap 就已經把 google、facebook
這樣的 name 直接寫在裡面了。一旦進入了 data access layer,要改動就很麻煩了。
如把這些 provider 的邏輯加入到這麼底層,除非將 provider 的 code 也拆切成不同 layer,
不然又注定要 cycle 了。還是先放棄 extension 機制,先確定 cycle 的問題可以解決。
TODO
-
util被很多 data access layer 的連到,而util又會回到api.auth.auth_util等。其實util中的 auth 完全可以拆出去,因為這些相關 function 的 caller 其實很集中,其他人並沒有需要。 - def current() 中有 cycle 須將 current user 的責任移出 user_models
-
user_util與api.auth.decorator的分別? 一個給 falsk api,一個給 gae?user_util中大致可以分三類,role util、auth decorator、???。"???" 的部分,就是造成 cycle 的來源,因
user_models.UserData.get_from_username_or_email竟然去呼叫user_util.get_possibly_current_user。乍看之下毫無關聯,
get_from_username_or_email像是一個 selector, 輸入的東西可能是 username 或是 email,然後希望取得一個 user 的 structure。 但原罪可能就是在 user 的資料很混亂,如果輸入的是 email,有可能是重複的...因此這邊的神邏輯就是先看看 current user,如果 current user 就已經滿足這個 selector,就直接 return current user 了。而如果沒滿足,才真的下 db query 去用 username 或是 email 去找看看。
(看到 "possible"、"possibly" 頭就有點痛。這種字都有感染性,一旦有一隻在 最底層,caller 每一個都會是 "possibly",possibly work。)
只要將
get_possibly_current_user搬到 user_models 當中,即可解決 cycle。 但這樣的搬移前提是建立在 current user 的責任是在 user_models 當中。 - user_models 中的
phantom_users.phantom_util.is_phantom_id只import phantom_users會 work? @@ -
user_models中,import phantom_users,乍看之下只有 import package, 所以沒有 cycle。但仔細一看,發現都直接去 referencephatom_users.XXX, 這表示在import user_models之前,或是呼叫這些 function 之前, 一定要有一個地方有 import 過這些 XXX,不然呼叫下去就會找不到...最可怕的是這種東西 pylint 沒有抓...
這種避開 cycle 的方法也真是太絕了...
-
topic_models - https://i11www.iti.kit.edu/_media/teaching/sommer2004/networkdrawing/spring.pdf
- https://www.researchgate.net/publication/282504460_A_Decision_Support_System_to_Refactor_Class_Cycles
- principle
ref:
留言