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 full-size slide

  2. Droidcon NYC 2014

    View full-size slide

  3. Droidcon SF 2016

    View full-size slide

  4. Philly Tech Week 2012

    View full-size slide

  5. Barcade Chelsea NYC

    View full-size slide

  6. Motorola Droid

    View full-size slide

  7. 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 full-size slide

  8. 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 full-size slide

  9. Legacy Code
    Code that someone else has written.

    View full-size slide

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

    View full-size slide

  11. Legacy Code
    Code without tests.

    View full-size slide

  12. The Mechanics of Change

    View full-size slide

  13. #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 full-size slide

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

    View full-size slide



  15. 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 full-size slide

  16. #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 full-size slide

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

    View full-size slide

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

    View full-size slide

  19. Exercise
    Finding the Seam

    View full-size slide

  20. 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 full-size slide

  21. 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 full-size slide

  22. 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 full-size slide

  23. 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 full-size slide

  24. 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 full-size slide

  25. Technical Debt

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  28. Strategies for Changing Software

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  31. 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 full-size slide

  32. 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 full-size slide

  33. 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 full-size slide

  34. 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 full-size slide

  35. #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 full-size slide

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

    View full-size slide

  37. 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 full-size slide

  38. 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 full-size slide

  39. 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 full-size slide

  40. #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 full-size slide

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

    View full-size slide

  42. 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 full-size slide

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

    View full-size slide

  44. 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 full-size slide

  45. 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 full-size slide

  46. #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 full-size slide

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

    View full-size slide

  48. Breaking Dependencies

    View full-size slide

  49. 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 full-size slide

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

    View full-size slide

  51. 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 full-size slide

  52. #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 full-size slide

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

    View full-size slide

  54. 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 full-size slide

  55. #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 full-size slide

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

    View full-size slide

  57. 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 full-size slide

  58. 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 full-size slide

  59. #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 full-size slide

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

    View full-size slide

  61. Final Thoughts

    View full-size slide

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

    View full-size slide



  63. 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 full-size slide

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

    View full-size slide