今までいくつかシステムの設計〜保守までやってきた中での失敗などを元に、 システム開発のあり方をまとめてみました。
概要
愚者は経験から学び、賢者は先人から学ぶと言いますが、 僕は残念ながら経験するまでは理解しようとしなかったので、 せめて経験から学ぶことと、今後同じ事態に陥る人が減るようにまとめておきます。
現時点までの個人の実体験を踏まえての考察なため、全く的外れだったり当たり前すぎて呆れるかもしれませんし、 既にアジャイルやXP、リーン開発などで語り尽くされた内容かもしれません。
そのあたりは後日改善していきたいと思っています。
前提条件
なお前提として以下二点を挙げます。
ユーザーのニーズが事前に定義できないこと。
もし事前に仕様を明確に定義できる場合は、保守などは考えずにそのニーズに合うものを作れば終わりです。 多少の優先度付けは必要かもしれませんが、そんな単純なケースであれば、どう開発してもなんとかなりそうです。
明確な定義は避けますが、いわゆるウォーターフォール的な長期のイテレーションでの開発を対象外としています。 というか最近のソフトウェア開発では、規模やリリース時期に関わらず内部的には短期のイテレーションを回すのが最善だと思っています。評価フェーズをどこまで徹底するかは置いておいて、開発のフローとして。 理由は以下の3つです。
- 単体の複雑度が増していて、長期のイテレーションは大抵人の理解が追いつかないだろうこと。
- システム内で責務をモジュール毎に分けて連携していくのが常識な中で、長期のイテレーションでは連携が遅れてしまうこと。
- プログラミングの抽象度が上がったことで、短期の繰り返しによるコードの書き直しのコストがかなり低減されていると思うこと。
既存システムが存在し、目的が似ている場合
ある程度似た目的のシステムが既にある場合は、そこをベースとして開発していくのが近道でしょう。 (フォークして開発するとか、プラグインを作るとか)
これが出来ない以下のようなケースの場合、新しい目的に向けた別の手段(新システムを0から作るなど)を取ることになるでしょう。
- 特許やライセンスの都合上利用できない
- サービスはあるが実装が公開されていない
- そもそも市場やOSSに似た目的のシステムがない。
その場合は、アルファ版を作った後は今回の対象範囲となります。
コードが再利用できないことが明白な場合
例えば以下のようなケースです。
- Javaの実装は既にあるが、性能向上のためCで書き直したい。
- rubyで書かれている便利なライブラリをJavaにも移植したい。
- RDB前提ではなくNoSQL前提にしたいので、処理単位やUIが大きく変わる。
その場合は、流用できるロジックは参考にしつつ、しかし既存のものとかなり違う中身になることでしょう。 ここでも、アルファ版を作った後は今回の対象範囲となります。
ポイント
ソフトウェア開発にあたって重要だと考えている4つのポイントは以下です。
- そのシステム(プロジェクト)の目指す方向性はどこなのかを明確にする。
- 機能追加は、必ず満たしたいニーズだけにフォーカスする。
- 機能開発と既存設計の変更は同時には行わない。
- 既存コードの変更はチーム全員が理解できる単位まで細かくする。
そのシステム(プロジェクト)の目指す方向性はどこなのかを明確にする。
プロジェクトとして何を目指すのか?どこまでを範囲とするのか?といったことを、実装を抜きにしてチーム内外と共有することで、
- 類似のシステムが既にないか調べることができ、車輪の再発明を防げる。
- プロジェクトの領域が明確になり、ユーザーのニーズとのミスマッチや理不尽な要求が減る。
- 各作業に優先順位を付ける際の指標となり、開発の効率が上がる。
どこまで具体的に突き詰めるかは、上記の目的が果たせるかどうかで判断する。
なお、方向性はなるべく変わらないことが望ましいが、チーム内では定期的に再定義すると良いと思う。
機能拡張は、最も解決したい問題だけにフォーカスする。
設計段階では、ユーザーのニーズを調べているうちに頭の中でおおまかな設計が出来上がってきて、色々な非機能要件も組み込んでいたりする。 そして、いざ仕様を策定する段階に入ると、無意識にその設計に引きずられてしまい、本来であれば必要のない箇所まで作りこんでしまう。 そうなると、
- 他人に説明するときに、結論ありきになり段階を飛ばして説明してしまい理解されない
- 全部作ろうとして工数や優先順位がメチャクチャになる。
- 他の案をシャットアウトした独りよがりな実装プランになる。
それを防ぐために、直近で問題になっていることやユーザーのニーズの特に重要なもののみにフォーカスして、その解決方法のみをアドホックに考えることが重要だと考えます。 その仕様がちゃんと理解されたら、さっさと作ってしまい再度ユーザー・社内に共有する。 すると網羅できていなかったパターンや新たな要望が出てくるので、それを繰り返し潰していく。
ただ、そういった細かなリリースやニーズの調査が難しいケースもあるでしょう。 その場合でも、複数の要望を聞いた上で、その要求を細かく再定義し足りないところは推測して補いリスト化し、問題ないか社内外にチェックしてもらい、問題なければ優先度の高いものからひとつずつ解決していきます。 こうすることで、リリース時に全部間に合えば問題ないし、間に合わなくても全部リバートされることはなく出来ている範囲で利用してもらえて、残りは次回に回すことが出来ます。
機能拡張と既存コードの変更は同時には行わない
機能拡張を設計・コーディングするときに、
- 既存のコードのこのAPIをこう直せば新機能で使えそうだな
- 機能拡張を含めてデータ構造を考えると、今のデータ構造はイケてないから直したい
などという考えが出てきます。 それ自体を考えることは必要だと思いますが、機能拡張時に同時並行でそれをやると、無理に構造を変えてバグを生んだり、実装が間に合わなかったり要求とズレていた時にリバートしたら手戻りが増えてしまったりします。
そこで、機能拡張と既存コードのリファクタリングをはっきり分け、
- 機能拡張は既存のコードに一切修正せずにコードを書く。
- 既存コードのリファクタリングは、外から見た挙動を一切変えずに内部を変える。(deprecatedや不要なため削除はアリ)
とします。 こうすることで上記の問題がなくなると共に、
- 機能拡張時は既存コードとの一貫性やコードの綺麗さを意識せずにガッと作れる。(むしろこの時点でデータ構造の最適化とか考えるのは早すぎる。)
- 機能拡張のたびに既存コードの変更は必ず発生するので、リファクタリングやテストの意識が徹底される。
- テストコードの寿命が伸びるかも。
といったことが見込めると考えます。
既存コードの変更はチーム全員が理解できる単位まで細かくする。
バグだらけでまともに動かない・ユーザーが誰も利用していない、ものは例外として、 とりあえず動いていてユーザーに利用されているものは、どんなコードでも最大限の敬意を払うべきです。
ちょっと見た時点だと、仕様がわかりにくいとか実装がクソだとか思って0から書き直したくなりますが、そんな無茶をすると
- どこかで辻褄が合わずバグに繋がる
- チームの人が差分を見ても問題ないのか判断がつかない
- 現状の挙動が少しでも変わると、ユーザーを混乱させかねない。(また、デグレはすごく評価が下がる。)
ということで慎重に行う必要があります。 この問題において、僕はチームの人全員が差分を理解でき、システムが提供する挙動に問題がないことが重要だと考えています。ここでいうチーム全員には非エンジニアが含まれる場合もあります。
まとめ
とにかく、コードの更新は誰もが理解できる粒度まで小さくすることが最重要で、データ構造やアルゴリズム、コードの最適化は二の次かなと思っています。 機能拡張であればその機能を満たす単位、既存コード変更であれば差分がわかる範囲です。
また、限られたリソースの中で最大限のメリットを提供するためには、プロジェクトとしての優先順位をきちんとつけなくてはいけないですよね、という当たり前の話もしました。