誤解されたら困るんでワタシから聞いたとは言わないで。
ESLint でもなんでもいいけれど「長過ぎる」「複雑過ぎる」なお行儀チェックは、整理がある程度まで進んできたら出来るだけ実施したいわけだね。とにかく完成させる・動くものを作る、を優先してると結構複雑なのを平気で書きがちだけれど、そういうことを続けてると「書いた本人さえ理解出来ないプログラム」になる。
ESLint の場合だと Limit Cyclomatic Complexity、enforce a maximum number of statements allowed in function blocks が該当する。
そうなんだけどさ、enforce a maximum number of statements allowed in function blocks のデフォルト、厳し過ぎなんじゃないのかね、10 statements がデフォルトなのだわよ。一般に、は知らんけれど、「普通のエンジニアにとって」は、たぶん「エディタでスクロールが必要になるならバカデカ過ぎ過ぎ」というのはおそらく揺るがなくて、なので20~30ステートメント程度が「うわぁやだ」と感じる閾値なんじゃないのかなぁ、と思うんだけれどねぇ。実際多少でも「現実的」なプログラミングであるならば、それくらいの量の機能まとまりは「ごくごく普通」だし、ついでにいえば「誤った機能分割ほど厄介なものはない」のであってだな、たいていの場合は「分けりゃいいってもんじゃない」ルールに従って、綺麗な分割が見つかるまでは闇雲に分割しない、てのも「普通の感覚」なわけなんだね。
このやだなぁをちょっとだけマシにするために導入したのがこんな:
1 function NodeVisFilterSettings() {
2 //
3 function _to_valarray(selector) {
4 return $(selector).map(function () {
5 return $(this).val().trim();
6 }).get();
7 }
8 function _csv_split(selector, conv) {
9 return $(selector).val().split(",").filter(function (e) {
10 return e.trim();
11 }).map(function (e) {
12 if (!conv) {
13 return e.trim().toUpperCase();
14 }
15 return conv(e.trim());
16 });
17 }
18 //
19 let _this = this;
20 this.settings = {"a": {}, "c": {}, "v": {}};
21 //
22 this._from_ui_a = function () {
23 let target = _this.settings["a"];
24 //
25 target["aired_thr"] = implhelper.build_yyyymm_range_from_ui(
26 $("#aired-year-min-thr").val(),
27 $("#aired-month-min-thr").val(),
28 $("#aired-year-max-thr").val(),
29 $("#aired-month-max-thr").val(),
30 "-");
31 target["prem_thr"] = implhelper.build_yyyymm_range_from_ui(
32 $("#prem-year-min-thr").val(),
33 $("#prem-season-min-thr").val(),
34 $("#prem-year-max-thr").val(),
35 $("#prem-season-max-thr").val(),
36 "");
37 //
38 let animetypesincl =
39 $("#animetypes-incl-or-excl-incl").is(":checked");
40 target["animetypesincl"] = animetypesincl;
41 let animetypes_sel =
42 animetypesincl ? "#animetypes-incl" : "#animetypes-excl";
43 target["animetypes"] = _csv_split(animetypes_sel);
44 //
45 let genresincl = $("#genres-incl-or-excl-incl").is(":checked");
46 target["genresincl"] = genresincl;
47 let genres_sel = genresincl ? "#genres-incl" : "#genres-excl";
48 target["genres"] = _csv_split(genres_sel, function (e) {
49 let k = Object.keys(_datmng.master["gnr"]).find(
50 (key) => (key.toLowerCase() === e.trim().toLowerCase()));
51 return _datmng.master["gnr"][k];
52 });
53 //
54 let producersincl =
55 $("#producers-incl-or-excl-incl").is(":checked");
56 target["producersincl"] = producersincl;
57 let producers_sel =
58 producersincl ? "#producers-incl" : "#producers-excl";
59 target["producers"] = _csv_split(producers_sel, function (e) {
60 return _datmng._urlbuilder.malid2id(e.trim());
61 });
62 //
63 target["popularity-thr"] =
64 _to_valarray("#popularity-min-thr, #popularity-max-thr");
65 target["ranked-thr"] =
66 _to_valarray("#ranked-min-thr, #ranked-max-thr");
67 target["episodes-thr"] =
68 _to_valarray("#episodes-min-thr, #episodes-max-thr");
69 target["duration-thr"] =
70 _to_valarray("#duration-min-thr, #duration-max-thr");
71 target["favorites-thr"] =
72 _to_valarray("#anime-favorites-min-thr, #anime-favorites-max-thr");
73 //
74 target["rating"] = [
75 "#rating-g",
76 "#rating-pg",
77 "#rating-pg13",
78 "#rating-r",
79 "#rating-rp",
80 "#rating-rx",
81 "#rating-none",
82 ].filter(function (sel) {
83 return $(sel).is(":checked");
84 }).map(function (sel) {
85 return $(sel).val();
86 });
87 };
88 //
89 this._from_ui_c = function () {
90 let target = _this.settings["c"];
91 //
92 target["chara-ms"] = $("#chara-ms").val();
93 //
94 target["favorites-thr"] =
95 _to_valarray(
96 "#chara-favorites-min-thr, #chara-favorites-max-thr");
97 };
98 //
99 this._from_ui_v = function () {
100 let target = _this.settings["v"];
101 //
102 target["bloodtypes"] = _csv_split("#cv-bloodtypes");
103 //
104 let nativesincl = $("#natives-incl-or-excl-incl").is(":checked");
105 target["nativesincl"] = nativesincl;
106 let natives_sel = nativesincl ? "#natives-incl" : "#natives-excl";
107 target["natives"] = _csv_split(natives_sel);
108 //
109 target["birth-thr"] =
110 _to_valarray("#cv-birth-min-thr, #cv-birth-max-thr");
111 //
112 target["favorites-thr"] =
113 _to_valarray(
114 "#actor-favorites-min-thr, #actor-favorites-max-thr");
115 //
116 target["actor-acting-total-thr"] =
117 _to_valarray(
118 "#actor-acting-total-min-thr, #actor-acting-total-max-thr");
119 //
120 target["actor-acting-main-thr"] =
121 _to_valarray(
122 "#actor-acting-main-min-thr, #actor-acting-main-max-thr");
123 };
124 //
125 this.from_ui = function () {
126 _this._from_ui_a();
127 _this._from_ui_c();
128 _this._from_ui_v();
129 };
130 //
131 }
入力コンポーネントから値をかき集めて構造に詰め込むだけの class で、当たり前だが入力フィールド数に「正比例」するわけね、ステートメント数が。無論「いろいろ命名規則を導入してどうにかループ処理で記述する」ことでもコンパクトには出来るけれど、そういうことではなく「html に書いてる id が剥き身で見えてた方がわかりやすい」とかいろいろあるでしょう、だから「ループした方が数十億倍ステキだ!」てわけでもない。愚直には愚直の良さがある。
すなわち「max-statements」は、こういった「単純移送処理」で露呈しやすいというわけだね。そしてこうしたものは「長いから万死に値する」という類のものでもないわけだ。どちらかといえば「長くなってもこればっかは致し方ないもの」だ。長くていやらしいのは「単純な繰り返しでない複雑なロジック」であって、むしろそうしたものだけが max-statements で引っかかるようであって欲しいわけだね。
で、結構長い時間このチェックを使ってたので随分前から気付いていたんだけれど、この max-statements のメトリクスってさ、「ほんとに function 単位」なのよね:
1 function foo() {
2 var bar = 1; // one statement (outer)
3 var baz = 2; // two statements (outer)
4 var qux = function () {
5 let x = 1; // one statement (inner)
6 let y = 2; // two statement (inner)
7 let z = 3; // three statement (inner)
8 }; // three statements (outer)
9 }
なので forEach やらなんやらの callback として匿名関数(というのかな?)で書いてると、その匿名関数内ローカルな statements 計測をするし、その forEach の外からみれば、どんだけその匿名関数がバカデカかろうが「one statement」なわけね。
というわけで…、IIFE てしまえば「誤魔化せる」わけなのよ:
1 function NodeVisFilterSettings() {
2 //
3 function _to_valarray(selector) {
4 return $(selector).map(function () {
5 return $(this).val().trim();
6 }).get();
7 }
8 function _csv_split(selector, conv) {
9 return $(selector).val().split(",").filter(function (e) {
10 return e.trim();
11 }).map(function (e) {
12 if (!conv) {
13 return e.trim().toUpperCase();
14 }
15 return conv(e.trim());
16 });
17 }
18 //
19 let _this = this;
20 this.settings = {"a": {}, "c": {}, "v": {}};
21 //
22 this._from_ui_a = function () {
23 let target = _this.settings["a"];
24 //
25 (function () {
26 target["aired_thr"] = implhelper.build_yyyymm_range_from_ui(
27 $("#aired-year-min-thr").val(),
28 $("#aired-month-min-thr").val(),
29 $("#aired-year-max-thr").val(),
30 $("#aired-month-max-thr").val(),
31 "-");
32 target["prem_thr"] = implhelper.build_yyyymm_range_from_ui(
33 $("#prem-year-min-thr").val(),
34 $("#prem-season-min-thr").val(),
35 $("#prem-year-max-thr").val(),
36 $("#prem-season-max-thr").val(),
37 "");
38 }());
39 //
40 (function () {
41 let animetypesincl =
42 $("#animetypes-incl-or-excl-incl").is(":checked");
43 target["animetypesincl"] = animetypesincl;
44 let animetypes_sel =
45 animetypesincl ? "#animetypes-incl" : "#animetypes-excl";
46 target["animetypes"] = _csv_split(animetypes_sel);
47 }());
48 //
49 (function () {
50 let genresincl = $("#genres-incl-or-excl-incl").is(":checked");
51 target["genresincl"] = genresincl;
52 let genres_sel = genresincl ? "#genres-incl" : "#genres-excl";
53 target["genres"] = _csv_split(genres_sel, function (e) {
54 let k = Object.keys(_datmng.master["gnr"]).find(
55 (key) => (key.toLowerCase() === e.trim().toLowerCase()));
56 return _datmng.master["gnr"][k];
57 });
58 }());
59 //
60 (function () {
61 let producersincl =
62 $("#producers-incl-or-excl-incl").is(":checked");
63 target["producersincl"] = producersincl;
64 let producers_sel =
65 producersincl ? "#producers-incl" : "#producers-excl";
66 target["producers"] = _csv_split(producers_sel, function (e) {
67 return _datmng._urlbuilder.malid2id(e.trim());
68 });
69 }());
70 //
71 (function () {
72 target["popularity-thr"] =
73 _to_valarray("#popularity-min-thr, #popularity-max-thr");
74 target["ranked-thr"] =
75 _to_valarray("#ranked-min-thr, #ranked-max-thr");
76 target["episodes-thr"] =
77 _to_valarray("#episodes-min-thr, #episodes-max-thr");
78 target["duration-thr"] =
79 _to_valarray("#duration-min-thr, #duration-max-thr");
80 target["favorites-thr"] =
81 _to_valarray("#anime-favorites-min-thr, #anime-favorites-max-thr");
82 }());
83 //
84 (function () {
85 target["rating"] = [
86 "#rating-g",
87 "#rating-pg",
88 "#rating-pg13",
89 "#rating-r",
90 "#rating-rp",
91 "#rating-rx",
92 "#rating-none",
93 ].filter(function (sel) {
94 return $(sel).is(":checked");
95 }).map(function (sel) {
96 return $(sel).val();
97 });
98 }());
99 };
100 //
101 this._from_ui_c = function () {
102 let target = _this.settings["c"];
103 //
104 target["chara-ms"] = $("#chara-ms").val();
105 //
106 target["favorites-thr"] =
107 _to_valarray(
108 "#chara-favorites-min-thr, #chara-favorites-max-thr");
109 };
110 //
111 this._from_ui_v = function () {
112 let target = _this.settings["v"];
113 //
114 target["bloodtypes"] = _csv_split("#cv-bloodtypes");
115 //
116 let nativesincl = $("#natives-incl-or-excl-incl").is(":checked");
117 target["nativesincl"] = nativesincl;
118 let natives_sel = nativesincl ? "#natives-incl" : "#natives-excl";
119 target["natives"] = _csv_split(natives_sel);
120 //
121 target["birth-thr"] =
122 _to_valarray("#cv-birth-min-thr, #cv-birth-max-thr");
123 //
124 target["favorites-thr"] =
125 _to_valarray(
126 "#actor-favorites-min-thr, #actor-favorites-max-thr");
127 //
128 target["actor-acting-total-thr"] =
129 _to_valarray(
130 "#actor-acting-total-min-thr, #actor-acting-total-max-thr");
131 //
132 target["actor-acting-main-thr"] =
133 _to_valarray(
134 "#actor-acting-main-min-thr, #actor-acting-main-max-thr");
135 };
136 //
137 this.from_ui = function () {
138 _this._from_ui_a();
139 _this._from_ui_c();
140 _this._from_ui_v();
141 };
142 //
143 }
ESLint 設定をどうしてるかによるけどもともと 25 とかでエラーだったのが、IIFE することでエラーが消える、と。
さて、ここからが「誤解してもらっては困る」の本題話。
「リンターを黙らせるために IIFE するのは悪なのかどうか」の話。無論「少しは性能悪くなったりするかもね」てのもあるだろうから、「なんも考えず適用しようとするなら悪だ」ろう。けどそれもわかった上でそうするとして、それでも十分に悪か?
これでワタシが思い知らされた通り、function だけがスコープを作るので、つまり「IIFE をスコープを作るためだけに利用する」ことが出来るだろう。そして、「なんで名前付きの function を作らないことがあるのか」が「自明でない」ことはないだろう、「汎用にはなりえないスペシャルでローカルな機能まとまり」には名前はそう易々とは付けられないし、そうしたものは「名前を付けて独立させるとかえって見通しが悪くなって保守しづらくな」りがちなわけだ。(実際にワタシの例で、IIFE した全てにいちいち名前を付けて独立させることを想像してみるがいい。)
つまり、まずは「スコープ限定化による局所化になるので嬉しい」という動機付けになりつつ、「名前を付けるまでもないが意味的なまとまりにはなる」ということならば、「リンターを黙らせる」ということではない「動機となる」とこじつけることが出来ますわな。これって実際ワタシが C/C++/java で良くやってたこれ:
1 /* */ {
2 int x = 1;
3 int y = 2;
4 hoge1(x, y);
5 }
6 /* */ {
7 int x = 3;
8 int y = 4;
9 hoge2(x, y);
10 }
とノリは一緒なんだよね。(xUnit で特によく使う。)でも C/C++/java なリンターで「ブロック単位」というステートメント数計測に基づくお行儀チェックなんて発想はないけどね。
というわけでワタシは「こういった単純移送系ではリンターを黙らせたい」のために IIFE を多用しようかなと思う。そうすることで、「本当にコンパクトにしたい問題処理の検出」とそのリファインに集中出来るであろう、というわけだ。
ということなんだけどね。たださぁ…、このまさに「function 単位の計測」でのがまさしく仇になって、「本当は滅茶苦茶複雑で自分でも読むのがイヤになるようなもの」ほど案外このチェックに引っかからなかったりすんのがねぇ。なぜって、そういう「非常に複雑で読解困難」なものほど細かい function をたくさん作って呼び出してるから(Array の forEach だらけの処理がまさしく)。なので正直もうひとつ「トップレベルからの計測」も欲しいなぁ、なんてことは思ったりする。