Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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. Chuck Greb
    Mobile Engineer @ Button
    Working Effectively with
    (Android) Legacy Code

    View Slide

  2. Intro

    View Slide

  3. Droidcon NYC 2014

    View Slide

  4. Droidcon SF 2016

    View Slide

  5. Philly Tech Week 2012

    View Slide

  6. Legacy

    View Slide

  7. Barcade Chelsea NYC

    View Slide

  8. View Slide

  9. View Slide

  10. Motorola Droid

    View Slide

  11. 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 {
    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 {
    private List mContacts;
    ContactArrayAdapter(Context context, int id, List contacts) {
    super(context, id, contacts);
    mContacts = contacts;
    // ...

    View Slide

  12. 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);
    }

    View Slide

  13. Legacy Code

    View Slide

  14. Legacy Code
    Code that someone else has written.

    View Slide

  15. Legacy Code
    Code that is difficult to change and that we don’t understand.

    View Slide

  16. Legacy Code
    Code without tests.

    View Slide

  17. The Mechanics of Change

    View Slide

  18. #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

    View Slide

  19. Refactoring

    View Slide

  20. Refactoring
    The act of improving design without changing behaviour.

    View Slide



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

    View Slide

  22. #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

    View Slide

  23. Seam

    View Slide

  24. Seam
    A place where you can alter behavior in your program without editing in that place.

    View Slide

  25. #dfua
    Seam Types
    1. Pre-processing Seams
    2. Link Seams
    3. Object Seams
    The Mechanics of Change

    View Slide

  26. Exercise

    View Slide

  27. Exercise
    Finding the Seam

    View Slide

  28. 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");
    }
    // ...
    }

    View Slide

  29. 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);
    }
    // ...
    }

    View Slide

  30. 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");
    }
    }

    View Slide

  31. 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();
    }
    // ...
    }

    View Slide

  32. 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");
    }
    }

    View Slide

  33. Technical Debt

    View Slide

  34. Technical Debt
    The refactoring effort needed to add a feature non-invasively.

    View Slide

  35. #dfua
    Tools
    • Refactoring Tools (Android Studio)
    • Unit Tests
    • Instrumentation Tests
    • UI Tests (Espresso)
    • Fake Collaborators
    The Mechanics of Change

    View Slide

  36. Strategies for Changing Software

    View Slide

  37. https://www.pexels.com/photo/blue-concrete-pavement-with-100m-sprint-paint-60230/
    Where do
    I start?

    View Slide

  38. https://www.pexels.com/photo/shallow-focus-photography-of-brown-tree-trunk-1250260/
    Sprout Method
    Strategies for Changing Software

    View Slide

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

    View Slide

  40. public class EventTracker {
    private EventCache eventCache;
    public EventTracker(EventCache eventCache) {
    this.eventCache = eventCache;
    }
    public void trackEvents(List events) {
    List 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
    }
    }

    View Slide

  41. public class EventTracker {

    // ...
    List uniqueEvents(List events) {
    ArrayList result = new ArrayList<>();
    for (Event event : events) {
    if (!eventCache.getEventList().contains(event)) {
    result.add(event);
    }
    }
    return result;
    }
    }

    View Slide

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

    View Slide

  43. #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

    View Slide

  44. https://www.pexels.com/photo/green-leafy-plant-starting-to-grow-on-beige-racks-127713/
    Sprout Class
    Strategies for Changing Software

    View Slide

  45. 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();
    }
    });
    }
    }
    });
    }
    // ...

    View Slide

  46. 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
    }
    });
    }
    }

    View Slide

  47. 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());
    }
    });
    }
    // ...
    }

    View Slide

  48. #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

    View Slide

  49. https://www.pexels.com/photo/burrito-chicken-delicious-dinner-461198/
    Wrap Class
    Strategies for Changing Software

    View Slide

  50. 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);
    }
    }

    View Slide

  51. public interface NotificationFactory {
    void notify(String title, String body, PendingIntent pendingIntent);
    }

    View Slide

  52. 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);
    }
    }

    View Slide

  53. 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() {
    // ...
    }
    }

    View Slide

  54. #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.

    View Slide

  55. #dfua
    More Strategies
    • Wrap Method
    • Effect Sketches
    • Inception Point
    • Pinch Point
    • Delete Dead Code
    Strategies for Changing Software

    View Slide

  56. Breaking Dependencies

    View Slide

  57. 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);
    }
    }

    View Slide

  58. https://www.pexels.com/photo/gray-scale-photo-of-gears-159298/
    Parameterize Method
    Breaking Dependencies

    View Slide

  59. 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);
    }
    }

    View Slide

  60. #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

    View Slide

  61. https://www.pexels.com/photo/newly-make-high-rise-building-162557/
    Parameterize Constructor
    Breaking Dependencies

    View Slide

  62. 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);
    }
    }

    View Slide

  63. #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

    View Slide

  64. https://www.pexels.com/photo/ball-ball-shaped-blur-daylight-269057/
    Introduce Static Setter
    Breaking Dependencies

    View Slide

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

    View Slide

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

    View Slide

  67. #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

    View Slide

  68. #dfua
    More Dependency Breaking Techniques
    • Extract Interface
    • Extract Implementer
    • Subclass and Override Method
    • Extract and Override Factory Method
    • Introduce Instance Delegator
    Breaking Dependencies

    View Slide

  69. Final Thoughts

    View Slide

  70. #dfua
    How do I know I’m not breaking anything?
    • Motivation
    • Single-Goal Editing
    • Refactoring Tools
    • Pair Programming
    • Tests!
    Final Thoughts

    View Slide



  71. 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/

    View Slide

  72. Chuck Greb
    @ecgreb
    github.com/ecgreb
    medium.com/@ecgreb
    Questions?
    Thank you!

    View Slide