前置き
リーンスタートアップ&スクラム駆動で1年半アプリを継続開発した時
巷によくあるデザインパターンや考え方がイマイチしっくり来なかったので試行錯誤しました
そのまとめです
注意
この方法が合うかどうかは状況に依ります
用法用量を守り適切にお願いします
あと読んだあとに
「それただのオブジェクト指向じゃん?」とか
「それただのMVCじゃん?」とか
「それただのDDDじゃん?」とか言いたくなると思います
そこらへん分かってる人にはあまり価値が無いかもしれません
対象者
アジャイル、リーンスタートアップ、グロースハックなどの開発をガチで行う事になった人
機能レベルの仕様変更がバンバン入る状況に置かれている人
プロダクトオーナーとの仕様会議に出られる人(内部設計、できれば外部設計、要件定義までやってる人)
リーンスタートアップって何?
時間があったら青本読んでみてください
乱暴にまとめてしまうと
サービスを1回リリースしただけじゃキツイので、試行錯誤することを前提に
最小限(MVP)で作って継続的にリリースして分析して伸ばしましょう
みたいな考え方です
コーディングに喩えると
ビルド一回しか出来ないなんて無理、ちゃんとテストとデバッグしましょう
その際、ちゃんとこまめにビルドしましょう
みたいな感じです
当たり前ですね
高速で開発できればできるほどビルドチャンスが増えるので成功確率が上がると思ってください
リーン駆動やグロースハックみたいな開発では何が起こるか
新規開発や機能改修が終わると、普通なら保守フェーズに入ります
一回作った機能や画面はあまり消しませんよね?
リーンなどの場合は開発した後「やっぱこれダメだわ」と大胆に機能を消すことが結構あります
消さないまでも、半分以上の改修が入るなどということもあります
あくまで全ては実験というスタンスなのです
リーンとはっきり言わなくても、本気でプロダクトを成功させようとするとそういう感じになっていくかもしれません
※一度まとめた要件定義をぶち壊せる現場や、自社サービスなど
私の場合は毎日レベルで仕様変更が入る。というかこちらから提案してました
こんなのどうですか?みたいに
リーン開発で困ること
仕様変更が異常なペースで入ると、それに対応しなければなりません
※もし受託開発なら、それに付き合うかどうかは関係者と相談してください
ほいほい付き合って会社が得することはあまりありませんし、オススメしません
本件の要求分析(ゴール)
- 機能をサクッと削除したい
- 仕様をサクッと変更したい
- 事故りたくない
- 毎回フルで単体テストしたくない
- 仕様変更の見積もりはミーティング中に終わらせたい
- いっそミーティング中に仕様変更反映してしまいたい
- 速く!もっと速く!!
結論:壊しやすいように作る
イメージ動画
https://www.youtube.com/results?search_query=Building+implosion
高速に作る魔法みたいな方法なんて知りません
そうではなく、高速で仕様変更に対応していく方法=最速でキレイに壊す方法です
もちろん、壊しちゃいけないものと壊すものは分けなければなりません
きちんとわけないとジェンガコードになります
(1ブロック変更するだけで、全てが崩壊してしまうコードのこと)
正直なところ、これから話す手法の多くはジェンガコード回避手段です
設計手法
1.将来壊す単位を予測する
壊すとしたら、ここからここまでだよねというのを理解するのが先決です
なので、できれば以下を把握しておいて欲しいです
- どういう経緯でそのアプリが作られるのかのコアの部分
- その仕様に至った経緯
(適当な例)
プロダクトオーナー曰く
ラーメン屋はたくさんあるが、自分好みのラーメン屋を見つけるのは意外と難しい
そこで好みのラーメン屋を見つけるアプリを作りたい
ラーメン屋と、ラーメンの種類の一覧は必要だよね、もちろんラーメンの詳細や画像も
ラーメン好きが好みリスト作って、その人をフォローできるようにしたい
全員がフォロー対象だ
おそらくこのアプリを出せば皆が自分好みのラーメン屋を発見できるようになり
かつ店主はユーザーの嗜好を把握できるばかりかpush通知で宣伝もできるようになるぞ
あ、ポイントシステムやクーポンも付けたいな!ガハハ。1ヶ月で開発できる?
という要求がでてきたとすると、コアは1行目です
2行目以下はソリューションなので全て変わる可能性があります
仕様に至った経緯は、この一行一行を何故必要か深掘りする必要があり、それが経緯です
「何故そのボタンはそこに置いたのか」まで担保できてると良いです
コア部分は壊しちゃいけない
当たり前ですが、コアは壊れない前提で作ります
これが壊れたら別のアプリになるというアイデンティティみたいなものです
(壊れることもありますが=アプリを捨てる)
ラーメン屋はたくさんあるが、自分好みのラーメン屋を見つけるのは意外と難しい
ここで壊れないのは何かといえば
ラーメン、店、自分という概念モデルが登場するということだけです
経緯から分析する
~~という問題があるが、***機能をここに追加することで、解決するはずだ
こういうのを集めます
**はずだ!**は仮説であって、リリースした結果違っていれば仕様変更対象になります
プロダクトオーナーが「これはメイン機能なんだよ」と言っても無視です
誰だって間違うんですから
この「仮説」は仕様の切れ目であって、壊す単位のヒントになります
もしこの仮説が崩れたらどこを崩すことになるかを考えておきます
2.概念モデルを作り、ER図を書く
ER図と言ってもシンプルなもので
絶対壊れない概念モデルを並べるだけです
今回はフォローシステムも入れるみたいなので、コアに加えてこうなります
要素は適当なのであとから考えても大丈夫です
簡単ですね
このモデルが大事です
仕様変更が入ろうが滅多なことではゆるがない、ありがたいアプリの基礎です
もちろん要素はちょいちょい変わりますが、存在自体は中々ゆるぎません
もし消すとか追加するという大改修になっても、この図に沿っていればそれほど大変なことにはなりません
(ちなみにこれってMVCのMだよね?とかドメインモデルだよね?というのは私が混乱するので脇に置いておきます)
機能や画面は基本的に、モデルに対してか、モデルの関係に対して発生します
ばーっと洗い出してみました
基本的な機能や画面は全部モデルにひも付きます
Model - (画面)
Model - (機能)
Model1 - Model2 - (機能)
Model1,Model2,Model3 - (画面)
のように
一般的なアプリを考えると
モデルが1個増えたら、一覧・詳細・検索・他との関係による機能が要求としてあがってきます
そして機能や画面が決まると、それを構成するのに小さいView〜複数画面が必要になります
つまり
1.モデル(概念)
2.機能・画面
3.機能・画面を満たすための要素
1→2→3の順で構築されて
1が一番壊れにくく、3が一番仕様変更の入るものとなります
仕様の経緯が必要なのは「2.機能・画面」「3.機能・画面を満たすための要素」
モデルは現実空間の写像でしかありません
しかも要素は追加しても大して怖くありません
2、3がどうやって決まったかは覚えておいたほうが良いです
これらは**「ユーザーを満足させるために、1を、2というモノで、3という工夫をして提供する」**というものであって
失敗した時は3→2→1の順で試行錯誤が入ります
3.モデルをメッセージ役にしてしまう
モデルが基礎だからとモデルに色々書きたくなりますが
そうではなくモデルをメッセージ役にしてしまいます
その方が何かと都合が良いです
図からわかるように、画面や機能は幾つかのモデルを持っているように見えます
例)ラーメン店一覧画面 → ラーメン店モデルの一覧を持っている
ユーザーのお気に入りラーメン一覧画面 → ユーザーモデルとラーメンモデルの一覧を持っている
あとモデルも、モデルを持てるようにします
というか誰でも、モデルを持てるようにします
とりあえずは単なるデータの塊ですから
普通ですね
利点
モデルをメッセージ役にして抽象化すると、画面や機能はモデルの中に対してかなり無関心でよくなります
ラーメン一覧を表示する画面にラーメンモデルの要素を渡してはいけません、要素の仕様が変わった時に影響範囲が点在していて死にます
抽象化していれば、多くの場合ほとんどModelの改修だけで済みます
(要はただのオブジェクト指向です)
また、普通の画面構成では
ラーメン一覧 → ラーメン絞込 → ラーメン詳細 → ラーメンお気に入り
のように文脈があり、その文脈=モデルとなっています(今はラーメン)
なので、次の画面にモデルを渡すという方法だとやりやすいです
(やりづらい場合は画面遷移がユーザーにとっても分かりづらいはずです)
ちなみにこれってMVCのMだよね?とか(ry
4.すべての意味ある操作は受け渡しにモデルを使う
ViewControllerやActivityはModelの中身を気にしちゃいけません
極力秘密です
モデルの中身を取り出してCellに渡すのではなく、CellにModelを渡します
Cellは中でModelを解釈していい感じにViewを設定します
Modelの中身を取り出してDBに書き込むのではなく、Modelに「ちょっと保存しといて」と命令します
あとはModelが上手いことやります
(私はModel内にDBのEntityを持つようにしてました)
APIからデータを受け取ってModelにするのではなく、
ViewControllerが受け取る時点で既にModelになってるようにします
誰がモデルの中身を見ていいのかといえば、ビジネスロジックを書いていないクラスだけです
ちなみにここで言うビジネスロジックは、試行錯誤で仕様変更がバリバリ入る可能性のあるClassです
(ViewControllerやActivityだと読み替えてもいいです)
利点
こうすることでまず担当領域のレイヤーを分けることが出来ます
仕様変更が機能側に入った時はModelの中を気にする必要はありません
仕様変更がModel側に入った場合は、Model名で検索すれば影響範囲が一目瞭然です
一つの機能が複数画面にまたがってる場合は特に扱いやすくなります
5.メソッドや関数の抽象度を上げる
目指すは機能の一行削除です
そこまで行かずとも、10分で1機能削れることが理想です
そのためには、どこかで抽象的な命令を出しておいて欲しいです
iOSだとViewControllerが指示を出す形になると思います
指示は可読性MAXで、文章レベルになっていることが望ましいです
VC「ラーメンモデル渡すから、あとよろしく。あ、中見てないからチェックもよろ」
→ setup with RamenModel and check if need
みたいな
OSSのコードがよくできてる
人気のLibraryのコードなんかが上手く書かれていて参考になります
大抵メソッド名は何をするか抽象的に書かれていて
その先では一行か少ないコードで別のメソッドを呼んでたりしますよね
具体的なコードは深く見えないところにごちゃごちゃ書いてます
(というかアレを主に参考にしました。非常に勉強になります)
煩雑でも良い部分と、抽象化されていてほしい部分
別に全てのコードを抽象化する必要はありません
お約束コードは別にそのままでいいです
ただ仕様に関わる部分やお約束ではない部分、切れ目がよくわからない部分は
コメントを書く代わりに一行にしてしまったほうが利点が多いです
コードを書いた翌日に再度見て「これ何してんだ?」と思ったらチャンスです
抽象化 or コメント
私はできるだけ抽象化派です
たまに一行メソッドも書きます
コメントが間違っていてもビルドエラーになりませんし
コメントの場合は途中に関係ないコードを入れたくなります(あるいは他の人に入れられたり、中途半端にコピーされます)
// コメント1
code1
// コメント2
code2 ←途中をコピーされてしまう
code14 ←後から差し込まれる
code15
// コメント3
code3' ←コメントとコードが合っていない
メソッド名をきちんと書いておけば、関係ないコードは差し込めないはずです
利点
考えることが減るので高速削除ができます
あとパーツとして他の画面へコピペ再利用もし易いし
何より仕様書が無くても怖くなくなります
仕様書問題
リーン開発のような状況では、現実問題仕様書を細部まで書いておく暇が無いです
そしてもし書いてもミスが多発するので「仕様書を信用するのがリスクになる」という本末転倒な状況になります
そのためコードの可読性を高めることは非常に重要になります
(これもgithubに公開されているOSSのドキュメント量をイメージしてもらうと良いと思います。あのくらいがちょうどいい感じになってきます)
うまくすれば、最上位レイヤーはエンジニア以外が見ても読める状態にできるかもしれません
6.機能改修で変更が生じるクラスはできるだけ少なめに
Modelベースのデザインパターンを作ろうとすると、Managerのような中間クラスを作りたくなりますが
極力避けたほうが事故リスクが減ります
特に一つの機能に対して処理が多くのクラスに及んでくると危険です
例えばそのクラスが論理的凝集になるパターンは多いと思います
1機能で使うためにManagerを作った
↓
他の機能でもManagerを再利用する
↓
似た処理が全てManagerに書かれ、対象範囲が広がり役割が重くなる
(論理的凝集)
↓
1機能を改修しようとしてManagerに手を入れる
↓
影響範囲が広くかつ複雑になっていて詰む(ジェンガコード)
↓
気づかなければ崩してしまい、気づいても崩れるから手を付けられなくなる
ManagerやらFactoryやらUtilやら、そういったものは作るとしても役割を十分制限して作って、
窓口を絞り抽象化しなければなりません(当たり前?)
書けるものはいっそModelに書いてしまったほうが個人的にはマシだと思います
(Fat Modelになりますが、弊害はあまり感じませんでした)
中間:ここまでできればどうなるか
要件定義以外は、そんなに難しいことはしていないつもりです
仕様変更が入った際にある程度楽になると思います
可読性が上がって寝ぼけていてもコードが書けるようになります
しかし速さが足りない!
ここまでできてもまだ遅いです
というかここまではできてる方が多いと思います(MVCとかで)
ここからは設計というより対処療法的な手法です
でも、個人的にはこっちがメインです
注意
予防線を張りますが、あまり常識的とは言えない手法もあります
そもそも今ある常識というのは建物で例えるときちんと建てることを目的にしています
ですが今は取り外しできるように建てなければなりません(コンテナみたいな?)
そうすると多くの点で逆行した手法にならざるをえないのかなと思います
(もちろんまだまだブラッシュアップできそうですが)
1.別のところに書かない、その場所に書く
const なんちゃらと上の方に定義したくなったり
ヘッダーファイルに定数やらをまとめたくなりますが
アレは保守の観点での書き方で
中の分かり難いコードを読まずともここだけ変えればいいよ!というものです
しかし機能改修では中を読むのです
中を読んでる時に「どっかに定義した定数」とか書かれても困ります
メソッドがきちんと定義で来てるならいっそマジックナンバーの方がありがたいです(マジックナンバーじゃないですねソレ)
もちろん色んな場所で使うものは定義すると思いますが
その場合もスコープ最小限の場所にした方が事故りません
一箇所にまとめたりされると影響範囲が広がりジェンガコード化します
(賛否ありそうですが、私は多言語化など必要でないならテキストすらその場に書きます
テキストも事故が非常に多いので)
2.処理をあちこちに書かない
似たような話ですが
処理が別ファイルのどっかのクラスに飛ぶとか、managerに飛んで行くみたいなのは危険です
複数箇所に飛んで行くなんて恐ろしいです
(Objective-Cで言えばプロトコル、通知の独自実装を複数人開発でやるとかは恐怖でしかありません
もちろん仕様上仕方がない面もありますが)
要は結合度を低く、凝集度を高くして、遷移先を明確にしましょうということなんですが
処理が複数箇所に渡る場合はビジネスロジック側からできるだけ隠すようにします
ここでクラスを新たに作ってしまうのは本末転倒で、同じ所に書くの原則を踏まえると、
「ファイルのずっと下の方に書く」というダサい手法になります
もちろん簡単にFatControllerが発生します
1000行超えのControllerなんて最悪だ!と言われますが
抽象度が高く保たれているなら私はFatでも問題ないというスタンスです
機能的凝集でまとめられた2000行のコードと、論理的凝集でまとめられたクラスが複数あるのでは
前者のほうが圧倒的に事故りません
常に機能が増えたり減ったりし続ける状況を想像してもらえればわかると思います
2000行付近まで来て我慢の限界を超えたら、その時初めて慎重に分割を検討します
Libraryなんかでも結構Fatなコードあるんですよね
3.安易に共通化しない
仕様変更した結果、別物になる可能性があります
これは同じ機能であるべきだと思っても関係ありません、決めるのはユーザーです
あるいは実験のため一要素だけ変更してみるということもあり得ます
似ているからといって共通化しないようにしましょう
コピペしましょう
また、安易に共通化すると、修正した時にどこまで影響が及ぶかわかりづらくなりジェンガコード化します
影響範囲が分かっても、共通化の中で分岐するなどというひどいコードになります
特に複数人で開発している場合は簡単に事故ります
他の人が用意した一本道のコードに途中から安易に乗っかるのはヤバイです
共通化して良いのは
- 他のプロジェクトでも使えるくらいに共通化できる部分
- 仕様とは全く関係のない低レイヤーの部分
- ビジネス上共通な部分(例えばキーカラーとかポイント計算とか)です
4.できるだけ継承しない
正確には、自分で作ったクラスを継承しないです
BaseViewControllerみたいなものもオススメしません
1画面だけ処理を変えたい場合が出た場合非常に危険です(ありえないと思いますか?)
1画面だけ処理を変えて、その次にBaseに手を入れる機会があった時、影響範囲をきちんと把握できるでしょうか
どのClassが継承していて、どのClassが継承していないか
どのClassが例外的な処理をしているのか
プロジェクトメンバーの誰かがどこかのタイミングで変更していないか
またアプリの場合、ライフサイクルが煩雑になります
そのライフサイクルをプロジェクトメンバー全員がきちんと把握できればいいですが
現実問題厳しいです
我慢してダサいコードを書きましょう
ただし、一度作ったら二度と手を入れなく、ビジネス上例外も発生しないなら継承しても問題ないと思います(ログの対応など)
Categoryやextensionの方がいいですね
継承するケースってだいぶ少ない?
5.分岐しない・フラグスイッチを作らない
保守性を考えてか、flagを大量に用意するコードが有りますが危険です
コードを読んでもその仕掛けがどうなってるのかわかりづらいので事故ります
ジェンガコードまっしぐらです
分岐もできるだけしてほしくないです
どうしても複雑な分岐を書きたい場合は外出しして抽象度を一定に保ちましょう
というかLibrary読みましょう
後半は正直Libraryの設計を参考にしたほうが早い気がします
彼らは複数人で、仕様変更がバンバン入る状況で開発しているのでそこらへんが非常に洗練されています
洗練されすぎててちょっと難しいですが
※ただ、実際のリーンの現場ではLibrary以上に変更が入るので、Libraryにもう一工夫必要になると思います
最後に
コードは仕様書で、読む人へのお手紙
そんなつもりで書くと可読性が上がります
わかりやすい文章に共通処理や分岐は無いですよね
と、なかなか疑わしいことも書きましたが私の経験ではこうでした
ぜひ機会があったら試行錯誤してみてください
今後、これまでのソフトウェア業界ではあまり無かったような「作って壊して」という行為が益々重要性を増してくると思います
(拡張性、可用性、移行性のどれでもなく「可変性」ですね)
目的によってデザインパターンも使い分ける時代なのかもしれません
そしてもし仕様変更特化型の設計ができたらドヤ顔でこう言ってください
「その変更なら3分でできますよ」
おまけ
概念モデルから考えるとアプリがキレイに作れます
複数のモデルが関わりすぎてる画面は煩雑になりユーザーにとってもわかりづらいです
何のモデルをどうする画面なのか考えて、次の画面に何のモデルを渡すのかを考えるとスッキリします
また、モデルに紐付いて機能が生成されることを意識できれば見積もりが非常に捗りますし
機能漏れも減ります
もちろんモデルを1個増やすと機能数が爆発してありえない工数になります
仕様変更がモデル追加では無いかは慎重に検討してください
リーン駆動をやるならできるだけ3つに抑えたいです(4つから爆発して検討事項が増えすぎるので)