Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Working Effectively with (Android) Legacy Code ...

Chuck Greb
October 12, 2018

Working Effectively with (Android) Legacy Code [DevFest Ukraine 2018]

How are we going to add this new feature when the code is a mess?

We can’t change this file-- it’s too risky!

How do I test this class when it depends on X, Y, and Z?

There is not enough time to make the changes you want!

What does this code even do!?

I feel overwhelmed and it’s never going to get any better.

Android isn’t new anymore. Most applications are not greenfield projects. Many of us find ourselves in the position of working with code we did not author and which we don’t fully understand.

In the spirit of Michael Feathers' classic book, Working Effectively with Legacy Code, this talk explores the ways we can navigate, maintain, and evolve a legacy codebase. We will cover topics like testing, refactoring, architecture, and dependency breaking techniques with examples based on the speaker’s own experience over the past 9+ years as an Android engineer.

Chuck Greb

October 12, 2018
Tweet

More Decks by Chuck Greb

Other Decks in Technology

Transcript

  1. public class ContactActivity extends ListActivity { public void onCreate(Bundle savedInstanceState)

    { super.onCreate(savedInstanceState); ContactApi.instance.initContext(getApplicationContext()); ContactApi.instance.initContentResolver(getContentResolver()); ContactApi[] params = new ContactApi[] { ContactApi.instance }; LoadContactsTask mLoadContactsTask = new LoadContactsTask(); mLoadContactsTask.execute(params); } private class LoadContactsTask extends AsyncTask<ContactApi, Void, ContactList> { private final ProgressDialog dialog = new ProgressDialog(ContactActivity.this); protected void onPreExecute() { dialog.setMessage("Loading contacts..."); dialog.show(); } protected ContactList doInBackground(ContactApi... params) { return new ContactList(params[0]); } protected void onPostExecute(final ContactList contacts) { if (dialog.isShowing()) { dialog.dismiss(); } setListAdapter(new ContactArrayAdapter(ContactActivity.this, R.layout.list_item, contacts)); } } private class ContactArrayAdapter extends ArrayAdapter<ContactList.Contact> { private List<ContactList.Contact> mContacts; ContactArrayAdapter(Context context, int id, List<ContactList.Contact> contacts) { super(context, id, contacts); mContacts = contacts; // ...
  2. public abstract class ContactApi { public static final ContactApi instance;

    static { int sdkVersion = Integer.parseInt(Build.VERSION.SDK); if (sdkVersion < Build.VERSION_CODES.ECLAIR) { instance = new ContactApiSdk3(); } else { instance = new ContactApiSdk5(); } } protected Context mContext; protected ContentResolver mResolver; public void initContext(Context context) { mContext = context; } public void initContentResolver(ContentResolver contentResolver) { mResolver = contentResolver; } public abstract String getColumnId(); public abstract String getColumnContactId(); public abstract String getColumnDisplayName(); public abstract String getColumnPhoneNumber(); public abstract String getColumnEmailAddress(); public abstract String getColumnGivenName(); public abstract String getColumnFamilyName(); public abstract Cursor queryContacts(); public abstract Cursor queryPhoneNumbers(); public abstract Cursor queryEmailAddresses(); public abstract Cursor queryStructuredNames(); public abstract Bitmap queryPhotoById(long id); }
  3. #dfua Four Reasons to Change Software 1. Adding a feature

    2. Fixing a bug 3. Improving the design 4. Optimizing resource usage The Mechanics of Change
  4. “ “ Any fool can write code that computers can

    understand. Good programmers write code humans can understand. - Martin Fowler The old and new Tappan Zee bridge side by side by Andrew Dallos via Flickr Creative Commons
  5. #dfua Sensing and Separation Sensing - break dependencies to sense,

    when we can’t access values computed by code. Separation - break dependencies to separate, when we can’t get code into a test harness The Mechanics of Change
  6. Seam A place where you can alter behavior in your

    program without editing in that place.
  7. #dfua Seam Types 1. Pre-processing Seams 2. Link Seams 3.

    Object Seams The Mechanics of Change
  8. public class MainActivity extends Activity { // ... @Override protected

    void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // ... NotificationFactory notificationFactory = new NotificationFactory(this); notificationFactory.notify("Title", "Body"); } // ... }
  9. public class MainActivity extends Activity implements MainController { // ...

    @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // ... MainPresenter presenter = new MainPresenter(this); presenter.displayNotification(this); } // ... }
  10. public class MainPresenter { private MainController controller; MainPresenter(MainController controller) {

    this.controller = controller; // ... } void displayNotification(Context context) { NotificationFactory notificationFactory = new NotificationFactory(context); notificationFactory.notify("Title", "Body"); } }
  11. public class MainActivity extends Activity implements MainController { // ...

    @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // ... NotificationFactory notificationFactory = new NotificationFactory(this); MainPresenter presenter = new MainPresenter(this, notificationFactory); presenter.displayNotification(); } // ... }
  12. public class MainPresenter { private MainController controller; private NotificationFactory notificationFactory;

    MainPresenter(MainController controller, NotificationFactory notificationFactory) { this.controller = controller; this.notificationFactory = notificationFactory; // ... } void displayNotification() { notificationFactory.notify("Title", "Body"); } }
  13. #dfua Tools • Refactoring Tools (Android Studio) • Unit Tests

    • Instrumentation Tests • UI Tests (Espresso) • Fake Collaborators The Mechanics of Change
  14. public class EventTracker { private EventCache eventCache; public EventTracker(EventCache eventCache)

    { this.eventCache = eventCache; } public void trackEvents(List<Event> events) { for (Event event : events) { event.track(); // HTTP request } eventCache.getEventList().addAll(events); eventCache.save(); // Write to SharedPrefs } }
  15. public class EventTracker { private EventCache eventCache; public EventTracker(EventCache eventCache)

    { this.eventCache = eventCache; } public void trackEvents(List<Event> events) { List<Event> eventsToAdd = new ArrayList<>(); for (Event event : events) { if (!eventCache.getEventList().contains(event)) { event.track(); // HTTP request eventsToAdd.add(event); } } eventCache.getEventList().addAll(eventsToAdd); eventCache.save(); // Write to SharedPrefs } }
  16. public class EventTracker {
 // ... List<Event> uniqueEvents(List<Event> events) {

    ArrayList<Event> result = new ArrayList<>(); for (Event event : events) { if (!eventCache.getEventList().contains(event)) { result.add(event); } } return result; } }
  17. public class EventTracker { private EventCache eventCache; public EventTracker(EventCache eventCache)

    { this.eventCache = eventCache; } public void trackEvents(List<Event> events) { List<Event> eventsToAdd = uniqueEvents(events); for (Event event : eventsToAdd) { event.track(); // HTTP request } eventCache.getEventList().addAll(eventsToAdd); eventCache.save(); // Write to SharedPrefs } // ... }
  18. #dfua Sprout Method 1. Identify where you need to make

    the code change. 2. Write a new method that will do the work required. 3. Determine what local vars are needed and make them arguments. 4. Write tests for the sprout method. 5. Invoke the sprout method from the original code. Strategies for Changing Software
  19. public class LoginActivity extends Activity { @Override protected void onCreate(@Nullable

    Bundle savedInstanceState) { super.onCreate(savedInstanceState); findViewById(R.id.button_login).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { TextView username = findViewById(R.id.username); TextView password = findViewById(R.id.password); if (!username.getText().toString().isEmpty() && !password.getText().toString().isEmpty()) { ApiClient apiClient = new ApiClient(“https://api.example.com/v1/login”); apiClient.checkUser(username.getText().toString(), password.getText().toString(), new ApiClient.Listener() { @Override public void onSuccess() { startActivity(new Intent(LoginActivity.this, MainActivity.class)); } @Override public void onFailure() { Toast.makeText(LoginActivity.this, "Error", LENGTH_SHORT).show(); } }); } } }); } // ...
  20. public class LoginPresenter { private final ApiClient apiClient; LoginPresenter(ApiClient apiClient)

    { this.apiClient = apiClient; } void onClickForgotPassword(String username) { apiClient.forgotPassword(username, new ApiClient.Listener() { @Override public void onSuccess() { // Do nothing } @Override public void onFailure() { // Log error } }); } }
  21. public class LoginActivity extends Activity { @Override protected void onCreate(@Nullable

    Bundle savedInstanceState) { super.onCreate(savedInstanceState); // ... final ApiClient apiClient = new ApiClient("https://api.example.com/v1"); final LoginPresenter loginPresenter = new LoginPresenter(apiClient); final TextView username = findViewById(R.id.username); findViewById(R.id.forgot_password).setOnClickListener( new View.OnClickListener() { @Override public void onClick(View v) { loginPresenter.onClickForgotPassword(username.getText().toString()); } }); } // ... }
  22. #dfua Sprout Class 1. Identify where you need to make

    the code change. 2. Write a new class that will do the work required. 3. Determine what local vars are needed and make them arguments to the constructor or a public method. 4. Write tests for the sprout class. 5. Invoke the sprout class from the original code. Strategies for Changing Software
  23. public class NotificationFactory { private static final int NOTIFICATION_ID =

    0x01; private Context context; public NotificationFactory(Context context) { this.context = context; } public void notify(String title, String body, PendingIntent pendingIntent) { Notification notification = new Notification.Builder(context) .setSmallIcon(R.drawable.ic_notification_small) .setContentTitle(title) .setContentText(body) .setAutoCancel(true) .setContentIntent(pendingIntent) .build(); NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(NOTIFICATION_ID, notification); } }
  24. public class SimpleNotificationFactory implements NotificationFactory { private static final int

    NOTIFICATION_ID = 0x01; private Context context; public SimpleNotificationFactory(Context context) { this.context = context; } @Override public void notify(String title, String body, PendingIntent pendingIntent) { Notification notification = new Notification.Builder(context) .setSmallIcon(R.drawable.ic_notification_small) .setContentTitle(title) .setContentText(body) .setAutoCancel(true) .setContentIntent(pendingIntent) .build(); NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.notify(NOTIFICATION_ID, notification); } }
  25. public class TrackingNotificationFactory implements NotificationFactory { private NotificationFactory notificationFactory; public

    TrackingNotificationFactory(NotificationFactory notificationFactory) { this.notificationFactory = notificationFactory; } @Override public void notify(String title, String body, PendingIntent pendingIntent) { trackEvent(); notificationFactory.notify(title, body, pendingIntent); } private void trackEvent() { // ... } }
  26. #dfua Wrap Class Strategies for Changing Software 1. Identify method

    where you need to make a change. 2. Create a class that accepts the class you are going to wrap as a constructor argument and delegate method calls to original class. 3. Create a method on the new class that does the work. 4. Instantiate the wrapper class where the new behavior is needed.
  27. #dfua More Strategies • Wrap Method • Effect Sketches •

    Inception Point • Pinch Point • Delete Dead Code Strategies for Changing Software
  28. public class UserPresenter { private final UserController controller; private UserProfile

    user; public UserPresenter(UserController controller) { this.controller = controller; } public void onProfileButtonClick(Context context) { if (user == null) { user = DatabaseManager.getInstance().fetchUser(context); } controller.showProfile(user); } }
  29. public class UserPresenter { private final UserController controller; private UserProfile

    user; public UserPresenter(UserController controller) { this.controller = controller; } public void onProfileButtonClick(Context context) { onProfileButtonClick(context, DatabaseManager.getInstance().fetchUser(context); } public void onProfileButtonClick(Context context, DatabaseManager databaseManager) { if (user == null) { user = databaseManager.fetchUser(context); } controller.showProfile(user); } }
  30. #dfua Parameterize Method 1. Identify the method you want to

    replace and make a copy of it. 2. Add a parameter to the method for the object to be replaced. 3. Delete the body of the copied method and make a call to the parameterized method. Breaking Dependencies
  31. public class UserPresenter { private final UserController controller; private final

    DatabaseManager databaseManager; private UserProfile user; public UserPresenter(UserController controller) { this(controller, DatabaseManager.getInstance()); } public UserPresenter(UserController controller, DatabaseManager databaseManager) { this.controller = controller; this.databaseManager = databaseManager; } public void onProfileButtonClick(Context context) { if (user == null) { user = databaseManager.fetchUser(context); } controller.showProfile(user); } }
  32. #dfua Parameterize Constructor 1. Identify the constructor to parameterize and

    make copy. 2. Add a parameter to the new constructor. Remove the object creation and add assign the parameter to an instance variable. 3. Remove the body of the old constructor and replace with a call to the new constructor. Breaking Dependencies
  33. public class DatabaseManager { private static final DatabaseManager instance =

    new DatabaseManager(); public static DatabaseManager getInstance() { return instance; } private DatabaseManager() { } public UserProfile fetchUser(Context context) { UserProfile user = new UserProfile(); // Fetch user profile data from SQLite database... return user; } }
  34. public class DatabaseManager { private static DatabaseManager instance = new

    DatabaseManager(); public static DatabaseManager getInstance() { return instance; } @VisibleForTesting public static void setTestInstance(DatabaseManager instance) { DatabaseManager.instance = instance; } protected DatabaseManager() { } public UserProfile fetchUser(Context context) { UserProfile user = new UserProfile(); // Fetch user profile data from SQLite database... return user; } }
  35. #dfua Introduce Static Setter 1. Decrease the protection on the

    constructor from private to protected so you can subclass and override. 2. Make the static instance mutable be removing keyword final. 3. Add a test-only method to set the instance in the test environment. Breaking Dependencies
  36. #dfua More Dependency Breaking Techniques • Extract Interface • Extract

    Implementer • Subclass and Override Method • Extract and Override Factory Method • Introduce Instance Delegator Breaking Dependencies
  37. #dfua How do I know I’m not breaking anything? •

    Motivation • Single-Goal Editing • Refactoring Tools • Pair Programming • Tests! Final Thoughts
  38. “ “ Remember, your code is your house, and you

    have to live in it. - Michael Feathers https://www.pexels.com/photo/architecture-facade-house-lawn-259600/