67
実装パターン リファクタリングでよく使う7つの基本的な実装パターン

Implementation patterns

Embed Size (px)

Citation preview

実装パターンリファクタリングでよく使う7つの基本的な実装パターン

牧 竜也Androidアプリ開発エンジニア

#1

public SearchRequest(String query, String lang, String order, String sort) {

this(query, lang, order, sort, LIMIT, PAGE);}

public SearchRequest(String query, String lang, String order, String sort, int limit, int page) {

mUrl = buildUrl(query, lang, order, sort, limit, page);}

パラメータの多いコンストラクタ

同じ型が連続すると 型チェックに弱い

public SearchRequest(String query, String lang, String order, String sort) {

this(query, lang, order, sort, LIMIT, PAGE);}

public SearchRequest(String query, String lang, String order, String sort, int limit, int page) {

mUrl = buildUrl(query, lang, order, sort, limit, page);}

任意のパラメータも 与える必要がある

public SearchRequest(String query, String lang, String order, String sort) {

this(query, lang, order, sort, LIMIT, PAGE);}

public SearchRequest(String query, String lang, String order, String sort, int limit, int page) {

mUrl = buildUrl(query, lang, order, sort, limit, page);}

Builder Pattern

public class Builder { private final String mQuery;

private String mOrder; private String mSort; private String mLang;

public Builder(String query) { mQuery = query; }

public Builder setOrder(String order) { mOrder = order; return this; }

public Builder setSort(String sort) { mSort = sort; return this; }

...

public SearchRequest build() { return new SearchRequest(this); }}

強制するのは 必須パラメータのみ

任意パラメータは メソッドから与える

public class Builder { private final String mQuery;

private String mOrder; private String mSort; private String mLang;

public Builder(String query) { mQuery = query; }

public Builder setOrder(String order) { mOrder = order; return this; }

public Builder setSort(String sort) { mSort = sort; return this; }

...

public SearchRequest build() { return new SearchRequest(this); }}

Request request = new SearchRequest.Builder("Java") .setLang("ja") .setOrder("date") .setSort("desc") .setLimit(100) .setPage(2) .build();

インスタンス生成時の 可読性が向上

インスタンス生成時の可読性が向上

#2

public void setPadding(int left, int top, int right, int bottom) { ...}

パラメータの多いメソッド

public void setPadding(int left, int top, int right, int bottom) { ...}

同じ型が連続すると 型チェックに弱い

Parameter Object

public class Padding { public int left; public int top; public int right; public int bottom;}

関連するパラメータを 1つのクラスとして定義

Padding padding = new Padding();padding.left = 10;padding.top = 20;padding.right = 10;padding.bottom = 20;

object.setPadding(padding);

切り出したクラスをパラメータとして与える

メソッド呼び出し時の可読性が向上

#3

public int getItemCount() { if (mCursor == null) { return 0; } return mCursor.getCount();}

public long getItemId(int position) { if (mCursor == null) { return NO_ID; } mCursor.moveToPosition(position); return mCursor.getLong(mRowIdColumn);}

public Cursor changeCursor(Cursor cursor) { Cursor oldCursor = mCursor; mCursor = cursor; return oldCursor;}

Nullableな フィールドを持つクラス

Nullになりうる 引数をそのまま受け取る

public int getItemCount() { if (mCursor == null) { return 0; } return mCursor.getCount();}

public long getItemId(int position) { if (mCursor == null) { return NO_ID; } mCursor.moveToPosition(position); return mCursor.getLong(mRowIdColumn);}

public Cursor changeCursor(Cursor cursor) { Cursor oldCursor = mCursor; mCursor = cursor; return oldCursor;}

public int getItemCount() { if (mCursor == null) { return 0; } return mCursor.getCount();}

public long getItemId(int position) { if (mCursor == null) { return NO_ID; } mCursor.moveToPosition(position); return mCursor.getLong(mRowIdColumn);}

