はじめに
本記事は僕が何かを作る際に気にしている事をプログラム視点で書いてます。
本来なら以下の内容の前にアイデアを考えるまでという必要がありますが、その部分は今回の内容とは関係ないので割愛してます。
前半はプログラムとは関係がありませんが、プログラムを作る時に起因するので記載してます。
※全体を通してもプログラムの書き方(サンプルコード)的なものは殆ど書いてません。
P.S.
書いてわかったのだが、この記事は未完成です。
僕の頭の中をまとめるためのメモになってます。
必要なものを揃える
僕が何かの製品を最初から最後まで作る時には、まず必要なものを揃えます。
例えばシューティングゲームを作る時には、何ができればいいかを考えます。
- 画像を描画できる
- 画像を動かせる
- 当たり判定が行える
- 画像を消せる
これだけの事ができれば簡単なシューティングゲームは作れます。
重要なのは「プロダクトを作る時に必要となることが何なのか?」をしっかり自分で定義する事です。
そして、その定義したものが実現できるかをあらかじめ検証する事が重要になります。
必要なものの粒度を考える
ゲームを作ったことがある人なら「あれ?」と思ったかもしれません。
本来ならもう一つ重要な事があります。
それは速度
です。
実際に作ったは良いけれど、遅くて話にならないということは往々にしてありがちです。
速度以外にも重要なものは多々あります。
そこで考えるべきことは
今回作るもので重要なことは何か?
という事です。
例えば、僕がビジネスで作るなら速度
は重要なことになります。
しかし、個人で作るなら速度
に関しては「後でなんとかなるだろう。」レベルでも問題ないのです。
(個人で作る場合は、作ることが重要だったりします。)
粒度を揃えるというのは「本当に必要なもの」と「必要かもしれないが、今は考えなくても良いもの」を分別する事です。
そして、今作る前に必要である「本当に必要なもの」だけにフォーカスを当てて調べたり検証することがポイントです。
なんでもかんでも必要なものとして定義するのは簡単ですが、それでは足が遅くなりプロダクトが作れません。
P.S.
本記事で話したいことから逸れるので深くは言及しませんが、僕はビジネスで考えるときはデータモデルを一番重要視して考えます。
今回の業務に適したデータモデルを設計し、DBエンジンに適したインデックスが何かを考えて時にはデータを入れて実測してみたりもします。
Webシステムの場合は、ある程度の経験があれば画面周りやビジネスロジックで困ることはありません。
困るのは「データモデルを個々の判断で作られること」や「統一性のないコードが量産されること」、「お客様が期待する使い方に耐えられるパフォーマンスを出すこと」などです。
作るものや立場によって重要なことは変わります。
プロダクトを作るというのは、はじめに重要な事を見極めることになります。
繋げる方法を考える
必要なものを考えるときに、同時並行する必要がある仕事がこの「繋げる」です。
必要なものがすべて揃っても、どのように繋げれば良いかがわからないと意味がありません。
上の例で例えば
画像が表示されていて、当たり判定のロジックはできている。
しかし、画像の位置情報と描画サイズを取得できないため当たり判定のロジックに渡せない。
なんて事になったら実装時に困ります。
なので「揃える」ということは「繋げる」という事と同時に考えなければならないのです。
ビジネスでも同じです。
モデル設計だけを考えて、モデル主体だけでインデックスを考えて「画面ではこう呼び出してほしい!」という進め方をしてしまうと本末転倒です。
モデル設計をする際には、画面にどのような情報を出すかを洗い出す必要があります。
なので、モデル設計は重要だけどモデルをどう使うかを考えないような設計はしてはいけません。
かといって、すべてデザイン通りに進めるのも危険です。
統一性のないデザインは設計泣かせでもあると同時に、ユーザーにとっても使いにくい場合があります。
その部分に関しては「揃える」と「繋げる」を定義している際にデザインと話し合うことが重要になります。
フォルダ構成
さて、いよいよ実装を・・・の前に。
小さいプロジェクトでファイルが20個くらいしかないならこの作業は不要です。
また、フレームワークを使っている場合にフレームワークが定義したフォルダの中が10個くらいで収まるなら不要です。
しかし、XCode開発をする際やフレームワークの指定したフォルダ内に大量のファイルが格納される可能性があるならあらかじめフォルダを切っておきましょう。
あらかじめフォルダを切ることで、どのフォルダに何をやらせるべきかが定義できます。
特に「揃える」で集めた情報は粒度が大きい場合(普通はプロダクトの中核処理)が殆どなのでその単位で切っても良いと思います。
これは後述する名前付けに匹敵するほど開発効率に影響します。
iOS開発の場合
iOS開発(XCode)にはグループ
という便利な概念がありますが、グループ
に頼ってしまうとGitHub上で見たりターミナルで見るときに目的のファイルが探せなくなります。
なので、フォルダとグループを紐付けて作成すると良いです。(グループの良さが軽減してしまいますが、最終的に煩雑なファイル構成になるよりもマシだと思います。)
特にリソースファイルをグループで管理すると画像の差し替えで困ります。
(画像はxcassetsを使うことをお勧めします。Spriteなどで画像を使う場合はfolder reference
を使う事をお勧めします。)
iOSの場合はフォルダの切り方はアプリによると思いますが、僕は以下の切り方をします。
(OpenGLで開発する場合)
- Config・・・アプリの設定ファイル各種を格納します。基本は
static let
のものだけになります。 - Common・・・アプリ共通で使用するライブラリーなどを格納します。僕は
extension用クラス
もこのフォルダに格納します。 - Model・・・データを扱うクラスを格納します。いくつもの種類のデータを扱う場合はさらにフォルダ分けをします。
- Scene・・・画面単位のSceneを格納します。
- Layer・・・画面内で扱うレイヤーを格納します。
- Node・・・レイヤーよりも粒度の小さいものを格納します。(ボタンとか、パーティクルとか。)
- Resources・・・リソースファイルを配置します。(画像とか、マップとか)
僕はCocos2dを使うのでSource
フォルダがこれらの上位にあります。
Railsの場合
Railsの場合は基本的にフレームワークに沿ったフォルダ構成が良いと思います。
ただし、コントローラーは肥大化しがちなのでrails generate
で作成する際にフォルダを切るルールを決めておきます。
(予め切っておいても良いですがrails generate
がフォルダを作成してくれるので、ルールだけ明文化しておいても良い。)
コントローラーに引きづられてViewなどのフォルダが切られるのでそれに沿って開発を進める。
railsのlayoutsは継承先に引き継がれるので、layouts単位で大きくフォルダを切っておくと幸せになりやすいです。
大きなフォルダ配下のコントローラーは親フォルダのコントローラーを継承していくと、レイアウトを継承したクラス群をまとめて管理できます。
(ログインセッションの管理も簡単になります。)
モデルに関してはフォルダ切りはできるだけやらない方が無難です。
というのも、呼び出し方が変わってしまうのとフラットに配置されてもそこまで煩わしくないことが殆どだからです。
しかし、例えばUserに関連するモデルが5個も6個もある場合などはフォルダ化してUserモデルで処理のパイプを行うなどをするとスマートな構成になったりします。
CoffeeScriptに関してはあまり分割されると可読性が落ちてしまう場合があるので、この辺りはプロジェクトでルールを決めると良いかと思います。
(シングルページアプリでなければ、CoffeeScriptの配置は初めに決めなくても後で大体なんとかなると思います。)
大クラスの名前付け
名前付けはプロジェクトを進める上でもかなり重要です。
しかし、重要なだけあってかなり難しい仕事でもあります。
また、全ての名前付けに多大な時間を浪費してしまうのも考えものです。
なので「揃える」で定義した処理に関連するクラスだけでもしっかりした名前をつけておきます。
(もちろん他のクラスもしっかりした名前をつける方が良いですが、処理内容を明確に示した命名は熟練プログラマーでも難儀する仕事です。
なので「粒度が小さいものに関してはある程度目をつぶってもうまくいける」と言うくらい基礎の命名をしっかりしましょうということです。)
基礎のクラスがしっかりしているとプロジェクトに耐久力が生まれます。
その後で多少変な事をしても十分耐えれるようになるので、ここの名前はしっかりつけましょう。
ポイントは以下になります。
- 短く親しみやすい名前が望ましい
- 処理の内容が初見で分かるものが望ましい
- 発音しやすい名前が望ましい
基礎の大きなクラスはできるだけ短い名前をつけて、そこから派生するクラスや関連するクラスの名前を長くすることでクラス名の長さで重要度がわかるという利点もあります。
疎結合で再利用性が高いプログラムを作る
密結合なクラスは再利用する際の条件が複雑になりがちです。
なので、できる限り名前付け
で付けた名前の挙動しかしない疎結合なクラスを作ることを心がけます。
これを行う時に効果的な方法は以下の手順で行うことです。
1. クラスを作成する
2. 空のメソッドを作成する。(インターフェースの定義)
3. そのクラスを利用するコードを書く。(挙動しなくても良いのでコードだけ先に書く)
4. 実装する。
初めに2
を行うことで、インターフェースだけ持ったメソッドが作られます。
それに対して3
を行うことで、定義したインターフェースの使い方が確立されます。
あとはそれに沿って実装すれば綺麗な形でクラスが作成されます。
(この辺りはTDDに少し似ている。レッドからグリーンへ・・・って感じですね。)
初めから実装を書いてしまうと「このケースの時にはこういう実装が必要だ」などの余計な情報がメソッドに書かれてしまい、結果メソッドが肥大化して名前と沿わない動作をしてしまいます。
初めにインターフェースを定義した場合は「このケースの時にはこういう実装が必要だ」という局面でも「新たなインターフェースが必要だ」などのハンドリングができるため余計な情報がメソッドに書き込まれづらくなります。
僕は大体においてこの方法でプログラムを書きますので、メソッドが肥大化することがあまりありません。
(でも、自分の想定外の事があるときには肥大化することもあります・・・)
究極的に言えば関数型言語になるのでしょうが、残念ながら僕は関数型言語に関してのスキルがありません。
ただ、関数型のロジックは便利なのでそれなりに多用してます。
(副作用がない関数は本当に理想形です。しかし、メソッド内でデータを設定したいという局面も出てきてしまいそれを実装してしまうので僕は究極的の領域には届いてません。)
これにはもう一つ重要な事があります。
「人は忘れる生き物」という点が重要です。
大量のコードを書く場合は、各メソッドの中に書いたコードを忘れることは多々あります。
久しぶりに使うメソッドで、メソッドの中のコードまで読んで対応してしまうと生産性が落ちてしまいます。
なので、できるだけ簡潔なインターフェースと名前付けをしてメソッドを読まなくても使える状況にしておくことが重要です。
実装について
RubyもSwiftもキーワード付き引数が使えますので、この機能は多用しましょう!
呼び出し側で無意味な引数を渡していると、実際にコードを見たときに「2番目の引数ってなんだっけ?」となりがちです。
キーワード付き引数は、引数に名前を与えてくれるのでインターフェースを明確化してくれます。
また、外部から受け取った引数なのか自クラスで定義した引数なのかを明確化するのも効果的です。
Swiftの場合はprivate
やweak
などを使えば簡単に明確化できる上に、動的ディスパッチを減らせたり無用なメモリリークを防げます。
Rubyの場合は下手にプライベートキーワードを使うと可読性が下がる場合もあるので、僕はあまり工夫を凝らしてません。(下手に命名ルールをつけると逆に可読性が落ちてしまう・・・)
なので、これはSwiftに限っての話になります。
ただし、Privateメソッドに関しては積極的に使っていきましょう。
黒魔術はほどほどにしましょう。
SwiftもRubyも黒魔術(クラス内の処理変更やリフレクション(Rubyのみ)など)が使えますが、基底となるクラスなどのみで使用しましょう。
特にRubyはインスタンス化されたオブジェクトに対してもメソッドの追加などができるので注意が必要です。
これらを業務コードで行ってしまうとどんなに綺麗に作ったプロジェクトでも一気に耐久性が落ちる可能性があります。
※SwiftはObjective-cに比べて使える黒魔術が減っている。そういう意味では保守性に優れたコードが書きやすい。一方で小回りがきかない局面もある。
iOSについて
iOSアプリはAPI連携にしてもボタンの押下処理にしてもコールバックやselecterなどの非同期処理が多く発生します。
なので必然的に高階関数を多用する事が増えます。
(特にOpenGLではLayerを閉じる場合のコールバックなどが多く発生します。)
コールバックが増えるということは、コールバックのルールを定義しておいたほうが無難です。
delegateと関数戻しが混在するとシステムが煩雑になるので、どの場合にdelegateを使うかなどの定義をしたほうがいいです。
ちなみに僕はdelegateは殆ど使いません。というのもdelegateのほうがコード量が増えるため、コールバック関数を多用します。
(その代わりweakの定義をしっかりしないとコールバック関数とキャプチャーされてる変数が解放されなくなるので注意が必要です。そういった意味ではdelegateの方がルールがシンプルでメモリの解放が楽です。)
以下、書いたけどボツにした
僕が使うルールは以下になります。
- 画面が閉じる場合のコールバックは生成時にコールバックを渡す。
- 上のレイヤーで発生したタップイベントを下位へ受け渡す場合はtapCallbackに関数を設定します。
1
と2
でコールバックを分けているのは生成〜破棄のコールバック関数を統一したいためです。
1
と2
を同じにするとLayer毎にコールバックの返却値が変わってしまうためコードが読みにくくなると思ったためです。
例)
let userLayer = UserLayer(select: true) {
// 閉じた場合の処理
}
userLayer.tapCallback = { user in
// ユーザー選択時の処理()
}
let itemLayer = ItemLayer(select: false) {
// 閉じた場合の処理
}
と書くところが
let userLayer = UserLayer(select: true) { user in
// 閉じた場合の処理、もしくは選択された場合の処理
}
let itemLayer = ItemLayer(select: false) { item in
// 閉じた場合の処理しか書きたくないが、itemが戻ってくる・・・。
}
のようになってしまったり、下手すれば複数の値を返されてしまい流用が難しくなると感じるためです。
(この処理では表示だけ、この処理では選択し結果がほしいけど同じLayerを使いたいと言った時に流用しにくい。)
と書きながら、この辺りはまだ僕の中で落ちきってないと感じました。
なので、書いたもののボツとさせてください・・・。
これだというルールが見つかったら別記事として書きます。
Railsの場合
RailsはiOSに比べてフレームワークが決めてくれる部分が多くあります。
REST設計にすれば基本的な部分で悩むことは殆どないでしょう。
(というか、基本的にREST設計に沿うことをお勧めします。)
ただし、イレギュラーな処理がある場合は注意が必要です。
例えば、データを取得しているけれど一部データを保存している場合。
足跡機能などで起こる現象だと思います。
この場合にPOSTにしてしまうと、処理内容がわかりにくくなります。
(行動履歴を取る処理があった場合、殆どの処理がPOSTになってしまい何が何だかわからなくなってしまいます。)
なので、行う処理の比重によって「実際は違う処理もしているけれど」という部分の区分けをする必要があります。
このように書けばわかると思いますが、モデル設計と同時に重要になるのがルーティング部分になります。
逆に言えばこの二つが強固であればあるほど耐久性が強いシステムを構築できます。
あと、Railsを行うときに注意点となるのがビジネスロジックをどこに書くべきかを定義しておかないといけない局面があります。
僕が巨大プロジェクトをRailsで組んでいた時代はRails2〜3系だったためConcern
が無く、Libフォルダを作ってビジネスロジックを書いてました。
これはファットコントローラーになることを恐れたためです。
ただ、このレイヤーはRailsに乗っ取っていないため少しカオス気味になってしまいました・・・。
まとめ
と、いろいろ書いてみると分かったのが僕は序盤の設計で勝負をかけけて中盤から終盤では序盤の土台に頼って乗り切っていることがわかりました。
というのも、細かい部分を突き詰めて書こうとしても上記の通り少し手法としては弱いと感じたからです。
しかし、書いてみて自分の弱いところがわかったのは十分な収穫なのでメモとして公開しておきます。