「だからボクはオブジェクト指向が使いこなせない」シリーズ
はじめに
本記事は、Android/Java開発において、オブジェクト指向を使いこなしたい、という方に向けた記事です。
私が10年近くAndroidアプリ開発に関わってきて思うのが、開発現場でオブジェクト指向らしいコードを書けるプログラマーは、残念ながら全体の2割にも満たない、ということです。(あくまで私個人の感想です)。
逆に言えば、オブジェクト指向らしいコードを書けるようになるだけで、開発現場で重宝される人材になれます。Androidの最新技術やKotlinの勉強をすることも大切ですが、開発現場でより即効性があり、かつ、Android以外のプログラミングにも応用できる、潰しのきく技術、それがオブジェクト指向らしいコードを書く技術です。
どうしたらオブジェクト指向らしいコードを書く技術を身につけられる?
※具体的な技術的な話をすぐに読みたい方は「開発現場」の段落までスキップしてください。
昔、先輩社員に「どうしたらオブジェクト指向らしいコードを書く技術を身につけられるか」を聞いたところ「開発経験を積むこと」というあいまいな答えをもらい悩んだことがあります。この「開発経験」は自分にとって余程都合が良いプロジェクトでないと意味がありません。テスターとしてプロジェクトに参画するのはもちろん、他者が書いたコードの拡張や改修する役割で参画してもあまりよい経験にはなりません(AndroidのAPIの使い方を知るという意味では意味がありますが)。自分でソフト構成を設計できるような新規案件や大きな機能ブロックを作成できる案件でないとなかなか「オブジェクト指向らしいコードを書く技術」を磨くチャンスがないのです。しかし、多くの仕事内容は、他者が書いたコードの拡張や改修する作業です。
オブジェクト指向らしいコードを書く技術を磨くには、自分で技術の本や記事を積極的に読み、実際に自分でコードを書き、頭をトレーニングする必要があります。
おすすめのトレーニング方法
「オブジェクト指向らしいコードを書く技術」を身につける為におすすめなのが「デザインパターン」と「リファクタリング」の技術を身につけることです。どちらもオブジェクト指向らしいコードを身につける為の要素がたくさん詰まっています。Android/Javaプログラマーは比較的幸運だと思います。なぜなら、結城 浩さんの良書中の良書があるからです。
- 増補改訂版Java言語で学ぶデザインパターン入門 ※最近(2021年10月現在)第3版が出版されたようです。
- Java言語で学ぶリファクタリング入門
私もこれらの本を読んだのがきっかけで、オブジェクト指向らしくコードを書こうとするようになりました。ただ良書ではあるのですが、どちらもそれなりにページ数が多い為、はじめは読むのに尻込みしてしまうかもしれません。そこで、初心者でも取っつきやすい内容を目指して本稿を書きました。
デザインパターン
デザインパターンについて、もう少し掘り下げます。デザインパターンはGoFの23パターンが有名で、上記のデザインパターン本もGoFの23パターンをベースにした本になっています。私個人の意見としては、最初は23種全部覚えるよりも、重要ないくつかのパターンを使いこなすのに時間を掛けた方がよいと思います。次のパターンを優先的にマスターするのがおすすめです。
- Template Methodパターン
- Factory Methodパターン
- Facadeパターン
- Stateパターン
- Builderパターン
- Observerパターン
この中で最初に覚えた方がよいパターンは「Template Method パターン」です。プログラミングのできる方だと、わざわざデザインパターンの名前も付ける必要もないくらいオブジェクト指向の性質そのものだと言う方もいると思います。それくらい「基本でかつ奥義」みたいなパターンです。この「Template Method パターン」について本記事を通して学んでいきましょう。
開発現場
※筆者注:導入部は物語風会話形式で書いてみました。
とあるソフト開発会社「エゥーゴソフト」開発チームの二人の会話から始まります。
登場人物説明
- ブライト(リーダー):社会人(&開発経験)5年目。新人の頃からAndroid/Java一筋で、厳しい先輩に教わったこともあり、現在では新規アプリ開発プロジェクトの立ち上げを任されている。
- アムロ(新人女性社員):大学生の頃は主にC言語でプログラミングしていた。Java言語は入門書の7割くらいは読んだが、正直オブジェクト指向と言われてもピンとこない。新人研修のときに初めてAndroidの作り方を学んだ。
ブライトさんの作成依頼
ブライトリーダー:アムロさん、こんな感じのAndroidアプリを作ってほしいんだ。(といって下記のイメージを見せる)
- アプリを起動すると背景が青い画面を表示し、中央に「次」ボタンを表示すること
- 「次」ボタンを押下すると、背景が黄色の画面->赤い画面->青い画面と切り替わること
- Backキーを押下するとアプリが終了すること(Androidアプリでデフォルトの動作のままでよい)
ブライトリーダー:アムロさんはAndroidを新人研修で教わったみたいだけど、いきなり全部作れって言うのは厳しいと思うから、途中まで(青い画面を表示して、ボタンが押された時のイベント処理をどう書けばよいのかがわかるところまで)作ってあるから、これをベースに作ってみて。
アムロ:わかりました。作ってみます。
ベースとなるコード(Javaファイル以外は省略)
サンプル:ColorSample_base
※サンプルはGithubに格納してあります。
package com.example.colorsample;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
public class BlueActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.layout).setBackgroundColor(Color.BLUE);
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 別のActivityに遷移する時の処理イメージ
// Intent intent = new Intent(BlueActivity.this, YellowActivity.class);
// intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
// startActivity(intent);
}
});
}
}
。。。。
アムロさんはAndroid Studioの操作に苦戦しつつも、アプリを作り上げました。
アムロ:できました。こんな感じでどうでしょう。(ブライトさんにAndroid端末の画面を見せながら操作する)
ブライトリーダー:いいね。動作は完璧だね。ソースコードの方を見せてもらえる?
アムロさんが作ったコード
サンプル:ColorSample_amuro
package com.example.colorsample;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
import android.widget.FrameLayout;
import androidx.appcompat.app.AppCompatActivity;
public class BlueActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.layout).setBackgroundColor(Color.BLUE);
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(BlueActivity.this, YellowActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
}
});
}
}
package com.example.colorsample;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
import android.widget.FrameLayout;
import androidx.appcompat.app.AppCompatActivity;
public class YellowActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.layout).setBackgroundColor(Color.YELLOW);
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(YellowActivity.this, RedActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
}
});
}
}
package com.example.colorsample;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
public class RedActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.layout).setBackgroundColor(Color.RED);
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(RedActivity.this, BlueActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
}
});
}
}
ブライトリーダー:(そうきたか。。。)うーん。残念だけどお仕事の成果物としては、このままのコードではダメだね。
アムロ:えー。ボクのコードの何がダメなんですか。ちゃんと仕様通り動いているじゃないですか。
ブライトリーダー:BlueActivity.javaとYellowActivity.javaとRedActivity.javaってなんか「とてもよく似ている」よね。
アムロ:まあ、BlueActivity.javaをコピペしてYellowActivityクラスとRedActivityクラスを作って、画面の色とか遷移先のActivityクラスのコードを修正したので。
ブライトリーダー:(コピペしたって言っちゃった。正直というか。。。)似たようなコードがあるってことはもっとコードを少なくして、スッキリさせることができるはずなんだ。コードで説明するとわかりずらいから、日本語に置き換えて説明しよう。
3つのクラスの動作を日本語で表現すると、次のようになります。
この中で、クラス毎に異なる部分を赤字にします。
(1)は3つのクラスとも同じですね。(2)と(3)は赤字以外の部分は同じです。
この同じ部分(重複部分)が「無駄」と言えます。無駄な部分を取り消し線で書きます。
この取り消し線の部分がなくなったら、スッキリしたコードになると思いませんか。そこで次のように、赤字の部分を〇〇と△△に置き換えます。
3つのクラスとも同じ内容になりました。
同じ内容なら1つのクラスで表現すればいいですよね。そこで3つのクラスのスーパークラス(親クラス)というものを考えてみます。
これをソースコードで表現すれば、スッキリしたコードになります(スーパークラスのクラス名をColorActivityとします)。
青色やYelloActivityのような情報を〇〇や△△に置き換えることを「抽象化」と言います。抽象化した情報を具体化するのはサブクラス(子クラス)側に任せ、スーパークラスは決めません。ソースコードを見ていきましょう。
サンプル:ColorSample_final
package com.example.colorsample;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
abstract public class ColorActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); //(1)
findViewById(R.id.layout).setBackgroundColor(getMyColor()); //(4)
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(ColorActivity.this, getNextClass()); //(5)
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
}
});
}
/**
* 自Activityの色を取得する
* @return 自Activityの色
*/
abstract protected int getMyColor(); //(2)
/**
* 次に遷移するActivity Classを取得する
* @return 次に遷移するActivity Class
*/
abstract Class getNextClass(); //(3)
}
「(1)レイアウトを「activity_main.xml」に設定する」については、3つのクラスで同じ内容でした。同じならば、親クラスで1回書けば、子クラスに書く必要はなくなります。
getMyColor()やgetNextClass()はabstractが付いています。ColorActivityはどんな色か、どんなクラスかを具体的に決めません。決めるのはサブクラスに任せます。色や次のクラスを取得し、それを(4)や(5)で使用することだけをスーパークラスで書きます。
サブクラス(子クラス)のソースコードも見ていきましょう。
package com.example.colorsample;
import android.graphics.Color;
public class BlueActivity extends ColorActivity {
@Override
protected int getMyColor() {
return Color.BLUE;
}
@Override
Class getNextClass() {
return YellowActivity.class;
}
}
package com.example.colorsample;
import android.graphics.Color;
public class YellowActivity extends ColorActivity {
@Override
protected int getMyColor() {
return Color.YELLOW;
}
@Override
Class getNextClass() {
return RedActivity.class;
}
}
package com.example.colorsample;
import android.graphics.Color;
public class RedActivity extends ColorActivity {
@Override
protected int getMyColor() {
return Color.RED;
}
@Override
Class getNextClass() {
return BlueActivity.class;
}
}
アムロ:なるほど。たしかにスッキリした感じがしますね。BlueActivityとか差分の情報しかないって感じがすごくします。サブクラスは、Android固有のメソッドが1つも出てこないんですね。。。でもこのようなコードに改造するのは大変な気がします(というかメンドクサイ)。なんでこんなことをやる必要があるのでしょうか。
ブライトリーダー:簡単に言えば
(1)「バグの量」を減らすこと
(2)クラスの役割分担を明確する
を行うことでメンテナンスしやすくなるんだ。メンテナンスしやすければ「楽に」働ける。
(1)は、ソースコードを人が書く以上、書く量が多ければ多い程ミスが出て、バグが生まれる可能性が高くなる、という考え方から来ている。だから基本的にコード量は少ない方がいいんだ。(Javaのコード量で開発工数の見積もりをしようとお客さんがいるけど、正直コード量で見積もりするのはナンセンスだよな。コピペ文化を増長するし。)
(2)は、日本語の赤字をつけた例を見ればわかると思うけど、本来BlueActivityやYelloActivityは「何色か」、「次のAcitvityクラスは何か」の2つを決めるだけでいいはずなのに黒字と赤字が混ざっている。言い換えれば、Android固有のメソッド(API)を呼ぶコード(setContentView()やstartActivity())と呼ばなくてもよいコードが混ざっている。例えば、AndroidのVersionが上がってAPIの仕様変更があって動かなくなったときにどこを修正すればいいのか調査する必要が出てきたとする。この場合、アムロさんのコードでは、BlueActivity、YelloActivity、RedActivityの3つのクラスの調査する必要がある。
<ColorSample_amuroのソフト構成>
それに比べてColorSample_finalサンプルは、ColorActivityだけを(基本的に)調査すればいいんだ。
<ColorSample_finalのソフト構成>
アムロさんには、ぜひ「ColorSample_final」のようなコードが書けるようになってほしいね。
アムロ:わかりました。がんばります!
Template Method パターンの理解を深める為のQ&A
- Q.Template Method パターンとは、何かを取得する処理を抽象化するパターンのことである。Yes or No
- A. No。抽象化の対象は動作(動詞)、つまりメソッドのことです。ただし、〇〇を取得するというメソッドの例が、Template Method パターンを理解する上で一番わかりやすい例だとは思います。
- Q. 「Parent parent = new Child();」のようなコードが出てこないんだけど、これって本当にTemplate Method パターンのサンプル?
- A. 今回のサンプルでは、「Parent parent = new Child();」の部分をAndroidが行っています。Template Method パターンの例としては、微妙かもしれませんが、メソッドを抽象化する感覚を感じて頂ければと思います。プログラミング言語の入門書を読んだだけだと、なかなか「abstract」や「interface」を自分で使ってみようという気にならないと思います。Template Method パターンを通じて、「abstract」や「interface」を自分が書いているコードに適用できないか考えてみましょう。
あとがき
ここまで読んでいただきありがとうございます。
本記事はできるだけ「オブジェクト指向の用語の説明」に傾注しないように注意して書きました。もともと数年前に、本屋で見かけたJava言語向けのオブジェクト指向の本が、あまりに現場の感覚と離れていて「使えない」と思ったのがきっかけでした。オブジェクト指向の用語を絵とか書いて(インスタンスとは~をハンコの絵で描いて説明している)伝えようとしているのですが、数学の公式をマスターするのに学校の先生が一生懸命黒板に書いても説明しても生徒が公式をマスターできないように、オブジェクト指向も自分で手と頭を使って初めて習得できるものだと思います。
本記事が少しでもオブジェクト指向の理解と実戦で活かせる助けになれば幸いです。