2017年12月23日

スパゲッティとライフサイクルとドメイン

この記事は、〇〇勉強してみた Advent Calendar 2017の23日目(12/23)の記事だよ。
次の記事はtosssaurusさんの『canvasを1ヶ月勉強してみた』みたい。

☆☆☆スパゲッティコードとの出会い☆☆☆

最近は、Javaじゃなくて、C#とかTypeScriptとか使ってるんだよね。
ほら、最近のアプリケーションって非同期処理が必須じゃない?
そうすると、スパゲッティなコードを割と自然に解消できるasync/awaitってすごいなぁって思うんだよね。

と言うか、ここのところ、いろんな人のコードをいっぱい見る機会があって、
async/awaitじゃなくても、『そういった構成』にしてないからスパゲッティになっているってコードを結構見かけて、
スパゲッティなコードってこういう風に作られていくんだなぁって思ったから、まとめてみようかなって。

Wikipediaとかみても、

・goto文の濫用
・多重継承の濫用
・スコープの拡大
・技術不足

とかしか書いてないし、
これって、特に最後の技術不足って、わかりません。って書いてあるようにしか思えないしね。
少しでも、スパゲッティなコードの理解と回避につながればいいなって思うの。

☆☆☆スパゲッティの概念的な定義☆☆☆

いつものことだけど、わたしの勝手な考えだから、鵜呑みにしておかしなことになっても知らないよ!

ってことで、いきなりわたしの答えから書いちゃうと、
次の2種類の書き方をしているプログラムをスパゲッティって言うんだと思う。
A. ライフサイクルに関する処理をライフサイクルの層から外してプログラムを構成する。
B. ドメイン重視のコンポーネント単位ではなく、アクション(1連の処理の流れ)としてプログラムを構成する。

ちなみに、これはだめな書き方だから、じゃぁ、どう書けば良いの?って観点で行くと、
『ライフサイクルやドメインにそった開発をする』っていうことかな。
そういう意味では、DDDな書き方している人はスパゲッティなコード書くことないのかもしれないね。

まぁ、どう書くべきか、みたいなのは、時間とともに変わっていくと思うけど。

☆☆☆スパゲッティの具体な成長例☆☆☆

じゃ、わかりやすい例を。
例だから、現実的かどうかとかは大目に見てもらえると助かる感じ?
わかりやすさ重視で!

一番わかりやすいのはこう言うのかな……。
『画面のボタンを押して、処理が終わるまで押せなくする』みたいな。
あんまりしない気もするけど例だから気にしないっ。

1.処理の配置間違えた。

private void Button_Click(object sender, RoutedEventArgs e) {
this.Button.IsEnabled = false;
this.doSomething();
}

private void doSomething() {
// なが〜いなが〜い処理。
Thread.Sleep(2000);
this.Button.IsEnabled = true;
}

あ、えっと、一応、UIにボタン(Button)があって、押すとButton_Clickが呼ばれるってことで。
ここの例は、VisualStudioでWPFのプロジェクトとテンプレで作って、MainWindowにButtonを配置してイベントを作っただけだよ。

さて、こんなコード書かないよ!って?いや、まぁ、普通はそうだとわたしも思うんだけど。
と言うか、そもそもボタンの切り替え動かないし……っていうのは置いといて。
特に、メソッドを切り出すような処理がなければ、こういう書き方することはあり得ないね。

これは、定義のAになっちゃっているのがよくわかると思うんだよね。
切り替えている処理が別々のメソッドに分散しちゃってるから。

じゃ、次はもうちょっと、書きそうな例ってことでちゃんとボタンが切り替わるように非同期にするよ。

2.非同期にした(非同期場所間違えた)。

private void Button_Click(object sender, RoutedEventArgs e) {
this.Button.IsEnabled = false;
this.doSomething();
}

private void doSomething() {
// なが〜いなが〜い非同期処理。
Task.Run(() => {
Thread.Sleep(2000);
this.Dispatcher.Invoke(() => {
this.Button.IsEnabled = true;
});
});
}

このくらいになると、書く人がちょっとだけいそうな気がする。

え?あ、うん、いや、まぁ、元の問題と対応があってないのはわかってるんだけど、
この流れだと、こう考えて書く人多そうだなってことでよろしく!
JavaScriptなWebだと、元に戻すのにわざわざDispatcherとかの行はいらないから、シンプルになって、余計に書く人多そう。

