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

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

    • Seams

    • Dependency breaking techniques

    • Strategies for changing software

    View full-size slide

  3. Legacy
    Vintage, historic, nostalgia

    View full-size slide

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

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

    View full-size slide

  6. Problem
    Built many years ago

    Large code base

    Millions of users

    Large tech debt

    View full-size slide

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

    View full-size slide

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

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

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

  11. 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 full-size 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…

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

    View full-size slide

  13. Finding a balance
    Move 

    KPIs
    Incrementally 

    improve

    View full-size slide

  14. Four Reasons for Changing
    Software

    View full-size slide

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

    2. Fixing a bug

    3. Improving the design

    4. Optimizing resource usage

    View full-size slide

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

    View full-size slide



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

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

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

    View full-size slide

  20. Seam Types
    • Pre-processing Seams

    • Link Seams

    • Object Seams

    View full-size slide

  21. Exercise
    Finding the seam

    View full-size slide

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

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

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

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

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

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

  28. Dependency Breaking
    Techniques
    How do I get code under test without changing behavior?

    View full-size slide

  29. Parametrize Constructor
    Dependency Breaking Techniques

    View full-size slide

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

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

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

  33. public class UserProfilePresenter implements BasePresenter {


    private DatabaseManager databaseManager;
    public UserProfilePresenter() {

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

    }

    View full-size slide

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

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

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

  37. Introduce Static Setter
    Dependency Breaking Techniques

    View full-size slide

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

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

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

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

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

  43. Instance Delegator
    Dependency Breaking Techniques

    View full-size slide

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

    }
    }

    View full-size slide

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

    }
    }

    View full-size slide

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

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

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

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

    }

    }

    View full-size slide

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

  51. Extract Interface
    Dependency Breaking Techniques

    View full-size slide

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

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

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

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

    View full-size slide

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

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

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

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

  60. Extract Implementor
    Dependency Breaking Techniques

    View full-size slide

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

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

    View full-size slide

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

  64. Extract Implementor
    1. Step 1…

    2. Step 2…

    3. Step 3…

    View full-size slide

  65. Dependency Breaking Techniques
    • Parameterize Constructor

    • Extract Interface

    • Instance Delegator

    • Introduce Static Setter

    • Extract Implementor

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

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

  70. HTTP

    Server
    Shared

    Prefs

    View full-size slide

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

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

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

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

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

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

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

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

  79. Password
    Email
    Password
    LOG IN

    View full-size slide

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

  81. Password
    Email
    Password
    LOG IN
    Forgot Password?

    View full-size slide

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

    View full-size slide

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

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

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

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

    View full-size slide

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

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

    View full-size slide

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

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

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

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

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

  94. Strategies for Changing Software
    • Sprout Method

    • Sprout Class

    • Wrap Class

    View full-size slide

  95. Conclusion
    Where do you go from here?

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide