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
  2. Working Effectively with (Android) Legacy Code • What is legacy

    code? • Seams • Dependency breaking techniques • Strategies for changing software
  3. 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; } } }
  4. 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… }
  5. 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
  6. 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
  7. 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
  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…
 } Deep Inheritance Hierarchies LoginActivity BaseAuthActivity Activity C Activity D Activity E BaseActivity
  9. Four Reasons for Changing Software 1. Adding a feature 2.

    Fixing a bug 3. Improving the design 4. Optimizing resource usage
  10. “ “ 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
  11. 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.
  12. Seam A place where you can alter behavior in your

    program without editing in that place.
  13. 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"); } !// !!... }
  14. 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"); } !// !!... }
  15. 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); } !// !!... }
  16. 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"); } !// !!... }
  17. 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(); } !// !!... }
  18. 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"); } !// !!... }
  19. 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() { } }); } } }
  20. 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() { } }); } } }
  21. 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
  22. public class UserProfilePresenter implements BasePresenter<UserView> {
 
 private DatabaseManager databaseManager;

    public UserProfilePresenter() { … } public UserProfilePresenter(DatabaseManager databaseManager) { this.databaseManager = databaseManager; } … }
  23. 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
  24. 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() { } }); } } }
  25. 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
  26. 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); } }
  27. 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; } }
  28. 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; } }
  29. 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; } }
  30. 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.
  31. public class VideoDetailsPresenter { public void onShowVideoDetails() { if (VideoUtils.canComment(video))

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

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

    (videoUtils.isCommentingAllowed(video)) { view.showCommentBox(); } if (videoUtils.isDownloadable(video)) { view.showDownloadButton(); } … } … }
  37. 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.
  38. 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); } … }
  39. 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); } … }
  40. 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() { … } … }
  41. 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() { … } … }
  42. 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); } 
 … 

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

  44. 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.
  45. 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) { !//!!... } }
  46. 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) { !//!!... } }
  47. Dependency Breaking Techniques • Parameterize Constructor • Extract Interface •

    Instance Delegator • Introduce Static Setter • Extract Implementor
  48. 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 } }
  49. 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 } }
  50. 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 } }
  51. 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 } }
  52. 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 } }
  53. 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; } }
  54. 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 } !// !!... }
  55. 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 } !// !!... }
  56. 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.
  57. 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… } }
  58. public class LoginPresenter { private final ApiClient apiClient; public LoginPresenter(final

    ApiClient apiClient) { this.apiClient = apiClient; } } Create a seam for ApiClient Sprout a class
  59. 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
  60. 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… } }
  61. 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.
  62. 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) { !//!!... } }
  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) { !//!!... } }
  64. 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")); } }
  65. 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")); } }
  66. 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")); } }
  67. 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.