#1で書かないって人は、もちろん、#2も書かないと思うんだけど。
じゃぁ、次でやっとUIの処理まとめる気になったことにする。
実際は、doSomethingとかがさらに複雑になるとか。呼び出し元の処理が複雑になると気づくはずだよね。

3.UIの記述場所をまとめたつもり。

private void Button_Click(object sender, RoutedEventArgs e) {
this.Button.IsEnabled = false;
this.doSomething(() => {
this.Button.IsEnabled = true;
});
}

private void doSomething(Action callback) {
// なが〜いなが〜い非同期処理。
Task.Run(() => {
Thread.Sleep(2000);
this.Dispatcher.Invoke(callback);
});
}

さて、これは、さらに書く人多そうな形だと思うんだよね。
コールバックっていうんだっけ。なんかすごく好きな人がいるみたいだよね。
わたしは、あまり使うの好きくないんだけど。

でも、doSomethingのメソッドがUIでの後処理を意識したメソッドになってしまっているね。
もうちょっと、ちゃんとUIを寄せてみようかな。

4.コールバックでUIと分ける。

private void Button_Click(object sender, RoutedEventArgs e) {
this.Button.IsEnabled = false;
this.doSomething(() => {
this.Dispatcher.Invoke(() => {
this.Button.IsEnabled = true;
});
});
}

private void doSomething(Action callback) {
// なが〜いなが〜い非同期処理。
Task.Run(() => {
Thread.Sleep(2000);
callback();
});
}

うん、まぁ、何となく良くなった感じ?
じゃぁ、ここで、なが〜いなが〜い処理、ずっと適当にSleepとか書いてきたんだけど、
まぁ、普通は通信処理とかになったりするわけで、当然例外とかあり得るわけだよね。
そうしたらどうなるかなぁって。

はっ、#2でおかしい対応していたことがばれてしまう!?(いまさら

5.コールバックでUIと分ける。

private void Button_Click(object sender, RoutedEventArgs e) {
this.Button.IsEnabled = false;
this.doSomething(() => {
this.doSomething(
() => {
this.Dispatcher.Invoke(() => {
this.Button.IsEnabled = true;
});
},
(message) => {
this.Dispatcher.Invoke(() => {
MessageBox.Show(message);
});
}
);
});
}

private void doSomething(Action callback, Action errorHandler) {
// なが〜いなが〜い非同期処理。
Task.Run(() => {
try {
Thread.Sleep(2000);
// throw new Exception("hoge");
} catch(Exception ex) {
errorHandler(ex.Message);
} finally {
callback();
}
});
}

