コードのリズム?
私はパワーメタルが好きですが、STRATOVARIUS聴いてるとコードがリズムよくかけるとか、そういう話ではなくてコードの流れとかまとまりとかの話です。
コードの対称性
私は、名著とかプログラマなら絶対に読むべきとか言われている、リーダブルコードとか、達人プログラマとかは天邪鬼なんで読んでません。
そんな私が読んで、気づきが多かったのが実装パターンです。私が買ったのが数年前ですが、すでに廃刊になってしまっているようで、見つけるのすごい大変でした。ちなみに池袋のどっかの本屋で買いました。
実装パターンにコードの対称性という概念の話が載っています。
これ、ものすごくなるほどなーと思う反面、すごく難しいなーと。あと、訳の問題もあると思うんですが、これってコードのリズムとか粒度なんじゃないかと思ってるんです。
コードを追加するときや修正するときに、粒度がそろっていると、どこに修正コードを入れるべきなのかがわかりやすくなるのでコードの粒度は意識したいところです。
以下、幾つか例をあげていってこんなときどう書くか?を考えていきます。
普段、Androidのコードを書くことが多いので、Androidのコードを例にとります。
また、使うライブラリによって、かなりコードが変わるので素のAndroidのコードにします。
なお、コードの粒度の話なので、コードの細かい指摘はご簡便を・・・
初期化
RegistrationActivityで、以下の要件を満たすコードをどう書くか?
- 名前、住所はあらかじめPreferencesに保存されている
- 2つのTextViewを上記の名前、住所で初期化する
public class RegistrationActivity extends Activity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// どう書く?
}
}
プログラミングの初心者だと以下のような感じでしょうか。
public class RegistrationActivity extends Activity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
SharedPreferences data = getSharedPreferences("", Context.MODE_PRIVATE);
String name = data.getString("name", "");
String address = data.getString("address", "");
// 名前の初期化
TextView nameTextView = (TextView)findViewById(R.id.-----);
nameTextView.setText(name);
// 住所の初期化
TextView addressTextView = (TextView)findViewById(R.id.-----);
addressTextView.setText(address);
}
}
これくらいなら、そんな悪くない気がしますね。電話番号が追加になっても、どこにコードを差し込めばいいかわかりやすいです。
ただ、年齢、趣味、年収、好きな言語とか、項目が増えてくるとonCreateがどんどん肥大化しますね。
そこで、TextViewの初期化をメソッドで切り離しましょう。
public class RegistrationActivity extends Activity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
SharedPreferences data = getSharedPreferences("", Context.MODE_PRIVATE);
String name = data.getString("name", "");
String address = data.getString("address", "");
this.initViews(name, address);
}
private void initViews(String name, String address) {
// 名前の初期化
TextView nameTextView = (TextView)findViewById(R.id.-----);
nameTextView.setText(name);
// 住所の初期化
TextView addressTextView = (TextView)findViewById(R.id.-----);
addressTextView.setText(address);
}
}
これで、onCreateは少しすっきりしました。ですが、項目が増えたとき、今度はinitViewsが肥大化します。
TextViewの初期化も項目ごとに切り離しましょう。
public class RegistrationActivity extends Activity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
SharedPreferences data = getSharedPreferences("", Context.MODE_PRIVATE);
String name = data.getString("name", "");
String address = data.getString("address", "");
this.initViews(name, address);
}
private void initViews(String name, String address) {
this.initNameTextView(name);
this.initAddressTextView(address);
}
private void initNameTextView(String name) {
....
}
private void initAddressTextView(String address) {
....
}
}
メソッド名は処理の内容を表すので、ここまでくるとコメントもいらないですね。
項目が増えたとき、今度はinitViewsへの引数が増える気がします、引数が多いメソッドは使いにくいので、今度はSharedPreferencesのインスタンスをinitViewsに渡すようにしましょう。
public class RegistrationActivity extends Activity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
SharedPreferences data = getSharedPreferences("", Context.MODE_PRIVATE);
this.initViews(data);
}
private void initViews(SharedPreferences data) {
String name = data.getString("name", "");
String address = data.getString("address", "");
this.initNameTextView(name);
this.initAddressTextView(address);
}
private void initNameTextView(String name) {
....
}
private void initAddressTextView(String address) {
....
}
}
これで、onCreateで何の初期化をしているのかがわかりやすくなった気がします。
SharedPreferenceの値を使用してビューを初期化するということが一目瞭然です。
また、項目が増えてもinit???メソッドを作成して、initViewsメソッドから作成したinit???メソッドを呼び出せばいいことがわかりやすいです。
ここまでやって、項目が増えたときにonCreateで増えた項目の初期化を書く人がいたら相当空気が読めない人です。
通常だとここまででいい気がしますが、粒度をあわせるという意味だと、SharedPreferencesから情報を取ってきているところがちょっと気になります・・・
いっそのこと、init???へSharedPreferencesを渡してしまいましょうか。
public class RegistrationActivity extends Activity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
SharedPreferences data = getSharedPreferences("", Context.MODE_PRIVATE);
this.initViews(data);
}
private void initViews(SharedPreferences data) {
this.initNameTextView(data);
this.initAddressTextView(data);
}
private void initNameTextView(SharedPreferences pref) {
TextView textView = (TextView)findViewById(R.id.-----);
textView.setText(pref.getString("name", ""));
}
private void initAddressTextView(SharedPreferences pref) {
....
}
}
粒度はあってきた気がします。初期化という意味ではここら辺が限界でしょうか。
ただ、初期化だけにとらわれずにTextViewへの変更というようにもっと一般化するとすると、もう一段階考える必要があるかもしれません。
update???メソッドを用意してinit???からupdateを呼び出します。
public class RegistrationActivity extends Activity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView()R.layout.activity_main;
SharedPreferences data = getSharedPreferences("", Context.MODE_PRIVATE);
this.initViews(data);
}
private void initViews(SharedPreferences data) {
this.initNameTextView(data);
this.initAddressTextView(data);
}
private void initNameTextView(SharedPreferences pref) {
this.updateNameTextView(pref.getString("name", ""));
}
private void updateNameTextView(String name) {
TextView textView = (TextView)findViewById(R.id.-----);
textView.setText(name);
}
}
かなり一般化もできた気がします。
やろうと思うと、もう一段階くらい一般化できる気がしますが、コードの粒度という意味ではそろった気がするのでここまでで。
コールバック
Android開発をしているとコールバックメソッドを書くことも多いので、コールバックメソッドについても考えてみたいと思います。
今回の要件はこんな感じで。
- APIを呼び出して帰ってきたときに呼ばれるonSuccessメソッド
- onSuccessの引数にはアクセストークンと秘密鍵の入ったAuthオブジェクトがわたってくる
- Authオブジェクトに入ったアクセストークンと秘密鍵をTextViewで表示する
public RegistrationActivity extends Activity implements ApiCallback {
public void onSuccess(Auth auth) {
// どう書く?
}
}
Androidはじめたばかりだと以下のような感じでしょうか。
public RegistrationActivity extends Activity implements ApiCallback {
public void onSuccess(Auth auth) {
// アクセストークンの表示
TextView accessTokenTextView = (TextView)findViewById(R.id.---);
accessTokenTextView.setText(auth.accessToken);
// 秘密鍵の表示
TextView secretKeyTextView = (TextView)findViewById(R.id.---);
secretKeyTextView.setText(auth.secretKey);
}
}
メソッド名って処理の名前をつける場合が普通だと思いますが、onSuccessって「成功時」って感じで、処理の名前って感じじゃないんですよね。処理を全部くくりだしてしまいましょう。
public RegistrationActivity extends Activity implements ApiCallback {
public void onSuccess(Auth auth) {
this.updateAuthViews(auth);
}
public void updateAuthViews(Auth auth) {
// アクセストークンの表示
TextView accessTokenTextView = (TextView)findViewById(R.id.---);
accessTokenTextView.setText(auth.accessToken);
// 秘密鍵の表示
TextView secretKeyTextView = (TextView)findViewById(R.id.---);
secretKeyTextView.setText(auth.secretKey);
}
}
こうしたら、初期化の例と同じで、TextViewの更新をひとつずつに切り分けます。
public RegistrationActivity extends Activity implements ApiCallback {
public void onSuccess(Auth auth) {
this.updateAuthViews(auth);
}
private void updateAuthViews(Auth auth) {
this.updateAccessTokentextView(auth.accessToken);
this.updateSecretKey(auth.secretKey);
}
private void updateAccessTokenTextView(String accessToken) {
TextView accessTokenTextView = (TextView)findViewById(R.id.---);
accessTokenTextView.setText(auth.accessToken);
}
private void updateSecretKeyTextView(String secretKey) {
TextView secretKeyTextView = (TextView)findViewById(R.id.---);
secretKeyTextView.setText(secretKey);
}
}
粒度はそろった気がします。
ここで、アクセストークンと秘密鍵をSharedPreferencesに保存したいという用件が出てきたら、どこに処理を追加するべきでしょうか?
APIからレスポンスが返ってきたときに何が起きるのか一目でわかるのがいいような気がします、ですからonSuccessに書きましょう。
public RegistrationActivity extends Activity implements ApiCallback {
public void onSuccess(Auth auth) {
this.updateAuthView(auth);
// レスポンスを保存
SharedPreferences.Editor editor = getSharedPreferences("", Context.MODE_PRIVATE).edit();
editor.put("accessToken", auth.accessToken);
editor.put("secretKey", auth.secretKey);
editor.commit();
}
private updateAuthViews(Auth auth) {}
}
ここまで読むとわかると思いますが、これ、粒度があってないですよね、SharedPreferencesへ保存する処理をくくりだしましょう。
public RegistrationActivity extends Activity implements ApiCallback {
public void onSuccess(Auth auth) {
this.updateAuthView(auth);
this.saveAuth(auth);
}
private updateAuthViews(Auth auth) {...}
private saveAuth(Auth auth) {
SharedPreferences.Editor editor = getSharedPreferences("", Context.MODE_PRIVATE).edit();
editor.put("accessToken", auth.accessToken);
editor.put("secretKey", auth.secretKey);
editor.commit();
}
}
これで、APIアクセスが成功したときに何が起こるのか一目でわかるようになり、かつ粒度もそろいました。
まとめ
Android開発始めたばかりのコードから、少しずつ粒度をあわせて、処理の追加や変更がしやすいコードに改善をしていきました。
粒度をあわせることはコードの可読性の観点からとても重要ですが、アーキテクチャ設計やライブラリ選定など他のことに気をとられて忘れがちです。開発初期段階で粒度をあわせておかないと、途中からあわせるのはとても大変です。
今回のコード、これが正解だというつもりもまったくなくて、もっとうまく粒度をあわせたコードの書き方があると思います。
重要なのはどのレベルの粒度でコードを書いていくか合わせることです。
はじめから粒度を合わせるのは難しく、私もリファクタリングを細かくしていく中であわせていくことが多いです、もし、今までコードの粒度を意識したことがなかったら1度意識して開発してみてはどうでしょうか、おそらく今までよりも保守のしやすいコードになると思います。