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

Working Effectively with Legacy Code (Droidcon SF)

613633962e5ab27cf572e7699e43d368?s=47 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.

613633962e5ab27cf572e7699e43d368?s=128

Chuck Greb

November 19, 2018
Tweet

Transcript

  1. Working Effectively with 
 (Android) 
 Legacy Code Mohit Sarveiya,

    Android @ Vimeo Chuck Greb, Android @ Button
  2. Working Effectively with (Android) Legacy Code • What is legacy

    code? • Seams • Dependency breaking techniques • Strategies for changing software
  3. Legacy Vintage, historic, nostalgia

  4. None
  5. None
  6. None
  7. None
  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<ContactApi, Void, ContactList> { 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<ContactList.Contact> { ContactArrayAdapter(Context context, int id, List<ContactList.Contact> 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; } } }
  9. Legacy Code What is legacy code? Challenge of legacy code.

  10. Problem Built many years ago Large code base Millions of

    users Large tech debt
  11. What is Legacy Code? Legacy code is untestable code.

  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… }
  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
  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
  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
  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
  17. Finding a balance Move 
 KPIs Incrementally 
 improve

  18. None
  19. Four Reasons for Changing Software

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

    Fixing a bug 3. Improving the design 4. Optimizing resource usage
  21. Refactoring The act of improving design without changing behavior.

  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
  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.
  24. Seam A place where you can alter behavior in your

    program without editing in that place.
  25. Seam Types • Pre-processing Seams • Link Seams • Object

    Seams
  26. Exercise Finding the seam

  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"); } !// !!... }
  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"); } !// !!... }
  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); } !// !!... }
  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"); } !// !!... }
  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(); } !// !!... }
  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"); } !// !!... }
  33. Dependency Breaking Techniques How do I get code under test

    without changing behavior?
  34. Parametrize Constructor Dependency Breaking Techniques

  35. public class UserProfilePresenter implements BasePresenter<UserView> { public UserProfilePresenter() { …

    } public void onProfileButtonClick() { if (user !== null) { DatabaseManager.getInstance().fetchUsers(new Callback() { public void onSuccess(User user) { view.showProfile(user); } public void onError() { } }); } } }
  36. public class UserProfilePresenter implements BasePresenter<UserView> { public UserProfilePresenter() { …

    } public void onProfileButtonClick() { if (user !== null) { DatabaseManager.getInstance().fetchUsers(new Callback() { public void onSuccess(User user) { view.showProfile(user); } public void onError() { } }); } } }
  37. public class UserProfilePresenter implements BasePresenter<UserView> { 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
  38. public class UserProfilePresenter implements BasePresenter<UserView> {
 
 private DatabaseManager databaseManager;

    public UserProfilePresenter() { … } public UserProfilePresenter(DatabaseManager databaseManager) { this.databaseManager = databaseManager; } … }
  39. public class UserProfilePresenter implements BasePresenter<UserView> {
 
 private DatabaseManager databaseManager;

    public UserProfilePresenter() { this(DatabaseManager.getInstance()); } public UserProfilePresenter(DatabaseManager databaseManager) { this.databaseManager = databaseManager; } … } Activity A Activity B Activity C Activity D
  40. public class UserProfilePresenter implements BasePresenter<UserView> { 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() { } }); } } }
  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
  42. Introduce Static Setter Dependency Breaking Techniques

  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); } }
  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; } }
  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; } }
  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; } }
  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.
  48. Instance Delegator Dependency Breaking Techniques

  49. public class VideoDetailsPresenter { public void onShowVideoDetails() { if (VideoUtils.canComment(video))

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

    { view.showCommentBox(); } if (VideoUtils.isDownloadableFile(video)) { view.showDownloadButton(); } … } }
  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) { … } … }
  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
  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
  54. public class VideoDetailsPresenter { public void onShowVideoDetails(VideoUtils videoUtils) { if

    (videoUtils.isCommentingAllowed(video)) { view.showCommentBox(); } if (videoUtils.isDownloadable(video)) { view.showDownloadButton(); } … } … }
  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.
  56. Extract Interface Dependency Breaking Techniques

  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); } … }
  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); } … }
  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() { … } … }
  60. public interface UserProvider { User getCurrentUser(); String getCurrentUserId(); }

  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() { … } … }
  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); } 
 … 

  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); } … 

  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.
  65. Extract Implementor Dependency Breaking Techniques

  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) { !//!!... } }
  67. public interface NotificationFactory { void notify(String title, String body, PendingIntent

    pendingIntent); }
  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) { !//!!... } }
  69. Extract Implementor 1. Step 1… 2. Step 2… 3. Step

    3…
  70. Dependency Breaking Techniques • Parameterize Constructor • Extract Interface •

    Instance Delegator • Introduce Static Setter • Extract Implementor
  71. Strategies for Changing Software How do I add features to

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

  73. 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 } }
  74. 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 } }
  75. HTTP Server Shared Prefs

  76. 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 } }
  77. 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 } }
  78. 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 } }
  79. 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; } }
  80. 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 } !// !!... }
  81. 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 } !// !!... }
  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.
  83. https://www.pexels.com/photo/green-leafy-plant-starting-to-grow-on-beige-racks-127713/ Sprout Class Strategies for Changing Software

  84. Password Email Password LOG IN

  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… } }
  86. Password Email Password LOG IN Forgot Password?

  87. public class LoginPresenter { private final ApiClient apiClient; public LoginPresenter(final

    ApiClient apiClient) { this.apiClient = apiClient; } } Create a seam for ApiClient Sprout a class
  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
  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… } }
  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.
  91. https://www.pexels.com/photo/burrito-chicken-delicious-dinner-461198/ Wrap Class Strategies for Changing Software

  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) { !//!!... } }
  93. public interface NotificationFactory { void notify(String title, String body, PendingIntent

    pendingIntent); }
  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) { !//!!... } }
  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")); } }
  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")); } }
  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")); } }
  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.
  99. Strategies for Changing Software • Sprout Method • Sprout Class

    • Wrap Class
  100. Conclusion Where do you go from here?

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

    30% UserSettingsPresenter.java 40%
  102. What problem is important to solve? ANRs Crashes Bugs Performance

    KPIs Roadmap
  103. Thank you! Thank you! twitter.com/heyitsmohit /ecgreb twitter.com Mohit Sarveiya Chuck

    Greb /msya github.com /ecgreb github.com