この記事はAteam Brides Inc. Advent Calendar 2019 25日目の記事です。
13日目の投稿に続き、再び最近 Firebase によるサーバーレス構成に心酔中の @okoshi が担当いたします。
が、この記事はFirebaseでもサーバーレスの話でもありません。
プログラムを改修するときにに、自分の変更がどこまで影響するのか、どうやって調べていますか。
少しでも変更したらすべてのプログラムを見て調査しているのであればそれでもかまいません。しかし、サービスがスケールするに従ってプログラムも多くなってくると、時間的な制約も相まって、作業負荷がメンバーにのしかかるようになります。人をたくさん投入して解決する方法も考えられますが、人件費もかかるので、コストに見合っているかどうか注意が必要です。
そんなとき、変更がどこまで影響するのか、どこまで調査したらいいのかを考えて、やってみたらうまくいったことを公開しようと思います。
この影響範囲の調査方法は、数年前(前職)Webアプリケーションフレームワークのフロントエンドまわりのソフトウェアのアーキテクティングをしていた頃に私が実践していたことで、非常にうまく運用できていて、チームのメンバーのモチベーションも高かったと記憶しています。その頃は、なんとなく「こうすればうまくいく」と頭の中にあった程度で人に伝えることはあまり考えてなかったのですが、文章に落とし込みたいと思います。
本記事を読めばきっとこれからはクラス図をみながら、影響範囲の調査を行ってみようと思ってもらえるはずです。
前提
影響調査を行う前に、クラス単位のユニットテストが十分に終わっていることが前提です。テストコードで担保されているといいですね。
クラス図、活用していますか?
さて、やっと本題に進んでいきます。
みなさんはクラス図を作っていますでしょうか。「いいえ」という方が多いでしょう。
なぜかはなんとなくわかります。「頻繁に仕様がかわるのに、一々作っていられない」「それって納品するためのドキュメントでしょ?うちは内製だから」「作ったところでいつ見るの?」「クラス図って何?」という声が聞こえてきそうです。
いやいや、ちょっと待ってください。クラス図は改修の必要のある範囲を特定できる優秀なツールでもあるんですよ。
矢印を遡ったクラス全てが対象
まずは以下のクラス図をみてください。このクラス図はあらゆるクラス構成を表しているわけではありません。説明のために適当に作ったものですが、これから説明する内容には必要十分なものになっていると思います。
矢印を遡ったクラスすべてが改修範囲の調査対象です。
クラス図で使う矢印はいろいろなものがありますが、実装だろうが継承だろうがどれを使ったとしても矢印の方向を遡ったものが対象と考えてかまいません。
ClassCに変更を加えてみた場合
ClassCはClassAからのみ使用されています。改修が必要になるかもしれないクラスはClassAのみです。
赤色のクラスが影響範囲です。
ClassBに変更を加えてみた場合
ClassBはClassCの親クラスですね。
このケースでは改修が必要になるかもしれないクラスはClassCとClassAです。
赤色のクラスが影響範囲です。
ClassHに変更を加えてみた場合
ClassHはユーティリティークラスでしょうか。3つのクラスから使用されていますね。
このケースでは改修が必要になるかもしれないクラスはClassE、ClassF、ClassD、ClassB、ClassC、ClassAです。
赤色のクラスが影響範囲です。
なぜそう言えるのか
本ページではクラス図を取り上げていますが、わかりやすくするためにメソッドレベルの実装から説明させていただきます。
例えば以下のようなRubyのソースがあったとします。
class CalcA
def calculate(p1, p2)
p1 + p2
end
end
class CalcB
def increment(p)
calc_a = CalcA.new
calc_a.calculate(p, 1)
end
end
JavaScriptだったらこう。
class CalcA {
calculate(p1, p2) {
return p1 + p2
}
}
class CalcB {
increment(p) {
const calcA = new CalcA()
return calcA.calculate(p, 1)
}
}
Javaだったらこう。
public class CalcA {
public int calculate(int p1, int p2) {
return p1 + p2;
}
}
public class CalcB {
public int increment(int p) {
CalcA calcA = new CalcA();
return calcA.calculate(p, 1);
}
}
PHPだったらこう。
<?php
class CalcA
{
public function calculate($p1, $p2)
{
return $p1 + $p2;
}
}
class CalcB
{
public function increment($p1)
{
$calcA = new CalcA();
return $calcA->calculate($p1, 1);
}
}
Pythonだったらこう。
class CalcA():
def calculate(self, p1, p2):
return p1 + p2
class CalcB():
def increment(self, p):
calcA = CalcA()
return calcA.calculate(p, 1)
Swiftだったらこう。
class CalcA {
func calculate(_ p1: Int, _ p2: Int) -> Int {
return p1 + p2
}
}
class CalcB {
func increment(_ p: Int) -> Int {
let calcA = CalcA()
return calcA.calculate(p, 1)
}
}
そろそろクドいといわれそうなのでこれ以上はやめておきます。
CalcBクラスのincrementメソッドからCalcAクラスのcaluculateメソッドを実行しています。
ちょっと考えてみよう
ここで考えてみてほしいことがあります。
-
(A) CalcAクラスのcaluclateメソッドを修正したケース
CalcAクラスのcalculateメソッドのp1 + p2をp1 * p2に変更したら、どこまでのプログラムをテストしますか。
-
(B) CalcBクラスのincrementメソッドを修正したケース
CalcBクラスのincrementメソッドのcalc_a.calculate(p1, 1)部分をcalc_a.calculate(p1, 2)にしたら、どこまでのプログラムをテストしますか。
(A)のケースではClassBが使用しているClassAクラスのcalculateメソッドの振る舞いが変わってしまっています。ClassBクラスのincrementクラスはClassAクラスのcalculateメソッドで第1引数の変数に第2引数の1が足し込まれた値が返ってくることを期待していたのでincrementというメソッド名になっていたのに、期待した結果を返さなくなり不具合になってしまいました。
これは、CalcAへの変更がCalcBに対して影響があったといえます。
つまり、本記事の通りに調査をしていれば、CalcAを改修するときは矢印を遡ってCalcBにも影響がある可能性があることが認識でき、あらかじめ調査できたことになるので、不具合を回避できていたかもしれません。
一方で(B)のケースではCalcBがCalcAの使い方を変えただけなので、CalcAを調査する必要がありません。
改めて本記事で説明している影響範囲の調査とは
え?、もしかしたら(B)のケースのように第2引数に2を与えていたら思いもよらなかった不具合が起きていたかもしれない?
そうですね。その恐れもありますね。
しかし、それはCalcAの問題であり、CalcBに変更を加えたことにより発生した不具合ではありません。以前にCalcAを作成したときか修正したときに見落とした不具合かと思われます。
この記事でいう影響範囲の調査とは、クラス単体では期待通りには動くけど、クラス同士を組み合わせて動かしたときに発生する不具合を探しています。そのため、クラス単体では不具合が起きない状態であることが前提です。
先述の前提条件にも書いた通り、クラス単位のユニットテストが十分に終わっていることが前提です。
外部フレームワークや外部ライブラリーとの関係はどう考えるのか
作ったクラスは、フレームワークから呼び出されるようになっていることが少なくありません。
例えば、executeというメソッドを定義しておけば、フレームワークがexecuteメソッドを呼び出してくれるというようにです。
ということは「外部フレームワークから呼び出されるということはこういうことではないのか」と思われるかもしれません。
「ということは、ClassAに変更が加わったとき、Frameworkに影響があるかもしれないということなのか?」という意見がでてきそうです。
でも安心してください。そこまで面倒をみる必要はありません。
それはなぜか。
外部と内部にはインターフェースという境界があると考える
外部のプログラム(フレームワークやライブラリー)と内部のプログラム(自前のプログラム)をつなぐものはなんでしょうか。インターフェースです。ここでいうインターフェースはJavaやTypeScriptのように明確にinterfaceキーワードが言語仕様として定義されているもののことではありません。
誤解を恐れずに言えば、外部に公開している使い方をここではインターフェースと呼んでいます。
例えばinterfaceキーワードが言語仕様にないRubyであればRDocやYardで書かれた仕様もここではインターフェースに含みます。
何が言いたいのかというと、外部のプログラムと内部のプログラムは、このインターフェースに従って作らなければ正しく動かないので外部のプログラムと内部のプログラムはインターフェースによる縛りがあります。
内部のプログラムは、クラス同士のインターフェースを如何様にもできるので、インターフェースに縛られているとはいえません。
つまり、言語仕様にinterfaceキーワードがなかったとしても、外部プログラムと内部プログラムの間にはインターフェースが存在するクラス図になると考えます。
ただし、実態に合わせたクラス図を作ると、こうはならないと思うので、ここで言いたいことは外部のプログラムと内部のプログラムを組み合わせたとき、影響範囲の調査の対象とは考えないよということです。
ただし、詳しく調査しても不具合が見つからないときはフレームワークやライブラリーを疑う時はあります。でもそれは自分で作成したプログラムが正しいことが証明された後の段階になります。
ここで説明したかったことをもう一つ。
インターフェースを設けると依存関係が逆になる
クラスとクラスの間にインターフェースを設けると矢印が逆になるので、調査範囲の広がりを制御することができ、複雑化を避けられるようになります。
言語仕様としてinterfaceキーワードを持つ言語において、インターフェースには実装を持たないのでインターフェースに矢印が向かっていたとしても実装は別のクラスに定義されます。そのため実装処理の変化が使用しているクラスに対して及ぼす影響はなくなるのです。
上記クラス図では、同じクラス構成であってもインターフェースがあることで、ClassAの実装の変更がClassBには影響しないと考えられます。
ではInterfaceAに変更があったらどうなるのか。同じ考え方ができます。上記のケースではClassAとClassBに影響があると考えられます。
線の種類ってほかにもあるのでは?
本記事のクラス図には関連、コンポジション、集約がでてきていません。
矢印を遡って対象を探せといっているので、これらの線は都合が悪いため省いています。
では、それらの線を使ったクラス図を見るときはどう考えたらいいのかというと、関連は双方向に矢印がある扱いにしてください。
コンポジションや集約は菱形のある部分を矢印の出発点として、線しかない方を矢にあたる部分として読み替えてください。
複雑なクラスは負の連鎖を招く
本記事で説明している複雑なクラスとは、クラス内に閉じたメソッドが大量にあったり、依存関係が非常に多いもののことです。
クラス内に閉じたメソッド(いわゆるprivateメソッドのこと)が大量にあると、処理があっちこっち移って、goto文がもたらした問題と同じような状態になりますし、依存関係が多ければ改修に伴う影響が不具合を誘発しやすくなります。
メソッド(関数)の処理内容も複雑性を挙げる要因になります。一つのメソッドが非常に長ければ、ある部分の修正がメソッドないのどこに影響するのかを調べなければいけません。メソッドが短くてもネストが深かったりすると、あらゆる条件を満たすケースを洗い出して調査しなければならず事実上不可能な状態に陥りかねません。
※メソッドの複雑性に関してはMcCabeの循環的複雑度等によって数値化することで、それをレビュー前の確認事項とすることで、複雑化を回避する運用が現実的だと思います。
最も注意しなければならないのは、プログラムを作り始めた頃に、複雑化の兆候が見られたのにも関わらず、その時点では問題ないから今後保守してくれる人に任せようと判断することです。その保守してくれる人は、「このプログラムをOKとしたならオレもいいよな」という思考になるのではないでしょうか。はい、多分そうなります。複雑化してきたプログラムに対してなぜそうなっているのかをレビューアに聞かれたらgitのdiffを見せながら「前の人がやった。そのときレビューアが通したのだから十分な検証があったからだ」と説明して納得させるかもしれません。(私はそういう現場を何度も見たので、十分にあり得ることだとおもいます)
負の連鎖です。
運用がはじまると、最速で機能をリリースしたいものですし、それを要求されます。そうなると、動いているものを必要以上に直そうとはしないので、いつまでも改修はされません。
だから、最初につくった人が複雑化しない構成にすることを意識しておくことが大切なのです。
影響範囲を意識したプログラミングは良循環を生む
ちょっと本題とは脱線しますが、ここまでの解説が頭に入っていると、影響範囲を意識したプログラミングができるようになりますし、よいプログラムが書けるようになると思っています。
なぜなら、プログラムの改修に伴う影響範囲をできるだけ小さくして、自分の負荷を下げようとするからです。
プログラムの単位を小さくし、見通しのよいプログラムを作ることが解決策だと意識するようになるということです。
自分の負荷が下がるということは、チームの負荷も下がるというメリットもあります。変更が簡単になればよりよい仕組みを入れられますし、新しいことにチャレンジできるということからチームのモチベーションもアップします。
こんな良循環が見込めます。
ふりかえり
クラス図を使って影響範囲を調査するようにしたら、結果的にチームメンバーのモチベーションが向上したという話ですが、どんなプロジェクトにおいてもクラス図を使って影響範囲を調査するようにしたら同じような効果が得られるとは限りません。
そもそもなんでクラス図を使って影響範囲を調査するようにしてみたのかといえば、思い返せば、影響範囲の調査をどうしたらいいのかわからないという意見や、時間をかけて調査していたことが課題感としてあったので、なんらかの道を示してあげれば解決するだろうと考えていました。そこでたまたま目についたクラス図を使ってみようとチームに提案したのがきっかけです。
それが運良くチームにフィットしたというわけです。
さて、ここで衝撃的(本当か?)なことを明かしますが、私はエイチームブライズのアドベントカレンダーに投稿しているにもかかわらず、エイチームブライズの社員ではありません。エイチームの社員(2019年末現在)です。
現在私はエンジニアが一人のチームにいるため、作業をするにはマンパワーが足りませんでした。しかしグループ会社のいろんな方の助けを得てプロジェクトを進めることができました。その中でも特にお世話になったのがエイチームブライズです。
エイチームのグループ会社では、組織を超えてお互いを助け合い支え合う文化があるので、超絶スーパーエンジニアでなくても新しいことに挑戦させてもらうことができます。
そんな私たちのチームで働きませんか?
エイチームは、インターネットを使った多様な技術を駆使し、幅広いビジネスの領域に挑戦し続ける名古屋の総合IT企業です。
そのグループ会社である株式会社エイチームブライズでは、一緒に働く仲間を募集しています!
上記求人をご覧いただき、少しでも興味を持っていただけた方は、まずはチャットでざっくばらんに話をしましょう。
技術的な話だけでなく、私たちが大切にしていることや、お任せしたいお仕事についてなどを詳しくお伝えいたします!
Qiita Jobsよりメッセージお待ちしております!