もうすでに、これはない……って感じになってきているわけだけど、
たぶん、少しずつ『処理を追加していってる』とか、『全体の構成じゃなくて一部だけ見直している』とかだと、
こうなっちゃう人、割といるんじゃないかなぁ?
ここでは、finallyにcallback置いたけど、finallyじゃなければ、Button_Clickの実装はまた変わってくるよね。
doSomethingの作りに非常に強く影響受けている感じ。
ほんと、ちょっとした変更も一蓮托生で、動き全部把握しておかないと即死って感じ?(物騒

まぁ、もうこのくらいでいいかな。
というわけで(?)、こういう風にコードが成長していくとスパゲッティになっていくって言うのは伝わったかなって。

じゃぁ、どうすればいいのかっていうと、次のような感じ。
まずは、Taskを戻すよ。

6.制御を戻す。

private void Button_Click(object sender, RoutedEventArgs e) {
this.Button.IsEnabled = false;
Task task = this.doSomething();
Task wait = Task.Run(() => {
try {
task.Wait();
} catch(AggregateException exs) {
this.Dispatcher.Invoke(() => {
exs.Handle((ex) => {
MessageBox.Show(ex.Message);
return true;
});
});
} finally {
this.Dispatcher.Invoke(() => {
this.Button.IsEnabled = true;
});
}
});
}

private Task doSomething() {
// なが〜いなが〜い非同期処理。
Task task = Task.Run(() => {
Thread.Sleep(2000);
// throw new Exception("hoge");
});
return task;
}

スパゲッティになった状態ではっきりわかる最大の問題は、
『非同期の制御を元に戻していない』ことだから、そこを直した感じ。

Taskを作ってる箇所が複数になって、もう#2あたりでのこともはっきり目に見えちゃうね。

じゃ、ちょっと、見にくいから、async/await使うよ。

7.async/awaitで。

private async void Button_Click(object sender, RoutedEventArgs e) {
this.Button.IsEnabled = false;
try {
await this.doSomething();
} catch(Exception ex) {
MessageBox.Show(ex.Message);
} finally {
this.Button.IsEnabled = true;
}
}

private Task doSomething() {
// なが〜いなが〜い非同期処理。
Task task = Task.Run(() => {
Thread.Sleep(2000);
// throw new Exception("hoge");
});
return task;
}

どうかな〜。だいぶわかりやすくなったかな〜。
async/awaitを使うと、非同期関係のところは、正しいライフサイクルに導いてくれる気がするんだよね。
補助してくれるというか。

まぁ、別にasync/await使おうってことじゃなくて、
非同期はTaskを戻すのがライフサイクルとかドメインを考える上で重要ってこと。
そして、ライフサイクルとかドメインを考えないとスパゲッティになっていくってこと。

どんどん処理を追加していくんじゃなくて、
ちゃんと、ライフサイクルとかドメインを考えて構成しないと、スパゲッティになるよってことっ。

たぶん、スパゲッティコードが生まれる理由は、
コンポーネント単位でプログラムを書かないで、
アクション単位でコードをがんがん追加しちゃうのが原因じゃないかな。
言葉の定義怪しいけど。

今回も、Button_ClickにUIに関する制御&表現を、doSomethingに実際に実行&実現したいことを、って、
分けていれば、最初から#6とか#7の方向に進んでいくはずだしね。

ただ、ライフサイクルは、割と簡単だけど、
実際には、ドメインにそって適宜?分割していくのはすごく難しいと思う。

個人で開発していると、ドメインが1個に見えてきちゃうし、
チームで開発していると、ドメインの内部の関係性を把握しにくい感じがする。

☆☆☆スパゲッティを探す☆☆☆

さて、視点をちょっと変えて、すでにスパゲッティになっているのをどう見つけるか、ちょっと考えてみる。
・引数を、使わずに次のメソッドにただ渡している。
 使われているところから発生源を追うんじゃなくて、変数がnewとかされてから、いったいどこまでそれが持ち運ばれていくかってみていく。
 そうすると、だいたい、さっきみたいにUIの最後の処理をするためーとかで、UIそのものをドメイン側の処理に横流ししているとか見つけることが……ある?
 まぁ、あったのっ。すっごく驚いたけど。
・フィールド経由でたまたまそこにある呼び出し先になるメソッドに値を渡す。
 たまに見かけたコードだと、ドメインのメソッド、さっきの例だとdoSomethingは別のクラスとかに分けたりするわけだけど、
 省略のために規模が小さければクラス分けないこともあると思うんだよね。
 と言うか、最初は小さいから分けないことも多いと思うの。

 で、引数で渡すのはおかしい!みたいなことをすると、フィールド経由で渡したりして……、もちろん、これもアウトだよね。
 あとで、クラス分けるとき困っちゃう。

 他のケースだと、全く別のメソッド呼び出していって、
 コールバックで戻ってきたときに使うから、と言う理由で、フィールドに保存しておくみたいな考えっぽいもの。
 これも結構すごかった。

 かければ何でもいいとはわたしは思わないのだ。

・クラスに関係ないインターフェースを実装している。
 これも結構あるかな。さっきの引数で持ち回るとき用でやっていたりするコードを見かけることがあるよ。
 たぶん、ラムダ式とかがわからなかったり、クラスの責任とかを考えなかったり、っていうことだと思うけど。
 かるく、同じアプリケーションコードの中でVisitorパターン発生してるよねきっと。

 インターフェースをクラスに実装している例がいっぱいネット上に転がっているからなのかな?関係ない?

・Task、Future、Promiseを作って、returnしていない(コントロールが手放されている)。
 定番でわかりやすいよね!例としても使わせてもらったし!
 ちなみに、UIとかのもともと戻り値の型がvoidなところで保持する必要がある場合は、
 フィールドに維持して管理するしかない……よね。

☆☆☆まとめ☆☆☆

長々書いてきたけど、結局、最初に書いた定義のAとBによってスパゲッティはできていくと思うの。
ここまで来たら、定義のAとBの違いは、もうわかっちゃっていると思うんだけど、
プラットフォームとかシステムに依存した仕組みとか制限によるものか、
アプリケーションのドメインによる表現とかによるものかの違いかな。

Aがわかるけど、Bがわからないひとは、BをAに強引に当てはめて恐ろしいスパゲッティを作ることが多い気がする。
両方わからない人はいろいろ警戒するから、そこまで元々巨大なスパゲッティは積極的には作らないかな。

つまり、自分がいったいどんなドメインを扱っているのかしっかりとらえるのが重要ってこと。
プログラムは人によって違う書き方になるっていうけど、わたしはあまりそう思わない。

ドメインの設計は人によって結構大きく変わるけど、そこが落ち着いたら、
それを表現する最適なコードは割と誰が書いても同じになると思っているよ。
まぁ、設計っていうか構成を考えるのとプログラムを一緒にすると、人によって違うっていえるかもだけど。

posted by すふぃあ at 11:35| Comment(0) | TrackBack(0) | プログラム

2017年12月02日

コメントの書き方〜すふぃあ流コメント術〜

☆☆☆はじめに☆☆☆

この記事は、個人開発 Advent Calendar 2017の2日目(12/02)の記事だよ。
前の記事は、kappy0322さんの『個人開発における開発プロセスを公開してみる (2017年冬)』だよ。
次の記事はturanukimaruさんの『ファイアーエムブレムヒーローズの戦闘結果計算ツールをKotlinでDDD的に作ってみた』みたい。

☆☆☆経緯とか☆☆☆

kappy0322さんから、Advent Calendarの招待を受けて、あれ、なんで招待されたのかな〜、とか戸惑いながら、
そういえば、前から書こうかなーって思っていた、コメントの書き方、この際だから書こうかな!と思い立って書くことにしたの。
ここで言うコメントって言うのは、ドキュメントコメントではないコメントのことね。

わたしは、プロの教育とか受けてないから、コメントの書き方も、当然、自己流なんだけど、
最近、いろんな人のソースコードを見る機会があって、
『何でこんなコメント書いてるの?』って思うのをいっぱい見てびっくりしたんだよね。たぶん、プロの人のも結構あったと思うんだけど……。

☆☆☆コメントの書き方の問題☆☆☆

で、どうも教育を受けたことがある人に聞くと、
『そのコードを書いた理由をコメントで書きなさい』
って、教えられているみたい。

実際に、ググってみると、本当に、理由を書きなさいみたいなのがいっぱい出てくる。
うん、いや、そうなんだけどさ、なんでそんな、てけとーな教え方してるの……?って思っちゃった。
いや、だってさ、こういうコメント書かれても困るでしょ。

// データが複数あるのでfor文で回す。
for(int i = 0; i < elements.length; ++i) {
// 略
}
// 処理を分けるために分岐する。
if(something.getID() > 0) {
// 略
}

極端な例ではあるけど、教えられたとおりに、『理由』を書いているよね。
でも、良いコメントだね!……ってならないよね?
だから、『理由を書く』っていうのは不十分だと思うの。

って、こういうところから、何が問題かとか、分析していってもいいんだけど、
わかっている人にはわかっていることだろうし、あんまりおもしろくないから、
その辺は省略して、わたしがコメントを書くときの考え方を紹介していくよ。

別に、書くのがめんどくさくなってきたとかそういうわけじゃない……から、ね?

あと、何度も書くけど、わたしはそういう系の教育受けてないから、これが本当によい方法かどうかはわからない〜。
いろいろ比較とか研究とかできるわけでもないし。
ただ、個人開発で困っている人に、少しでも役に立てば良いな、とは思っているよ。

☆☆☆すふぃあ流コメント術☆☆☆

最大のポリシーは、
『あとで変更するときの判断基準を書く。』
ということ。

だって、システムとかアプリケーションを、作って〜リリースして〜書き換えて〜ってやっていると、
コメントが必要な時って、『そういうとき』だよね?

このポリシーから、コメントを書く、より具体的な方針は、、
『普通ではない書き方をしたところには、その根拠を示す。』
ということになると思っているよ。

コメントを書いていないところは、コードそのままの意図で、おかしな動きをしていたら、それはバグとして直されたり、最適化されたりする。
コメントがあるところは、その妥当性をコメントをみて判断してから、変更や修正ができるってわけ。

たとえば、

// このメソッドの定義より、要素の最後には、○○を含めて返す必要がある。
// ○○は、××の特性を含むため、△△の処理は行ってはならないので除外されるように条件を調整している。
for(int = 0; i < elements.length - 1; ++i) {
// 略
}
return elements;

まぁ、サンプルだから、もっと別のコード書きなよっていうのはあると思うんだけど、そこはそれ。
でも、こうすれば、なんで-1しているの??っていう理由と、それが妥当かどうかの判断ができるはず。
実は、この見解が間違っているとか、見解が変わったとかなら、よりどころとなっているコメントごと直すって感じ。

単に、『理由』ではなくて、変更するときの『判断基準』となることを書いていくのが重要ってこと。
ほんと、ただ、それだけだよ?みんなが知りたいことってそういうことじゃないのかな?

コメントが長くなるじゃん!って言うのもあるかもしれないけど、
必要なことは書くべきだと思うの。
くだらないことじゃないわけだからね。

かなり自分でも困ったソースコードの付近には、15行超えるようなコメント書いたこともあったかな。
まぁ、稀だけど。
読んで判断基準にならないコメントなんて、だからなんなの!って叫びたくなるだけじゃない?

☆☆☆弱点とか☆☆☆

この書き方の最大の弱点は、『普通』っていうのを共有しないといけないこと。
基本的には、必要十分なコードで、設計ポリシーにあっていれば問題は起きないはず。

裏を返すと、実力に大きな差があるチームだと、失敗すると思う。
まぁ、足手まといな人がいると、きっと別の意味で失敗するだろうから、大丈夫な気もするんだけどね。
その辺は想像だから、よくわかんない。

もう一つは、設計が微妙だとコメントがすっごく増えること。
だから、作っているときに気づいた、設計が微妙なところはすぐに直していかないとダメかも。
その設計がおかしいと言うコメントを大量に残すことになるからね。

って、この書き方じゃなくてもコメントが増えること自体は同じかもしれないけど。

☆☆☆よくありそうな質問と回答☆☆☆

Q. 難しいコードに説明コメントつけるのはどうなの?
A.
難しいって、普通じゃないってことでしょ?それは書き換える基準も含めてコメント書かないとだめじゃない?
まさか、難しいのが普通になって……ないよね?
もし、どこもかしこも難しくてコメントだらけになりそうって言うことなら、
それは設計がおかしいんだと思うよ。

Q. 必要なコメントが書かれていなかったら、困ったことになるんじゃないの?
A.
それは、つまり、書き換えるときに、必要な変更の判断基準が書かれてなくて、
さくさく書き換えた結果、間違った書き換えになっちゃうんじゃないか、って言うことだよね。

良いんじゃない?だって、前書いた人(過去の自分含む)さえ、それに気づいてなかったんでしょ?
気づけて良かったじゃない。
と言うだけだと思う。

てけとーに書かれていたのが、たまたま、動いていただけで、それが壊れるだけのこと。
壊れるのは、困るけど、それは、元を書いた人の問題だと思う。

このコメントの書き方をしていると、書き換える元にコメントを残さなかったことが悪いってことになんじゃないかな。
いや、まぁ、どっちが悪いとか言っても慰めにもならないんだけどさ。

なんか、どんなひどいコードでも、動いていれば正義みたいな考え方もあるみたいだけど、
それが『絶対』だとはわたしは思わない。もちろん、動いていることも大事だけどね。
動いていても、手抜きを許すわけじゃないし、ミスはありえることだと思う。

『過去のわたし!しっかりしろ!』って思うこと、よくある。
そこには、きっと、コメント以外の対策みたいなのが必要なんじゃないかと思ってみたりするよ。

Q. 書き直す予定がないときもコメント書くべきなの?
A.
書き直す予定がないって言うのは、バグが絶対にないってことかな。
それとも、バグがあったら、そのソースコードを全く参照しないで作り直すって言うことかな。
というわけで、そんなことは、まず、あり得ないと思うから、書いた方が良いと思う。

ただ、ちょっとしたスクリプトとかまで毎回書くかとかは知らないっ。
でも、やっぱり、あとでスクリプトを発掘すると、
あれ?これなんでこうなっているんだっけ?みたいなことは、わたしもよくあるけどね?(゜▽、゜

☆☆☆まとめ☆☆☆

さぁ、『理由』じゃなくて、『判断基準』をコメントに書いていってみようよ!
コメントを書くときに後の人のことを想像して考えることがやりやすくなるんじゃないかな。
わたしは、何にも責任とれないけどね!('-^*/

posted by すふぃあ at 12:10| Comment(0) | TrackBack(0) | プログラム