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

Working Effectively with Legacy Code (Droidcon SF)

Chuck Greb
November 19, 2018

Working Effectively with Legacy Code (Droidcon SF)

* 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 on code we didn't author and which we don’t fully understand.

In the spirit of Michael Feathers' classic book, this talk explores ways we can navigate, maintain, improve and evolve Android legacy code. We will cover topics like architecture, refactoring, testing, and dependency breaking techniques using examples from the speakers’ combined 16+ years of experience working on Android.

Chuck Greb

November 19, 2018
Tweet

More Decks by Chuck Greb

Other Decks in Technology

Transcript

  1. Working Effectively with 

    (Android) 

    Legacy Code
    Mohit Sarveiya, Android @ Vimeo

    Chuck Greb, Android @ Button

    View Slide

  2. Working Effectively with (Android) Legacy Code
    • What is legacy code?

    • Seams

    • Dependency breaking techniques

    • Strategies for changing software

    View Slide

  3. Legacy
    Vintage, historic, nostalgia

    View Slide

  4. View Slide

  5. View Slide

  6. View Slide

  7. View Slide

  8. public class ContactActivity extends ListActivity {
    private static boolean isFirstLoad = false;
    @Override 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);
    @Override protected void onPreExecute() {
    if (!dialog.isShowing()) {
    if (isFirstLoad) {
    dialog.setMessage("Loading contacts!!...");
    } else {
    dialog.setMessage("Updating contacts!!...");
    }
    dialog.show();
    }
    }
    @Override protected ContactList doInBackground(ContactApi!!... params) {
    return new ContactList(params[0]);
    }
    @Override 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 {
    ContactArrayAdapter(Context context, int id, List contacts) {
    super(context, id, contacts);
    }
    @Override public View getView(int position, View convertView, ViewGroup parent) {
    if (convertView !== null) {
    convertView = getLayoutInflater().inflate(R.layout.list_item, null);
    }
    !// !!...
    return convertView;
    }
    }
    }

    View Slide

  9. Legacy Code
    What is legacy code? Challenge of legacy code.

    View Slide

  10. Problem
    Built many years ago

    Large code base

    Millions of users

    Large tech debt

    View Slide

  11. What is Legacy Code?
    Legacy code is untestable code.

    View Slide

  12. public class LoginActivity extends BaseAuthActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_login);
    final EventCache eventCache = new EventCache();
    LoginManager loginManager = new LoginManager();
    SessionManager sessionManager = SessionManager.getInstance();
    if (!sessionManager.viewOnboardingScreen()) {
    loginManager.login(email, password, new LoginManager.Callback() {
    @Override
    public void onSuccess(User user) {
    Event event = new Event(LOGIN_SUCCESS);
    for (Event cachedEvent : eventCache.getEvents()) {
    if (cachedEvent.getType() !!= event.getType()) {
    event.track();
    eventCache.add(event);
    }
    }
    }
    }

    !//1000 lines of more code…
    }

    View Slide

  13. public class LoginActivity extends BaseAuthActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_login);
    final EventCache eventCache = new EventCache();
    LoginManager loginManager = new LoginManager();
    SessionManager sessionManager = SessionManager.getInstance();
    if (!sessionManager.viewOnboardingScreen()) {
    loginManager.login(email, password, new LoginManager.Callback() {
    @Override
    public void onSuccess(User user) {
    Event event = new Event(LOGIN_SUCCESS);
    for (Event cachedEvent : eventCache.getEvents()) {
    if (cachedEvent.getType() !!= event.getType()) {
    event.track();
    eventCache.add(event);
    }
    }
    }
    !//1000 lines of more code…
    Singletons

    View Slide

  14. public class LoginActivity extends BaseAuthActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_login);
    final EventCache eventCache = new EventCache();
    LoginManager loginManager = new LoginManager();
    SessionManager sessionManager = SessionManager.getInstance();
    if (!sessionManager.viewOnboardingScreen()) {
    loginManager.login(email, password, new LoginManager.Callback() {
    @Override
    public void onSuccess(User user) {
    Event event = new Event(LOGIN_SUCCESS);
    for (Event cachedEvent : eventCache.getEvents()) {
    if (cachedEvent.getType() !!= event.getType()) {
    event.track();
    eventCache.add(event);
    }
    }
    }
    !//1000 lines of more code…
    Break Dependencies

    View Slide

  15. public class LoginActivity extends BaseAuthActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_login);
    final EventCache eventCache = new EventCache();
    LoginManager loginManager = new LoginManager();
    SessionManager sessionManager = SessionManager.getInstance();
    if (!sessionManager.viewOnboardingScreen()) {
    loginManager.login(email, password, new LoginManager.Callback() {
    @Override
    public void onSuccess(User user) {
    Event event = new Event(LOGIN_SUCCESS);
    for (Event cachedEvent : eventCache.getEvents()) {
    if (cachedEvent.getType() !!= event.getType()) {
    event.track();
    eventCache.add(event);
    }
    }
    }
    !//1000 lines of more code…
    Nested Logic

    View Slide

  16. public class LoginActivity extends BaseAuthActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_login);
    final EventCache eventCache = new EventCache();
    LoginManager loginManager = new LoginManager();
    SessionManager sessionManager = SessionManager.getInstance();
    if (!sessionManager.viewOnboardingScreen()) {
    loginManager.login(email, password, new LoginManager.Callback() {
    @Override
    public void onSuccess(User user) {
    Event event = new Event(LOGIN_SUCCESS);
    for (Event cachedEvent : eventCache.getEvents()) {
    if (cachedEvent.getType() !!= event.getType()) {
    event.track();
    eventCache.add(event);
    }
    }
    }
    }
    !//1000 lines of more code…

    }
    Deep Inheritance Hierarchies
    LoginActivity
    BaseAuthActivity
    Activity C
    Activity D
    Activity E
    BaseActivity

    View Slide

  17. Finding a balance
    Move 

    KPIs
    Incrementally 

    improve

    View Slide

  18. View Slide

  19. Four Reasons for Changing
    Software

    View Slide

  20. Four Reasons for Changing Software
    1. Adding a feature

    2. Fixing a bug

    3. Improving the design

    4. Optimizing resource usage

    View Slide

  21. Refactoring
    The act of improving design without changing behavior.

    View Slide



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

  23. Sensing and Separation
    Sensing - break dependencies to sense behavior or values,

    when we can’t access values computed by code.

    Separation - break dependencies to separate components,

    when we can’t get code into a test harness.

    View Slide

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

    View Slide

  25. Seam Types
    • Pre-processing Seams

    • Link Seams

    • Object Seams

    View Slide

  26. Exercise
    Finding the seam

    View Slide

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

  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. Dependency Breaking
    Techniques
    How do I get code under test without changing behavior?

    View Slide

  34. Parametrize Constructor
    Dependency Breaking Techniques

    View Slide

  35. public class UserProfilePresenter implements BasePresenter {
    public UserProfilePresenter() {

    }
    public void onProfileButtonClick() {
    if (user !== null) {
    DatabaseManager.getInstance().fetchUsers(new Callback() {
    public void onSuccess(User user) {
    view.showProfile(user);
    }
    public void onError() {
    }
    });
    }
    }
    }

    View Slide

  36. public class UserProfilePresenter implements BasePresenter {
    public UserProfilePresenter() {

    }
    public void onProfileButtonClick() {
    if (user !== null) {
    DatabaseManager.getInstance().fetchUsers(new Callback() {
    public void onSuccess(User user) {
    view.showProfile(user);
    }
    public void onError() {
    }
    });
    }
    }
    }

    View Slide

  37. public class UserProfilePresenter implements BasePresenter {
    public UserProfilePresenter() {

    }
    public void onProfileButtonClick() {
    if (user !== null) {
    DatabaseManager.getInstance().fetchUsers(new Callback() {
    public void onSuccess(User user) {
    view.showProfile(user);
    }
    public void onError() {
    }
    });
    }
    }
    }
    Activity A
    Activity B
    Activity C
    Activity D

    View Slide

  38. public class UserProfilePresenter implements BasePresenter {


    private DatabaseManager databaseManager;
    public UserProfilePresenter() {

    }
    public UserProfilePresenter(DatabaseManager databaseManager) {
    this.databaseManager = databaseManager;
    }

    }

    View Slide

  39. public class UserProfilePresenter implements BasePresenter {


    private DatabaseManager databaseManager;
    public UserProfilePresenter() {
    this(DatabaseManager.getInstance());
    }
    public UserProfilePresenter(DatabaseManager databaseManager) {
    this.databaseManager = databaseManager;
    }

    }
    Activity A
    Activity B
    Activity C
    Activity D

    View Slide

  40. public class UserProfilePresenter implements BasePresenter {
    private DatabaseManager databaseManager;
    public UserProfilePresenter() {
    this(DatabaseManager.getInstance());
    }
    public UserProfilePresenter(DatabaseManager databaseManager) {
    this.databaseManager = databaseManager;
    }
    public void onProfileButtonClick() {
    if (user !== null) {
    databaseManager.fetchUsers(new Callback() {
    public void onSuccess(User user) {
    view.showProfile(user);
    }
    public void onError() {
    }
    });
    }
    }
    }

    View Slide

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

    Parametrize Constructor

    View Slide

  42. Introduce Static Setter
    Dependency Breaking Techniques

    View Slide

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

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

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

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

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

    View Slide

  48. Instance Delegator
    Dependency Breaking Techniques

    View Slide

  49. public class VideoDetailsPresenter {
    public void onShowVideoDetails() {
    if (VideoUtils.canComment(video)) {
    view.showCommentBox();
    }
    if (VideoUtils.isDownloadableFile(video)) {
    view.showDownloadButton();
    }

    }
    }

    View Slide

  50. public class VideoDetailsPresenter {
    public void onShowVideoDetails() {
    if (VideoUtils.canComment(video)) {
    view.showCommentBox();
    }
    if (VideoUtils.isDownloadableFile(video)) {
    view.showDownloadButton();
    }

    }
    }

    View Slide

  51. public class VideoUtils {
    public static Boolean isDRM(Video video) { … }
    public static Boolean isPublic(Video video) { … }
    public static Boolean canComment(Video video) { … }
    public static Boolean shouldMarkAsPrivate(Video video) { … }
    public static Boolean isHlsFile(Video video) { … }
    public static Boolean isDownloadableFile(Video video) { … }

    }

    View Slide

  52. public class VideoUtils {
    public VideoUtils() { … }
    public Boolean isCommentingAllowed(Video video) {
    return canComment(video);
    }
    public static Boolean canComment(Video video) { … }
    public Boolean isDownloadable(Video video) {
    return isDownloadableFile(video);
    }
    public static boolean isDownloadableFile(Video video) { … }

    }
    Create public constructor

    View Slide

  53. public class VideoUtils {
    public VideoUtils() { … }
    public Boolean isCommentingAllowed(Video video) {
    return canComment(video);
    }
    public static Boolean canComment(Video video) { … }
    public Boolean isDownloadable(Video video) {
    return isDownloadableFile(video);
    }
    public static boolean isDownloadableFile(Video video) { … }

    }
    Create instance delegator
    Create instance delegator

    View Slide

  54. public class VideoDetailsPresenter {
    public void onShowVideoDetails(VideoUtils videoUtils) {
    if (videoUtils.isCommentingAllowed(video)) {
    view.showCommentBox();
    }
    if (videoUtils.isDownloadable(video)) {
    view.showDownloadButton();
    }

    }

    }

    View Slide

  55. Instance Delegator
    1. Identify a static method that is problematic to use in a test.

    2. Create an instance method for the method on the class.

    3. Use Parametrize method technique to supply an instance of to
    the location where the static method call was made.

    View Slide

  56. Extract Interface
    Dependency Breaking Techniques

    View Slide

  57. public class VideoSettingsPresenter {
    private VideoSettingsView view;
    public void updatePrivacySettings() {
    final User user = AuthenticationHelper.getInstance().getCurrentUser();
    if (user.getAccountType() !== AccountType.PRO) {
    view.showUpgradeButtonForHide();
    view.showUpgradeButtonShareLink();
    }
    }
    public void onSaveVideoSettings() {
    final String currentUserId = AuthenticationHelper.getInstance().getCurrentUserId();
    apiClient.saveVideoSettings(userId, videoSettings, callback);
    }

    }

    View Slide

  58. public class VideoSettingsPresenter {
    private VideoSettingsView view;
    public void updatePrivacySettings() {
    final User user = AuthenticationHelper.getInstance().getCurrentUser();
    if (user.getAccountType() !== AccountType.PRO) {
    view.showUpgradeButtonForHide();
    view.showUpgradeButtonShareLink();
    }
    }
    public void onSaveVideoSettings() {
    final String currentUserId = AuthenticationHelper.getInstance().getCurrentUserId();
    apiClient.saveVideoSettings(userId, videoSettings, callback);
    }

    }

    View Slide

  59. public class AuthenticationHelper {
    private User user;
    public void loginWithEmail(String email, String password) { … }
    public void loginOut() { … }
    public void trackAuthEvent() { … }
    public User getCurrentUser() { … }
    public User getCurrentUserId() { … }

    }

    View Slide

  60. public interface UserProvider {
    User getCurrentUser();
    String getCurrentUserId();
    }

    View Slide

  61. public class AuthenticationHelper implements UserProvider {
    private User user;
    public void loginWithEmail(String email, String password) { … }
    public void loginOut() { … }
    public void trackAuthEvent() { … }
    @Override
    public User getCurrentUser() { … }
    @Override
    public User getCurrentUserId() { … }

    }

    View Slide

  62. public class VideoSettingsPresenter {
    final UserProvider userProvider;
    public VideoSettingsPresenter(final UserProvider userProvider) {
    this.userProvider = userProvider;
    }
    public void updatePrivacySettings() {
    final User user = userProvider.getCurrentUser();
    if (user.getAccountType() !== AccountType.PRO) {
    view.showUpgradeButtonForHide();
    view.showUpgradeButtonShareLink();
    }
    }
    public void onSaveVideoSettings() {
    final String currentUserId = userProvider.getCurrentUserId();
    apiClient.saveVideoSettings(userId, videoSettings, callback);
    } 



    View Slide

  63. public class VideoSettingsPresenter {
    final UserProvider userProvider;
    public VideoSettingsPresenter(final UserProvider userProvider) {
    this.userProvider = userProvider;
    }
    public void updatePrivacySettings() {
    final User user = userProvider.getCurrentUser();
    if (user.getAccountType() !== AccountType.PRO) {
    view.showUpgradeButtonForHide();
    view.showUpgradeButtonShareLink();
    }
    }
    public void onSaveVideoSettings() {
    final String currentUserId = userProvider.getCurrentUserId();
    apiClient.saveVideoSettings(userId, videoSettings, callback);
    }


    View Slide

  64. Extract Interface
    1. Create a new interface with the name you’d like to use.

    2. Make a class that you are extracting from implement the
    interface.

    3. Change the place where you want to use the object so that it
    uses the interface rather than the original class.

    4. Compile the system and introduce a new method declaration
    on the interface for each method.

    View Slide

  65. Extract Implementor
    Dependency Breaking Techniques

    View Slide

  66. 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) {
    !//!!...
    }
    }

    View Slide

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

    View Slide

  68. 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) {
    !//!!...
    }
    }

    View Slide

  69. Extract Implementor
    1. Step 1…

    2. Step 2…

    3. Step 3…

    View Slide

  70. Dependency Breaking Techniques
    • Parameterize Constructor

    • Extract Interface

    • Instance Delegator

    • Introduce Static Setter

    • Extract Implementor

    View Slide

  71. Strategies for Changing
    Software
    How do I add features to a legacy code base?

    View Slide

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

    View Slide

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

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

  75. HTTP

    Server
    Shared

    Prefs

    View Slide

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

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

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

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

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

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

  82. Sprout Method
    1. Identify where you need to make the code change.

    2. Write a new method that will do the work required.

    3. Pass any local variables needed as argument(s).

    4. Write tests for the sprout method.

    5. Invoke the sprout method from the original code.

    View Slide

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

    View Slide

  84. Password
    Email
    Password
    LOG IN

    View Slide

  85. public class LoginActivity extends BaseAuthActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    loginButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {

    if (!username.getText().toString().isEmpty() !&& !password.getText().toString().isEmpty()) {
    ApiClient apiClient = new ApiClient("https:!//api.example.com/v1/login");
    apiClient.login(username, password, new Callback() {
    @Override
    public void onSuccess() {
    startActivity(new Intent(LoginActivity.this, MainActivity.class));
    }
    @Override
    public void onFailure() {
    Toast.makeText(LoginActivity.this, "Error", LENGTH_SHORT).show();
    }
    });
    }
    }
    });

    !//10000 lines of more code…
    }
    }

    View Slide

  86. Password
    Email
    Password
    LOG IN
    Forgot Password?

    View Slide

  87. public class LoginPresenter {
    private final ApiClient apiClient;
    public LoginPresenter(final ApiClient apiClient) {
    this.apiClient = apiClient;
    }
    }
    Create a seam for ApiClient
    Sprout a class

    View Slide

  88. public class LoginPresenter {
    private final ApiClient apiClient;
    public LoginPresenter(final ApiClient apiClient) {
    this.apiClient = apiClient;
    }
    public void onClickForgotPassword(String username) {
    apiClient.forgotPassword(username, new ApiClient.Callback() {
    @Override
    public void onSuccess() {
    }
    @Override
    public void onError() {
    }
    });
    }
    }
    New feature

    View Slide

  89. public class LoginActivity extends AppCompatActivity {
    @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);
    forgotPasswordButton.setOnClickListener(
    new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    loginPresenter.onClickForgotPassword(usernameTextView);
    }
    });
    }
    !//1000 lines of more code…
    }
    }

    View Slide

  90. Sprout Class
    1. Identify dependencies needed for your new logic.

    2. Create another class for the new feature. Inject the needed
    dependencies.

    3. Update legacy code to use the sprout class.

    View Slide

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

    View Slide

  92. 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) {
    !//!!...
    }
    }

    View Slide

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

    View Slide

  94. 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) {
    !//!!...
    }
    }

    View Slide

  95. public class TrackingNotificationFactory implements NotificationFactory {
    private NotificationFactory notificationFactory;
    private EventTracker eventTracker;
    public TrackingNotificationFactory(NotificationFactory notificationFactory,
    EventTracker eventTracker) {
    this.notificationFactory = notificationFactory;
    this.eventTracker = eventTracker;
    }
    @Override
    public void notify(String title, String body, PendingIntent pendingIntent) {
    trackEvent();
    notificationFactory.notify(title, body, pendingIntent);
    }
    private void trackEvent() {
    eventTracker.trackEvent(new Event(“notification-shown"));
    }
    }

    View Slide

  96. public class TrackingNotificationFactory implements NotificationFactory {
    private NotificationFactory notificationFactory;
    private EventTracker eventTracker;
    public TrackingNotificationFactory(NotificationFactory notificationFactory,
    EventTracker eventTracker) {
    this.notificationFactory = notificationFactory;
    this.eventTracker = eventTracker;
    }
    @Override
    public void notify(String title, String body, PendingIntent pendingIntent) {
    trackEvent();
    notificationFactory.notify(title, body, pendingIntent);
    }
    private void trackEvent() {
    eventTracker.trackEvent(new Event(“notification-shown"));
    }
    }

    View Slide

  97. public class TrackingNotificationFactory implements NotificationFactory {
    private NotificationFactory notificationFactory;
    private EventTracker eventTracker;
    public TrackingNotificationFactory(NotificationFactory notificationFactory,
    EventTracker eventTracker) {
    this.notificationFactory = notificationFactory;
    this.eventTracker = eventTracker;
    }
    @Override
    public void notify(String title, String body, PendingIntent pendingIntent) {
    trackEvent();
    notificationFactory.notify(title, body, pendingIntent);
    }
    private void trackEvent() {
    eventTracker.trackEvent(new Event(“notification-shown"));
    }
    }

    View Slide

  98. Wrap Class
    1. Identify existing class or method that needs to change.

    2. Create a new class that accepts the existing class as a
    constructor argument.

    3. Delegate existing method calls to original class.

    4. Create a new method on the new class that does the work.

    5. Instantiate the wrapper class where the new behavior is
    needed.

    View Slide

  99. Strategies for Changing Software
    • Sprout Method

    • Sprout Class

    • Wrap Class

    View Slide

  100. Conclusion
    Where do you go from here?

    View Slide

  101. Code Coverage
    20%
    LoC covered
    Filename Covered
    LoginManager.java 10%
    UserProfilePresenter.java 30%
    UserSettingsPresenter.java 40%

    View Slide

  102. What problem is important to solve?
    ANRs
    Crashes
    Bugs
    Performance
    KPIs
    Roadmap

    View Slide

  103. Thank you!
    Thank you!
    twitter.com/heyitsmohit /ecgreb
    twitter.com
    Mohit Sarveiya Chuck Greb
    /msya
    github.com /ecgreb
    github.com

    View Slide