public Cursor changeCursor(Cursor cursor) { Cursor oldCursor = mCursor; mCursor = cursor; return oldCursor;}

Nullチェックが点在し 実装が複雑化

Null Object

public class NullCursor implements Cursor {

@Override public int getCount() { return 0; }

@Override public int getPosition() { return 0; }

@Override public boolean move(int offset) { return true; }

@Override public boolean moveToPosition(int position) { return true; }

...

}

適切な振舞をする Null Objectを定義

@Overridepublic int getItemCount() { return mCursor.getCount();}

@Overridepublic long getItemId(int position) { mCursor.moveToPosition(position); return mCursor.getLong(mRowIdColumn);}

public Cursor changeCursor(Cursor cursor) { Cursor oldCursor = mCursor; if (mCursor == null) { mCursor = new NullCursor(); } else { mCursor = cursor; } return oldCursor;}

Nullが与えられたら Null Objectを代わりに利用

@Overridepublic int getItemCount() { return mCursor.getCount();}

@Overridepublic long getItemId(int position) { mCursor.moveToPosition(position); return mCursor.getLong(mRowIdColumn);}

public Cursor changeCursor(Cursor cursor) { Cursor oldCursor = mCursor; if (mCursor == null) { mCursor = new NullCursor(); } else { mCursor = cursor; } return oldCursor;}

他の処理では Nullチェックが不要

Nullチェックが減りコードがシンプルに

#4

public class UpdateTask extends Task {

@Override public void execute() throws TaskException { SQLiteDatabase db = mHelper.getWritableDatabase(); try { db.beginTransaction(); db.update(TABLE, mValues, mWhere, mArgs); db.setTransactionSuccessful(); } catch (SQLiteException e) { throw new TaskException(e); } finally { db.endTransaction(); } }}

DB操作など 定型的な処理が多いクラス

責務ごとに 複数のクラスが存在

public class DeleteTask extends Task {

@Override public void execute() throws TaskException { SQLiteDatabase db = mHelper.getWritableDatabase(); try { db.beginTransaction(); db.delete(TABLE, mWhere, mArgs); db.setTransactionSuccessful(); } catch (SQLiteException e) { throw new TaskException(e); } finally { db.endTransaction(); } }}

複数のサブクラスで似たような処理を実装

public class XxxTask extends Task {

@Override public void execute() throws TaskException { SQLiteDatabase db = mHelper.getWritableDatabase(); try { db.beginTransaction(); db.doXxx(); db.setTransactionSuccessful(); } catch (SQLiteException e) { throw new TaskException(e); } finally { db.endTransaction(); } }}

Templete Method

public abstract class SQLiteTask extends Task {

public final void execute() throws TaskException { SQLiteDatabase db = mHelper.getWritableDatabase(); try { db.beginTransaction(); runInTx(db); db.setTransactionSuccessful(); } catch (SQLiteException e) { throw new TaskException(e); } finally { db.endTransaction(); } }

protected abstract void runInTx(SQLiteDatabase db) throws SQLiteException;}

定型処理を抽象クラスで定義

public abstract class SQLiteTask extends Task { {

public final void execute() throws TaskException { SQLiteDatabase db = mHelper.getWritableDatabase(); try { db.beginTransaction(); runInTx(db); db.setTransactionSuccessful(); } catch (SQLiteException e) { throw new TaskException(e); } finally { db.endTransaction(); } }

protected abstract void runInTx(SQLiteDatabase db) throws SQLiteException;}

サブクラスの担当は 抽象メソッドとして定義

public class UpdateTask extends SQLiteTask {

@Override protected void runInTx(SQLiteDatabase db) throws SQLiteException { db.update(TABLE, mValues, mWhere, mArgs); }}

定型処理以外は 各サブクラスで実装

public class DeleteTask extends SQLiteTask {

@Override protected void runInTx(SQLiteDatabase db) throws SQLiteException { db.delete(TABLE, mWhere, mArgs); }}

定型処理以外は 各サブクラスで実装

重複コードがなくなりシンプルに

#5

public class ViewAnimator { private final Animation mAnimation; private final Animator mAnimator;

public ViewAnimator(int from, int to) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { mAnimation = ValueAnimation.ofInt(from, to); mAnimator = null; } else { mAnimation = null; mAnimator = ValueAnimator.ofInt(from, to); } }

...

public void start() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { mView.start(mAnimation); } else { mAnimator.start(); } }}

