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;
...

執行測試!!
螢幕擷圖存為 2016-02-19 17:47:46
通過測試。
接著觀察 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());
    }

啟動測試!!
螢幕擷圖存為 2016-02-22 14:24:01
完成這次重構後,onCreate() 函式內容的註解將被具有命名的函式取代掉,且其內容的函式呼叫會維持在相同的抽象層次, e.g.,

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_layout);
        initAJConnectBtn();
        initCurrentNetwork();
        initAnnouncedNames();
        initDeviceDetector();
        registerDeviceDetector();
    }