こんにちは。ますぐれです。昨年のアドベントカレンダーではSplatoonに関係する記事を書きましたが、せっかくなので今年は技術系のお話を書こうと思います。
といっても、ぼくはそこまでプログラミングをやりこんでいるわけでもないので、最近理解出来てとてもうれしかった抽象化の概念について皆さんに追体験してもらおうと思います。
どういう話かというと、インターフェースを適切に使うことで、プログラムの一部を差し替えることになったとき、ほとんど変更しなくて済むよねという話です。
対象読者
- インターフェースが大事なのはなんとなくわかるけど使いどころがよくわからない人
- 具体例でインターフェースが使われるところを見てみたい人
本題
早速ですが本題に移っていきましょう。ぼくが実際に作ったのは、Twitter上でお気に入り登録した画像をダウンロードし、googleDriveにアップロードしてバックアップを取るといったアプリです。言語はjavaでした。ライブラリとしてはtwitter4jとgoogleDriveAPIを利用しています。
このプログラムの簡単な流れは以下の通りです。
- twitter4jのラッパークラスを作成
- ラッパークラスを使って画像のURLのリストを取得
- ダウンロードクラスにそのリストを投げて画像をローカルに保存
- googleDriveAPIのラッパークラスを起動
- ローカルに保存した画像をドライブにアップロード
もしかするとラッパークラスという言葉を初めて耳にする方もいらっしゃるかもしれません。というわけで、ここでラッパークラスについて少し説明をさせていただきます。知っている方は次の節は読み飛ばしてもらって構いません。
ラッパークラスとは
ラッパークラスとは、ライブラリを自分が使いやすいようにするために、自分のプログラムとライブラリの間に挟むクラスのことです。具体例としてtwitter4jのラッパーについて考えてみましょう。
ぼくたちが今回twitter4jに期待していることはなんでしょうか。当然ツイッターから画像を取得するメソッドです。画像を取得したいのですから当然ですね。
しかし、そのような具体的なメソッドがライブラリに用意されていることはあまりありません。ライブラリは様々な用途に利用できるように、一つのことしかしないメソッドで構成されているのが一般的だからです。例としては、ツイートを投稿する、一つのツイートにお気に入りをつけるなどといったメソッドがあります。
こうしておけば、使う人自身が自分の目的に沿ってメソッドを組み合わせることでさまざまなプログラムを書くことができるようになるというわけです。ライブラリってとっても便利ですね。
つまり画像を取得するにもいくつかのメソッドを組み合わせる必要があります。これを直接書いてしまうとこれをメイン関数内でやるとごちゃごちゃしてしまって面倒そうですね。確かに利便性の面ではメソッドが細かい方が便利ですが、使う側としては一つのメソッドでまとまっていた方がやりやすいはずです。そこで、twitterから画像を取得することに特化したクラスを一つ作って、それをメイン関数で呼び出すようにしましょう。ライブラリに関する処理は全部その特化したクラスに押し付けて、メインの方では関数を呼び出すだけにしようということです。
このライブラリ用の処理を押し付けたクラスがラッパークラスです。ライブラリを利用するときに用意すると便利なので知らなかった方は押さえておくといいかもしれません。
ワークフローの抽象化
ラッパークラスについて説明したところでメイン関数の方に戻りましょう。先ほどのワークフローはこのような感じでした。
- twitter4jのラッパークラスを生成
- ラッパークラスを使って画像のURLのリストを取得
- ダウンロードクラスにそのリストを投げて画像をローカルに保存
- googleDriveAPIのラッパークラスを生成
- ローカルに保存した画像をドライブにアップロード
これは非常に具体的な中身になっています。実際にプログラムが処理するフローを載せているだけですからね。
これでもいいじゃんと思う方はいらっしゃるかもしれません。ぼくもそうでした。これでちゃんと動きそうだし機能も分離できているしよくない? って思うんですよね。実際それでいいときもあります。しかし、これにはまだ改良の余地があります。
例えばツイッターからだけではなく、フェイスブックやインスタからも画像を取得したいとなったらどうしましょうか。今のままでは新しいアプリを作らないと対応できそうにありません。少なくともメイン関数も含め大改修が必要になるはずです。
そのような機能を追加するときにもっと簡単にできたら良いですよね。だって、画像のURLを取得した後の流れはみんな同じになるはずですから。そこ以外を変更しなくても動くようにしたいわけです。
ツイッターもインスタもフェイスブックも、全部ひっくるめて言ってしまえば画像のダウンロード先です。ついでにグーグルドライブも画像のアップロード先に過ぎません。というわけで、ワークフロー中のここら辺の表現を書き換えてみましょう。こんな感じになります。
- ダウンロード先と接続するクラスを生成
- そこから画像のURLのリストを取得
- 実際にダウンロードする
- アップロードクラスを生成
- 実際にアップロードする
良さそうですね。これなら突然の仕様変更が来てもその部分を差し替えるだけで済みそうです。ただ、今はワークフローを書き換えただけなので、実際のソースコードはこのような感じになっています。
TwitterWrapper twitterWrapper = new TwitterWrapper();
twitterWrapper.getUrls();
これはワークフローに書かれていることよりも具体的です。ワークフロー上にはツイッターなどといった単語は出てきていませんが、ソースコード中には出現してしまっていますね。
このコードをワークフローと同じ抽象度にするためには下のように書かれていなくてはいけません。
Downloader downloader = new Downloader();
downloader.getUrls();
名前を合わせました。ですが、これは実は上と名前を差し替えただけの全く同じものなので何も解決していません。結局、Downloaderクラスにはツイッターと通信するための処理が書かれてしまうということです。これでは先ほど言ったような、ツイッターの所をインスタ用の処理に差し替えるみたいなことはできませんね。
実は、このようになってしまうのには理由があります。それは名前と実装が完全にくっついているからです。名前と実装がくっついている状態ではそこで実装されていることに名前を付けるしかありませんので、今の状態は仕方がないといえば仕方がありません。
ぼくたちは、メイン関数上では画像のダウンロード先であるツイッター、インスタ、フェイスブックをまとめて相手したいんです。でも処理自体はそれぞれで別にしなければなりません。
そんな魔法のようなことができるのでしょうか? 今のままではもちろんできないので、ここで新たな武器を投入します。それこそが今回とりあげるインターフェースです。
インターフェースによる名前と実装の分離
インターフェースとは、様々なクラスにある性質を取り出し、それの存在を保証するものです。
今回の例でいえば、ツイッター、インスタ、フェイスブックはすべて画像のダウンロード先という性質を持っています。そしてぼくたちは、この性質に対してほしい画像のURLのリストを取得したいと考えています。
そこで、こんな感じのインターフェースを用意します。
IDownloader.javaimport java.util.List;
public interface IDownloader {
public List<String> getUrls();
}
これで、これを実装したクラスがgetUrls()というメソッドを持つことが保証されました。こんなことが許されるのは、これを実装するクラスが画像のダウンロード先であるという性質を持っていると決めたからです。画像のダウンロード先であるなら、ダウンロードする画像のURLのリストを返すという性質を持つのは明らかでしょう。
これがインターフェースの力です。後は先ほどのTwitterWrapperクラスでこれをimplementsすることで、以下のようにメイン関数を書くことができますね。
Main.javaIDownloader downloader = new TwitterWrapper();
downloader.getUrls();
これで使う側は中身を意識することなくgetUrls()を使えます。例えばインスタから画像を取りたいってなったらTwitterWrapperクラスのインスタバージョンを作り、そこだけを差し替えればこのプログラムは正常に動きます。
同様の手法でアップローダーについてもインターフェースを作ることができます。こうなれば変更する場所は必要最低限になるので最高ですね。これが抽象化されたプログラムが変更に強いといわれる所以だと思います。
インターフェースとは共通する性質をとり出すために必要なものだ、と先ほど言いました。そう考えれば、javaなど多くの言語でインターフェースを複数個実装できる理由が分かると思います。複数の性質を持つクラスなどもこれを使えば簡単に作ることができますからね。
ここまででお腹一杯という方はここまでをしっかり覚えて帰っていただけたら幸いです。この先ではもう少しこれを抽象化しようという話をします。
更なる抽象化
実は、上のmain関数はまだTwitterWrapperに依存しています。関数内でnewしているからです。これをなくすことを考えましょう。
newしなかったら使えないじゃん!ってなるかもしれませんが、実はそんなことはありません。オブジェクトを取得する方法としては、実際に宣言するほかにメソッドの返り値として受け取る方法があります。これを利用しましょう。
例えば他のクラスにこんなメソッドを用意しましょう。
public class DownloaderFactory {
public IFileDownloader create(String name) {
if(name.equals("Twitter")) {
return new TwitterWrapper();
} else if(name.equals("Instagram") {
return new InstagramWrapper();
} else {
//該当するものがないときはTwitterWrapperを返すことにしておく
return new TwitterWrapper();
}
}
}
これはダウンロードしたいSNSを文字列で指定することでそれに対応するdownloaderを返す関数です。それぞれのdownloaderはIFileDownloaderをimplementsしています。
これはdownloaderを作る工場のように見えるので、DownloaderFactoryという名前にしました。
そんでもってメインでこのように記述します。
main.javaDownloaderFactory df = new DownloaderFactory();
IDownloader downloader = df.create("Twitter");
downloader.getImageUrls();
はい。newを消してもnewと同等の処理ができるようになりました。これは文字列を引数にとって処理を変えられるので、プログラムのargとしてこの文字列をもらうようにすれば、プログラムをビルドしなおさなくても処理を変えることができ、柔軟な運用ができるソフトウェアにできそうですね。
まとめ
インターフェースは抽象化をすると必要になる概念だというお話でした。初学者の頃はこれが全く分からなくて謎でしたが、実際に自分でコードを書くようになったら理解できたんですよね。なのでこの記事を読んでインターフェースの価値がよく分からなくても、実際に使う段階になったら理解できるかもしれません。
それと、抽象的なコードでも具体的なコードでもきちんと書かれていれば正しく動作するという点では同じです。違うのは機能の追加しやすさ・変更のしやすさなのです。インターフェース等を使いこなせる自信がなければ、具体的なコードを書いてももちろんオーケーです。途中まで作って止めるよりは最後まで完成させた方がいいに決まってますから。
もちろん物事を抽象化することはとても楽しいので、実装するかしないかはともかく、これを抽象化するとどうなるかなということは常々考えてもらえたらなあと思います。
ぼくの記事は以上になります。お付き合いいただきありがとうございました。明日はsobaya007さんの記事です!ぼくも楽しみにしています。