APIバージョンに応じて 異なるAPIやクラスを切替

バージョン判定が点在し クラスやメソッドを切替

public class ViewAnimator { private final Animation mAnimation; private final Animator mAnimator;

public ViewAnimator(int from, int to) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { mAnimation = ValueAnimation.ofInt(from, to); mAnimator = null; } else { mAnimation = null; mAnimator = ValueAnimator.ofInt(from, to); } }

...

public void start() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { mView.start(mAnimation); } else { mAnimator.start(); } }}

Adapter Pattern

互換性担保のための共通インターフェースを定義

public abstract class ViewAnimator {

public ViewAnimator(int from, int to) { ... }

public abstract void setDuration(long duration);

public abstract void setInterpolator(Interpolator i);

public abstract void start();}

各サブクラスは 複数ではなく単一責務を負う

public class OldViewAnimator extends ViewAnimator { private Animation mAnimation; private View mView;

...

@Override public void setDuration(long duration) { mAnimation.setDuration(duration); }

@Override public void setInterpolator(Interpolator i) { mAnimation.setInterpolator(i); }

@Override public void start() { mView.startAnimation(mAnimation); }}

public class NewViewAnimator extends ViewAnimator { private Animator mAnimator; private View mView;

...

@Override public void setDuration(long duration) { mAnimator.setDuration(duration); }

@Override public void setInterpolator(Interpolator i) { mAnimator.setInterpolator(i); }

@Override public void start() { mAnimator.start(); }}

各サブクラスは 複数ではなく単一責務を負う

public static ViewAnimator create(int version, int from, int to) { if (version < Build.VERSION_CODES.HONEYCOMB) { return new OldViewAnimator(from, to); } else { return new NewViewAnimator(from, to); }}

インスタンスの生成は Factory methodで隠蔽

統一されたインターフェースでシンプルに

#6

@Overridepublic boolean onOptionsItemSelected(MenuItem item) { int itemId = item.getItemId(); if (itemId == R.id.action_refresh) { refresh(); return true; } else if (itemId == R.id.action_edit) { edit(); return true; } else if (itemId == R.id.action_delete) { delete(); return true; } else if (itemId == R.id.action_clear) { clear(); return true; } return super.onOptionsItemSelected(item);}

条件によって 処理をディスパッチ

条件が増えると 条件分岐が肥大化

@Overridepublic boolean onOptionsItemSelected(MenuItem item) { int itemId = item.getItemId(); if (itemId == R.id.action_refresh) { refresh(); return true; } else if (itemId == R.id.action_edit) { edit(); return true; } else if (itemId == R.id.action_delete) { delete(); return true; } else if (itemId == R.id.action_clear) { clear(); return true; } else if (itemId == R.id.action_setting) { showSetting(); return true; } else if (itemId == R.id.action_help) { showHelp(); return true; } return super.onOptionsItemSelected(item);}

private void refresh() {...

}

private void edit() {...

}

private void delete() {...

}

private void clear() {...

}

private void showSetting() {...

}

private void showHelp() {...

}

メソッドも増えて クラスも肥大化

Command Pattern

public interface Command {boolean execute();

}

各処理に共通する インターフェースを定義

public class RefreshCommand implements Command {

@Override public boolean execute() { ... return true; }}

処理ごとに サブクラスを作成

mCommands = new SparseArray<>();mCommands.append( R.id.action_edit, new EditCommand());mCommands.append( R.id.action_delete, new DeleteCommand());mCommands.append( R.id.action_clear, new ClearCommand());mCommands.append( R.id.action_refresh, new RefreshCommand());

