OSSから設計について学ぶ〜クラスの依存方向について〜
要約
僕が間違ってました。すみませんでした。
いきさつ
「Mapを使ったアプリケーションで、Mapの上にMarkerを立てたい」
という要件を満たすために、leafletというOSSを使うことにした。
- 公式サイト
インターフェース(ライブラリの使い方)を予想してみた
OSSを使うときに、
こんな感じのインターフェースじゃないかなって予想してみた。
僕「MapがMarkerを持ってるから、こうだろ」
僕「だから、インターフェースはこんな感じだろ」
const map: Map = new Map(); const marker: Marker = new Marker(); map.addMarker(marker);
僕「で、実装はこんな感じだろ」
class Map { private markers: Array<Marker>; constructor() { this.markers = []; } addMarker(marker: Marker) { this.markers.push(marker) } } class Marker { constructor() { } }
僕「うーむ。実に自然な発想だぞ。」
実際のインターフェース
const map: Map = new Map(); const marker: Marker = new Marker(); marker.addTo(map);
僕「え…?」
class Map { constructor() { } } class Marker { private map: Map constructor() { } addTo(map: Map) { this.map = map; } }
僕「え…?え...?」
Mapクラス「もしかして」
Markerクラス「わたしたち」
僕「(依存方向が)いれかわってる〜〜〜〜!?」
他のOSSもみてみた
google map JavaScript API、mapboxGL, mapboxJSなどもみてみたが、
皆同じようなインターフェースだった。
つまり、
MapがMarkerに依存しているのではなく、
MarkerがMapに依存していた。
どうしてこんな設計なのか考えてみた
こっちのほうが変更に強いからだった。
変更に強いとは
例えば、MapにMarkerじゃなくてPopupも追加しようと思ったら、
僕が考えた実装方法だったらこうなる。
※ 理解を簡単にするため、ソースコードは極限まで単純化しています
class Map { private markers: Array<Marker>; private popups: Array<Popup>; constructor() { this.markers = []; this.popups = []; // 追加 } addMarker(marker: Marker) { this.markers.push(marker) } // 追加 addPopup(popup: Popup) { this.popups.push(popup) } } // 追加 class Popup { constructor() { } }
つまり、Popupの機能を追加しようとしたときに、
Mapの方も修正が必要になってくる。
さらにOverlay(地図上に何かを覆い隠す)を機能として追加してみると、、、
いい感じに「やな感じ」がしてきましたね笑
やな感じの正体
新たな機能を追加するたびに、Mapの方にも修正が発生してしまっているというのが、
やな感じの正体です。
これは「開放・閉鎖の法則」に違反しています。
開放/閉鎖原則に沿ったソフトウェアは、既存のソースコードを変更せずに機能修正や機能追加を行うことができる。 そのため、品質検査を再実行する必要がない。(wiki)
また、MapとMarkerだけ使いたいときもあるはずです。
そのときも、MapはPopupやOverlayのことまで知っている必要がある、というのもおかしな話ですね。
依存関係を逆転してみる
class Map { constructor() { } } // 追加 class Popup { private map: Map constructor() { } addTo(map: Map) { this.map = map; } }
既存のコードに手を加えずに、機能を追加することができます。
すごい。すごいね。
どんなときに応用できるか
たとえば、ペイントのようなアプリをつくるとしましょう。
機能をボンボコ追加しても、修正をしても他のクラスには影響がでません。
素敵ですね。
まとめ
依存関係を逆転して考えると、
その後の機能追加時のコストに大きな差が出てきそうです。
そんなことを、今回知りました。
ただ、今回本当に単純化しているので、実際はもうちょっと複雑なはず。
注意したいです。
本当にオブジェクト指向を使う必要があるのか?
タイトルについて
すみません、煽りました。
いきさつ
オブジェクト指向で設計した時と、
オブジェクト指向を使わなかった時について、
「単純にコード量だけで」比較してみます。
その上で、オブジェクト指向の利点について再度考えてみます。
つくるもの
- 1秒間に1, 数字がカウントアップされる
- カウントアップされるたびに、2で割ったあまりと3で割ったあまりが表示される
コード
NOTオブジェクト指向でかく
const view1Input = document.getElementById("view1Input"); const view2Input = document.getElementById("view2Input"); const view3Input = document.getElementById("view3Input"); var count = 0; const countUp = () => { count++; view1Input.value = count % 2; view2Input.value = count % 3; view3Input.value = count; }; setInterval(countUp, 1000);
オブジェクト指向で設計して書く
- input要素の書き換え…Viewクラス
- カウンター値保持...Modelクラス
- 処理開始...Mainクラス
という簡単な責務で考えます。 そしてとりあえずobserverパターンで実装します。
すると。
class ViewModCounter { constructor(el, model) { this.inputEl = el; this.model = model; this.mod = 1; } setMod(num) { this.mod = num; } render() { this.inputEl.value = this.model.count % this.mod; } } class ViewCounter { constructor(el, model) { this.inputEl = el; // viewはmodelに依存している this.model = model; } render() { this.inputEl.value = this.model.count; } } class Model { constructor(count) { this.count = count; this.listeners = []; } setCount(count) { this.count = count; this.notify(); } addListener(listener) { this.listeners.push(listener); } notify() { this.listeners.forEach((listener, index, listeners) => { listener.render(); }); } } class Main { constructor() { this.model = new Model(1); const view1Input = document.getElementById("view1Input"); const view2Input = document.getElementById("view2Input"); const view3Input = document.getElementById("view3Input"); let view1 = new ViewModCounter(view1Input, this.model); view1.setMod(2); let view2 = new ViewModCounter(view2Input, this.model); view2.setMod(3); let viewCounter = new ViewCounter(view3Input, this.model); this.model.addListener(view1); this.model.addListener(view2); this.model.addListener(viewCounter); } run() { const model = this.model; let sec = 0; setInterval(() => { sec++; model.setCount(sec); }, 1000); } } const main = new Main(); main.run();
めっちゃ増えた
記述量だけみると、オブジェクト指向で設計するほうが大変です。
※ もちろん要件によっては記述量が逆転することもありえます
じゃあいつオブジェクト指向設計を使うのか
個人的には、
- 要件が複雑になった時
- 扱うモノが増えた時
この時、オブジェクト指向設計じゃないとかなり苦しむな、と感じています。 あとは保守のときですね。きちんと設計しないと一つの修正がいろんなところに飛んだりします。
注意
オブジェクト指向設計とはクラス化することではありません。
あくまでも、オブジェクト指向は手段で、
目的は「実装時にいかに楽に、そして保守時にいかに楽になれるか」というところにあります。
それを意識せずにただただクラス化するだけだと、
逆に辛くなることだって起こりえます。
(辛くなったことがある)
そこらへんの話がこの本にかいてあります(もっかい読みたい)
DDDのリポジトリについてちょっとしらべた
いきさつ
昔書いていたアプリケーションがあまりにあまりなので、
(モデルの責務がいたるところに染み出している)
DDD使って書き換えられないか検討するために勉強を始めた(1日目)
Repositoryからやっていきます
Repositoryについて
エンティティの永続化を担当する。
あと、エンティティの検索を担当する。
使われているところ&メリット
主にServiceクラスで使用されている。
ただし、Serviceクラスが依存しているのはRepositoryクラスそのものではなく、
Repositoryのinterfaceに依存している。
<?php class UserService { private $_userRepository; public function __constructor(IUserRepository $userRepository) { $this->_userRepository = $userRepository; } }
なので、もし別のリポジトリを使うことになってもServiceクラスを書き換える必要はない。
つまり、ORMを別のものに替えたり、もっと簡易なクエリビルダ的なものにしたりすることができる。
感想
そもそも今までActiveRecordパターンを使ったフレームワーク(Rails, FuelPHPのORM)での開発がほとんどで、
永続化したり検索したりする部分も"Model"でやっていたので新鮮。
ただ、まだちゃんとしたメリットまではわかっていない。
これから勉強していく。
JavaScriptのgetter, setterについて復習してみた
getter, setterとは
オブジェクトに値を代入したり、
参照したりする時に呼ばれる関数のこと。
書き方
set {プロパティ名}(value) { // 処理 } get {プロパティ名}() { // 処理 }
のように書く
実例
var obj = { set value(val) { this._value = val + 1; }, get value() { return 'getterからは' + this._value + 'を返す' } } obj.value = 1; // setterが呼ばれる console.log(obj.value); // getterからは2を返す ←getterが呼ばれている
用途
値をセットしたり、値を参照したりする時、
決まった処理をする際に便利。
json-serverが鬼のように便利で、しかも可愛かった
いきさつ
APIのmockがほしかった。
今までexpressとかで実際にAPIをつくっていたのだが、
ちょっとめんどくさくなってきた。
何かいい方法がないかな〜と思ってたら「json-server」という良さげなものがあったので試してみた。
install
npm install -g json-server
jsonデータを用意
db.jsonというファイルを用意する。
{ todos: [ { id: 1, content: aaaa, done: false }, { id: 2, content: bbb, done: true }, { id: 3, content: ccc, done: true } ] }
json-serverを起動
json-server --watch db.json
絵文字がかわいい。
localhost:3000にアクセス
こっちもかわいいかよ。
localhost:3000/todos/1 にアクセス
{ "id": 1, "content": "aaaa", "done": false }
しっかり返ってきてる。
まとめ
GETしか試していないが、
ドキュメントに
GET /posts GET /posts/1 POST /posts PUT /posts/1 PATCH /posts/1 DELETE /posts/1
とあるのでいろいろ使えそう。
あとかわいい(重要)
【突然の沈黙】JavaScriptのobjectスプレッド演算子について
いきさつ
ソースコードを読んでいたら、
var arr = [1, 2, 3] var arr2 = [4, 5, 6] arr.push(...arr2)
とつぜん処理の中で沈黙(...)し始めた。
気持ち悪かったので調べてみた。
ドキュメントを見てみる
スプレッド構文を使うと、関数呼び出しでは 0 個以上の引数として、 Array リテラルでは 0 個以上の要素として、 Object リテラルでは 0 個以上の key-value のペアとして、 Array や String などの iterable オブジェクトをその場で展開します。
うーん。
わかるようなわからんような。
実際に試してみよう。
...Array, ...Stringはどうなるのか
...Array だと
var arr = [1,2,3] console.log(...arr) // 1 2 3
ばらばらになるイメージ?
...Stringだと
var str = 'hello world' console.log(...str) // h e l l o w o r l d
やはりばらばらになる。
ばらばらにしたものを、何かに渡してみる
関数呼出しに渡すその1
ためしに、...arrを3つの引数を受け取る関数に渡してみる。
var arr = [1, 2, 3] function add(x, y, z) { return x + y + z } console.log(add(...arr)) // 6
なるほど。たしかに、
関数呼出しでは3つの引数として展開してくれている。
関数呼出しに渡すその2
この2つの配列をマージするケースを考えてみる。
var arr = [1,2,3] var arr2 = [4,5,6] arr.push(arr2) console.log(arr) // [1, 2, 3, Array(3)]
普通にpushで書くとマージできない(当たり前だけど)
これじゃない。これがやりたいんじゃない。
ここで、
objectスプレッド演算子を使って展開した値を、
配列が持っている関数、pushに渡してみる。
var arr = [1,2,3] var arr2 = [4,5,6] arr.push(...arr2) console.log(arr) // [1, 2, 3, 4, 5, 6]
おお。たしかに関数呼出し内で引数として展開されていることが確認できる。
なるほどねぇ。
配列に渡してみる
var str = 'hello world' console.log([...str]) // ["h", "e", "l", "l", "o", " ", "w", "o", "r", "l", "d"]
おー。確かに配列内で要素として展開されている。
これでsplitを使わなくても良くなるな。
まとめ
objectスプレッド演算子を使うと「ばらばらになる」
そのばらばらになったものを、関数に渡したり配列に渡したり、
Objectに渡したり(まだ試してないけど)することにより、
処理の記述が簡単になる。
Vue.jsにて、特定のroutingの時に後からComponentを読み込む〜ログイン後にコンポーネントを読み込みたいからLazy Loadしてみた〜
いきさつ
ログイン前はログインコンポーネントだけ、
ログイン後はアプリケーションに必要なコンポーネントを読み込みたい。
目的は、
ログイン前に読み込むJavaScriptソースコードの軽量化と、
それによるセキュリティリスクの減少。
やりかた
はじめ、こんな感じだったとします。
import Vue from 'vue' import Router from 'vue-router' /** ログイン画面で読み込むコンポーネント */ import Login from '@/components/Login' /** ログイン後に読み込みたい(Lazy Load)したいコンポーネント */ import Index from '@/components/Index' Vue.use(Router) export default new Router({ routes: [ { path: '/login', name: 'Login', component: Login }, { path: '/', name: 'index', component: Index } ] })
これを、下のようにするだけです。
import Vue from 'vue' import Router from 'vue-router' /** ログイン画面で読み込むコンポーネント */ import Login from '@/components/Login' /** ログイン後に読み込みたい(Lazy Load)したいコンポーネント */ const Index = function () { return import('@/components/index') } // 後は同じ
importとreturn importの違いですが、
import Index from '@/components/Index'
とすると、Indexにはオブジェクトが入り、
const Index = function () { return import('@/components/index') }
とすると、__webpack_requireという関数が入るようです。
ログイン後webpack_requireが実行され、その際にソースが読み込まれる感じ。
結果
ログインボタンを押した後、
1.jsというファイルがあとから読み込まれている事が確認できます。
この1.jsにはindexコンポーネントの情報が入っていました。