API • just if least “X” minutes passed since last check • if the user has moved at least “Y” meters • does not wake GPS if time constraint not met • returns an empty list if nothing found
single static `getInstance()` method • should be easy to implement • if we need other classes make them singleton as well • access other components via static getInstance()
dbHelper; private LocationManager locationManager; private LocationTracker(Context context) { this.dbHelper = DbHelper.getInstance(context); this.locationManager = (LocationManager) context.getSystemService(Context. LOCATION_SERVICE); } public static LocationTracker getInstance(Context context) { if (instance == null) { instance = new LocationTracker(context) ; } return instance; } } Keep reference to dependencies Do work in the constructors to get them
since last check if (System.currentTimeMillis() - DbHelper.getInstance(context).getLastCheck() < MIN_INTERVAL_MS) { return new ArrayList<>(); } //... }
since last check if (System. currentTimeMillis() - DbHelper. getInstance(context).getLastCheck() < MIN_INTERVAL_MS) { return new ArrayList<>(); } //Check if location has changed enough since last check LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); Location lastLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); //... }
since last check if (System. currentTimeMillis() - DbHelper. getInstance(context).getLastCheck() < MIN_INTERVAL_MS) { return new ArrayList<>(); } //Check if location has changed enough since last check LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); Location lastLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); Location lastUsedLocation = DbHelper.getInstance(context).getLastUsedLocation() if (lastLocation.distanceTo(lastUsedLocation) < MIN_DISTANCE_METERS) { return new ArrayList<>(); } //... }
around the block to simulate 500 m distance change? • wait 1 hour until a new check is performed by the app ? • … I have a better idea ! I’ll unit test it !
for details. at android.database.sqlite.SQLiteOpenHelper.getReadableDatabase(SQLiteOpenHelper.java) at ro.corebuild.cleancode.bad.DbHelper.getLastCheck(DbHelper.java:48) at ro.corebuild.cleancode.bad.LocationTracker.checkNewNearbyPlaces(LocationTracker.java:49) at ro.corebuild.cleancode.BadLocationTrackerTest.simpleTest(BadLocationTrackerTest.java:24) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:606)
checkNewNearbyPlaces() throws SecurityException { //Check if a enough time passed since last check if (System.currentTimeMillis() - DbHelper.getInstance(context).getLastCheck() < MIN_INTERVAL_MS) { //... } }
checkNewNearbyPlaces() throws SecurityException { //Check if a enough time passed since last check if (System.currentTimeMillis() - DbHelper.getInstance(context).getLastCheck() < MIN_INTERVAL_MS) { //... } } DbHelper.java public static DbHelper getInstance(Context context) { if (instance == null) { instance = new DbHelper(context); } return instance; }
JVM (not a real device) • there is no context on the JVM • there is no real database on the JVM • everytime we access System components tests might crash
“Dependency Inversion Principle” ◦ “Depend upon Abstractions. Do not depend upon concretions.” • class has concrete dependencies - DbHelper & LocationManager • DbHelper & LocationManager are hidden dependencies • db storage & location dependencies can’t be stubbed / faked / mocked • same stands for the API dependency
since last check if (System. currentTimeMillis() - storage.getLastCheck() < MIN_INTERVAL_MS) { return new ArrayList<>(); } //Check if location has changed enough since last check Location lastLocation = locationProvider.getMostRecentLocation(); Location lastUsedLocation = storage.getLastUsedLocation(); if (lastLocation.distanceTo(lastUsedLocation) < MIN_DISTANCE_METERS) { return new ArrayList<>(); } //Store last location & check storage.setLastUsedLocation(lastLocation); storage.setLastCheck(System. currentTimeMillis()); return fetchPlacesFromApi(lastLocation); }
enough time passed since last check if (System.currentTimeMillis() - storage.getLastCheck() < MIN_INTERVAL_MS) { return new ArrayList<>(); } //Check if location has changed enough since last check Location lastLocation = locationProvider .getMostRecentLocation() ; Location lastUsedLocation = storage.getLastUsedLocation() ; if (lastLocation.distanceTo(lastUsedLocation) < MIN_DISTANCE_METERS) { return new ArrayList<>(); } //Store last location & check storage.setLastUsedLocation(lastLocation) ; storage.setLastCheck(System. currentTimeMillis()); return fetchPlacesFromApi(lastLocation) ; }
{ //Check if a enough time passed since last check if (System.currentTimeMillis() - storage.getLastCheck() < MIN_INTERVAL_MS) { return new ArrayList<>(); } //Check if location has changed enough since last check Location lastLocation = locationProvider.getMostRecentLocation(); Location lastUsedLocation = storage.getLastUsedLocation(); if (lastLocation.distanceTo(lastUsedLocation) < MIN_DISTANCE_METERS) { return new ArrayList<>(); } //Store last location & check storage.setLastUsedLocation(lastLocation); storage.setLastCheck(System.currentTimeMillis()); return fetchPlacesFromApi(lastLocation); }
a enough time passed since last check if (System.currentTimeMillis() - storage.getLastCheck() < MIN_INTERVAL_MS) { return new ArrayList<>(); } //Check if location has changed enough since last check Location lastLocation = locationProvider.getMostRecentLocation(); Location lastUsedLocation = storage.getLastUsedLocation(); if (lastLocation.distanceTo(lastUsedLocation) < MIN_DISTANCE_METERS) { return new ArrayList<>(); } //Store last location & check storage.setLastUsedLocation(lastLocation); storage.setLastCheck(System.currentTimeMillis()); return fetchPlacesFromApi(lastLocation); } System dependency !
} public class SystemClock implements Clock { @Override public long timeInMillis() { return System.currentTimeMillis(); } } public interface PlacesApi { @GET("/places") List<Place> getPlaces(@Query("lat") double latitude, @Query("lng") double longitude); } Retrofit for our API
Clock clock) {...} @Override public List<Place> checkNearbyPlaces() { //Check if a enough time passed since last check if (clock.timeInMillis() - storage.getLastCheck() < MIN_INTERVAL_MS) { return new ArrayList<>(); } //Check if location has changed enough since last check Location location = locationProvider.getMostRecentLocation(); if (location.distanceTo(storage.getLastUsedLocation()) < MIN_DISTANCE_METERS) { return new ArrayList<>(); } //Store last location & check storage.setLastUsedLocation(location); storage.setLastCheck(System.currentTimeMillis()); return placesApi.getPlaces(location.getLatitude(), location.getLongitude()); }
complex scenarios can be simulated • easy to write unit tests • we can simulate location changes, movement • we can simulate time • we don’t need to wait for a dependency to be ready (eg: API) to continue / start development (we can mock it) (of course, you will be using some DI framework… Dagger)
stub / dummy implementations can be used in integration tests as well not only UNIT tests • implementations can be swapped at runtime • we can easily reproduce complex bugs