コマンドの識別子がキーの コマンドマップを定義

@Overridepublic boolean onOptionsItemSelected(MenuItem item) { int itemId = item.getItemId(); Command command = mCommands.get( itemId, new NullCommand()); return command.execute(getApplicationContext());}

コマンドマップから コマンドを引き当てて実行

巨大な条件分岐がなくなりシンプルに

#7

public void changeState(int state) { if (state == STATE_LOADING) { mLoadingView.setVisibility(View.VISIBLE); mSuccessView.setVisibility(View.GONE); mFailureView.setVisibility(View.GONE); } else if (state == STATE_SUCCESS) { mLoadingView.setVisibility(View.GONE); mSuccessView.setVisibility(View.VISIBLE); mFailureView.setVisibility(View.GONE); } else if (state == STATE_FAILURE) { mLoadingView.setVisibility(View.GONE); mSuccessView.setVisibility(View.GONE); mFailureView.setVisibility(View.VISIBLE); }}

Viewの表示切替などで 複雑な条件分岐

public void changeState(int state) { if (state == STATE_LOADING) { mLoadingView.setVisibility(View.VISIBLE); mSuccessView.setVisibility(View.GONE); mFailureView.setVisibility(View.GONE); } else if (state == STATE_SUCCESS) { mLoadingView.setVisibility(View.GONE); mSuccessView.setVisibility(View.VISIBLE); mFailureView.setVisibility(View.GONE); } else if (state == STATE_FAILURE) { mLoadingView.setVisibility(View.GONE); mSuccessView.setVisibility(View.GONE); mFailureView.setVisibility(View.VISIBLE); } else if (state == STATE_SETTING) { ... } else if (state == STATE_INITIAL) { ... }}

状態が増えると 条件分岐が更に肥大化

Double Dispatch

public enum State { LOADING { @Override void change(StatefulFrameLayout layout) { layout.getLoadingView() .setVisibility(View.VISIBLE); layout.getSuccessView() .setVisibility(View.GONE); layout.getFailureView() .setVisibility(View.GONE); } }, SUCCESS { @Override void change(StatefulFrameLayout layout) { layout.getLoadingView() .setVisibility(View.GONE); layout.getSuccessView() .setVisibility(View.VISIBLE); layout.getFailureView() .setVisibility(View.GONE); } }, ... };

abstract void change(StatefulFrameLayout layout);}

抽象メソッド化して サブクラスに実装を強制

public enum State { LOADING { @Override void change(StatefulFrameLayout layout) { layout.getLoadingView() .setVisibility(View.VISIBLE); layout.getSuccessView() .setVisibility(View.GONE); layout.getFailureView() .setVisibility(View.GONE); } }, SUCCESS { @Override void change(StatefulFrameLayout layout) { layout.getLoadingView() .setVisibility(View.GONE); layout.getSuccessView() .setVisibility(View.VISIBLE); layout.getFailureView() .setVisibility(View.GONE); } }, ... };

abstract void change(StatefulFrameLayout layout);}

実際の処理は サブクラスや列挙型で実装

public enum State { LOADING { @Override void change(StatefulFrameLayout layout) { layout.getLoadingView() .setVisibility(View.VISIBLE); layout.getSuccessView() .setVisibility(View.GONE); layout.getFailureView() .setVisibility(View.GONE); } }, SUCCESS { @Override void change(StatefulFrameLayout layout) { layout.getLoadingView() .setVisibility(View.GONE); layout.getSuccessView() .setVisibility(View.VISIBLE); layout.getFailureView() .setVisibility(View.GONE); } }, ... };

abstract void change(StatefulFrameLayout layout);}

状態ごとに サブクラスや列挙型を定義

public void changeState(State state) { state.change(this);}

自身をパラメータとして メソッドを呼び出す

肥大化しがちな条件分岐がシンプルに

まとめ

実装パターンを適切に使って 効果的にリファクタリング