ConfigClient 為 Alljoyn 的一個小專案,由於規模不大適合拿來作重構介紹。
重構過程會放到 github 方便觀察其內容。
首先 ConfigClient 有三個 class,各為 MainActivity,ConfigApplication , ConfigActivity。
我們先從 MainActivity 開始。
在開始重構之前先為 MainActivity 建立 UnitTest Project , 確保重構沒有破壞原本的功能。
建議使用 wizard 來建立 Android Test Project(ATP) , 預設的 ATP 會和被測專案分開建立避免弄亂專案結構。
完成 ATP 的建立後,必須新增 UnitTest class e.g.
public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity> { private MainActivity mMainActivity; private BroadcastReceiver mBroadcastReceiver; private Button mAJConnectBtn; private TextView mCurrentNetworkView; private ArrayAdapter<Device> mDeviceAdapter; public MainActivityTest() { super(MainActivity.class); } @Override protected void setUp() throws Exception { super.setUp(); setActivityInitialTouchMode(true); mMainActivity = getActivity(); initLayoutComponents(); } private void initLayoutComponents() { mAJConnectBtn = (Button) mMainActivity.findViewById(R.id.AllJoynConnect); mCurrentNetworkView = (TextView) mMainActivity.findViewById(R.id.current_network_name); } public void testMainActivityNotNull() { assertNotNull(mMainActivity); } }
由於還沒開始重構,先測試 MainActivity 不為空值,其他的測項會在重構過程中一一加入。
開始重構!!!
首先來觀察 MainActivity 的欄位,e.g.
public class MainActivity extends Activity implements OnCreateContextMenuListener { protected static final String TAG = ConfigApplication.TAG; private BroadcastReceiver m_receiver; private Button m_AJConnect; private TextView m_currentNetwork; ArrayAdapter<Device> deviceAdapter; ...
第3行的 TAG 用來印出訊息,目的通常為 Debug 用。但一般來說 TAG 的值都為該類別名稱,以了解該類別內的流程順序。
因此將它改為MainActivity,把存取權限改為 private 。
觀察其他的欄位發現命名規則並沒有遵循一定的規則,這裡導入 android 的命名規則,參考這裡 android code style
因此修改以下欄位名稱
m_receiver -> mReceiver
m_AJConnect -> mAJConnectBtn
m_CurrentNetwork -> mCurrentNetWorkView
deviceAdapter 的存取範圍只在MainActivity 內,因此順便修改存取權限為 private
deviceAdapter -> mDeviceAdapter
完成後如下
public class MainActivity extends Activity implements OnCreateContextMenuListener { private static final String TAG = "MainActivity"; private BroadcastReceiver mReceiver; private Button mAJConnectBtn; private TextView mCurrentNetworkView; private ArrayAdapter<Device> mDeviceAdapter; ...
執行測試!!
通過測試。
接著觀察 onCreate() 函式,它太長了,很明顯出現 “Long Method(過長函式)” 的壞味道。
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main_layout); ConfigApplication application = (ConfigApplication) getApplication(); // ************** AllJoyn Connect/Disconnect Button ****************** mAJConnectBtn = (Button) findViewById(R.id.AllJoynConnect); mAJConnectBtn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (mAJConnectBtn.getText().equals(getString(R.string.AllJoynConnect))) { allJoynConnect(); } else if (mAJConnectBtn.getText().equals(getString(R.string.AllJoynDisconnect))) { allJoynDisconnect(); } } }); // ***************** Current Network ********************* mCurrentNetworkView = (TextView) findViewById(R.id.current_network_name); String ssid = application.getCurrentSSID(); mCurrentNetworkView.setText(getString(R.string.current_network, ssid)); // ************** Announced names list ****************** ListView listView = (ListView) findViewById(R.id.device_list); mDeviceAdapter = new ArrayAdapter<Device>(this, android.R.layout.simple_list_item_1); listView.setAdapter(mDeviceAdapter); listView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Device selectedDevice = mDeviceAdapter.getItem(position); Intent intent = new Intent(MainActivity.this, ConfigActivity.class); intent.putExtra(ConfigApplication.EXTRA_DEVICE_ID, selectedDevice.appId); startActivity(intent); } }); // ************** Class receiver ****************** mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (ConfigApplication.ACTION_DEVICE_FOUND.equals(intent.getAction()) || ConfigApplication.ACTION_DEVICE_LOST.equals(intent.getAction())) { mDeviceAdapter.clear(); HashMap<UUID, Device> deviceList = ((ConfigApplication) getApplication()).getDeviceList(); if (deviceList == null) { return; } addAllDevices(deviceList.values()); } else if (ConfigApplication.ACTION_CONNECTED_TO_NETWORK.equals(intent.getAction())) { String ssid = intent.getStringExtra(ConfigApplication.EXTRA_NETWORK_SSID); mCurrentNetworkView.setText(getString(R.string.current_network, ssid)); } } }; IntentFilter filter = new IntentFilter(); filter.addAction(ConfigApplication.ACTION_DEVICE_FOUND); filter.addAction(ConfigApplication.ACTION_DEVICE_LOST); filter.addAction(ConfigApplication.ACTION_CONNECTED_TO_NETWORK); registerReceiver(mReceiver, filter); }
使用 ”Extract Method(提取函式)” 來重構它。
提取哪段呢?? 可以看到其內使用了註釋來說明部份區塊的功能,事實上這些註釋都可以使用具有描述性名稱的函式來取代。
因此我們先提取第9~21行,並命名為 initAJConnectBtn() 函式。
private void initAJConnectBtn() { mAJConnectBtn = (Button) findViewById(R.id.AllJoynConnect); mAJConnectBtn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (mAJConnectBtn.getText().equals(getString(R.string.AllJoynConnect))) { allJoynConnect(); } else if (mAJConnectBtn.getText().equals(getString(R.string.AllJoynDisconnect))) { allJoynDisconnect(); } } }); }
刪除註釋並使用函式取代原區塊。
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main_layout); ConfigApplication application = (ConfigApplication) getApplication(); initAJConnectBtn(); ...
接著準備提取第24~26行,在提取之前先來看看可能發生的問題。
在第26行需要 ssid 參數傳入,因此如果直接提取第24~26行就必須建立參數並傳入 e.g.
private void initCurrentNetwork(ConfigApplication application) { mCurrentNetworkView = (TextView) findViewById(R.id.current_network_name); String ssid = application.getCurrentSSID(); mCurrentNetworkView.setText(getString(R.string.current_network, ssid)); }
但真的需要 application 參數嗎??
先回到未提取前的狀態來看,第25行的 ssid 是藉由 application.getCurrentSSID() 來取得。
而 application 則是在第6行的 (ConfigApplication) getApplication() 而來。
但在第6行的 application 區域變數除了供給第25行取得 ssid 外,沒有再做其他的事情。
因此可以使用 “Inline temp (內連化暫時變數)” 處理 ssid e.g.
private void initCurrentNetwork() { mCurrentNetworkView = (TextView) findViewById(R.id.current_network_name); mCurrentNetworkView.setText(getString(R.string.current_network, ((ConfigApplication) getApplication()).getCurrentSSID())); }
在原本onCreate的第6行 ConfigApplication application = (ConfigApplication) getApplication(); 也可以刪除了
接著我們提取第29~42行並命名為 initAnnouncedNames() 函式 e.g.,
private void initAnnouncedNames() { ListView listView = (ListView) findViewById(R.id.device_list); mDeviceAdapter = new ArrayAdapter<Device>(this, android.R.layout.simple_list_item_1); listView.setAdapter(mDeviceAdapter); listView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Device selectedDevice = mDeviceAdapter.getItem(position); Intent intent = new Intent(MainActivity.this, ConfigActivity.class); intent.putExtra(ConfigApplication.EXTRA_DEVICE_ID, selectedDevice.appId); startActivity(intent); } }); }
最後提取45~72行,分為2個函式,首先45~67行為初始化動作而68~72則是註冊的動作。
提取45~67為 initDeviceDetector 函式,而68~72為 registerDeviceDetector 函式,e.g.
private void initDeviceDetector() { mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (ConfigApplication.ACTION_DEVICE_FOUND.equals(intent.getAction()) || ConfigApplication.ACTION_DEVICE_LOST.equals(intent.getAction())) { mDeviceAdapter.clear(); HashMap<UUID, Device> deviceList = ((ConfigApplication) getApplication()).getDeviceList(); if (deviceList == null) { return; } addAllDevices(deviceList.values()); } else if (ConfigApplication.ACTION_CONNECTED_TO_NETWORK.equals(intent.getAction())) { String ssid = intent.getStringExtra(ConfigApplication.EXTRA_NETWORK_SSID); mCurrentNetworkView.setText(getString(R.string.current_network, ssid)); } } }; } private void registerDeviceDetector() { IntentFilter filter = new IntentFilter(); filter.addAction(ConfigApplication.ACTION_DEVICE_FOUND); filter.addAction(ConfigApplication.ACTION_DEVICE_LOST); filter.addAction(ConfigApplication.ACTION_CONNECTED_TO_NETWORK); registerReceiver(mReceiver, filter); }
因為本次的重構改動到 Layout Component,因此需要在 Unit Test 加入關於 Layout Component 的測項。e.g.
public void testAJConnectBtnStringCorrect() { assertEquals(getActivity().getString(R.string.AllJoynConnect), mAJConnectBtn.getText().toString()); } public void testCurrentNetWorkViewStringCorrect() { String expect =getActivity().getString(R.string.current_network, ((ConfigApplication) getActivity().getApplication()).getCurrentSSID()); assertEquals(expect, mCurrentNetworkView.getText().toString()); }
啟動測試!!
完成這次重構後,onCreate() 函式內容的註解將被具有命名的函式取代掉,且其內容的函式呼叫會維持在相同的抽象層次, e.g.,
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main_layout); initAJConnectBtn(); initCurrentNetwork(); initAnnouncedNames(); initDeviceDetector(); registerDeviceDetector(); }