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

Modularity and Decoupling, The Right Way

Modularity and Decoupling, The Right Way

Creating and maintaining a Java codebase is not an easy task, especially with a codebase with thousands of tests. We often try to solve this problem with new libraries, frameworks and even new languages that can create a false sense of decoupling and modularity.
But the answer is not in the libraries, nor in the frameworks. The answer is in the core of the Java language itself and it has been there since the early stages.
In this talk, you will learn how to organize your codebase in such a way that your code is really decoupled and modularized, and so that you don't have to modify your tests when you decide to change some implementation details (like choosing a different HTTP library or a different database abstraction layer).

Carlos Chacin

July 10, 2019
Tweet

More Decks by Carlos Chacin

Other Decks in Programming

Transcript

  1. • Replace your http client changing only 1 or 2

    lines? • Rename an interface method changing only 1 or 2 lines? • Have unit tests without a mocking framework? • Rename an impl without changing mocks in tests? • Hide your implementation details? • Instantiate your application without a DI framework? Can you...
  2. package com.example.app.domain; public class CacheImpl implements Cache { @Inject public

    Cache(final Jedis jedis) { } } @ExtendWith(MockitoExtension.class) class CacheImplTest { @Test void test(@Mock Jedis jedis) throws Exception { when(jedis.get(“a”)).thenReturn(“b”); assertThat(cache.get(“a”)).isEqualTo(“b”); verify(jedis).get(“a”); } }
  3. View Logic Cache Persistence Redis File System PostgreSQL Json API

    CustomerDTO @Inject @Inject @Inject @Inject @Inject @Inject
  4. Device Logic Cache Persistence Redis File System PostgreSQL Json API

    CustomerDTO @Inject @Inject @Inject @Inject @Inject @Inject
  5. Device Logic Cache Persistence Redis File System PostgreSQL Json API

    CustomerDTO @Inject @Inject @Inject @Inject @Inject @Inject
  6. public interface CustomerCache { void saveToCache(final Customer customer); Optional<Customer> getFromCache(final

    UUID id); void method1(); String method2(); } @Alternative public class FileSystemCache implements CustomerCache { @Override public void saveToCache(Customer customer) { System.out.println("SAVED TO FILE SYSTEM"); } @Override public Optional<Customer> getFromCache(UUID id) { System.out.println("RETRIEVED FROM FILE SYSTEM"); return Optional.empty(); } @Override public void method1() {} @Override public String method2() { return null; } }
  7. Device Logic Cache Persistence Redis File System PostgreSQL Json API

    CustomerDTO @Inject @Inject @Inject @Inject @Inject @Inject
  8. public interface CustomerRepository { public void saveToDb(final Customer customer); public

    Optional<Customer> getFromDb(final UUID id); public void method1(); public String method2(); }
  9. public interface CustomerRepository { public void saveToDb(final Customer customer); public

    Optional<Customer> getFromDb(final UUID id); public void method1(); public String method2(); } @Alternative public class DbRepository implements CustomerRepository { @Override public void saveToDb(Customer customer) { System.out.println("SAVED TO DATABASE"); } @Override public Optional<Customer> getFromDb(UUID id) { System.out.println("RETRIEVED FROM DATABASE"); return Optional.empty(); } @Override public void method1() {} @Override public String method2() { return null; } }
  10. Device Logic Cache Persistence Redis File System PostgreSQL Json API

    CustomerDTO @Inject @Inject @Inject @Inject @Inject @Inject
  11. public Optional<Customer> find(final UUID id) { Optional<Customer> cachedCustomer = this.cache.getFromCache(id);

    if (cachedCustomer.isPresent()) { return cachedCustomer; } else { Optional<Customer> dbCustomer = this.repository.getFromDb(id); if (dbCustomer.isPresent()) { this.cache.saveToCache(dbCustomer.get()); return dbCustomer; } } return Optional.empty(); }
  12. public class CustomerEndpoint { private final CustomerService service; @Inject public

    CustomerEndpoint(final CustomerService service) { this.service = service; } public void create(Customer customer) { service.create(customer); } public Optional<Customer> find(UUID id) { return service.find(id); } }
  13. @Test @DisplayName("return customer from db when not present in cache")

    void return_customer_from_db_when_not_present_in_cache( @Mock CustomerRepository repository, @Mock CustomerCache cache) throws Exception { // Given var customer = Customer.create(id, id.toString()); when(cache.getFromCache(id)).thenReturn(Optional.empty()); when(repository.getFromDb(id)).thenReturn(Optional.of(customer)); // When var sut = new CustomerService(repository, cache); // Then assertThat(sut.find(id)).contains(Customer.create(id, id.toString())); verify(cache).saveToCache(customer); }
  14. public interface Cache {} public final class Customer {} public

    final class CustomerEndpoint {} public interface CustomerEndpointFactory {} public final class DbRepository {} public final class FileSystemCache {} public final class HttpRepository {} public final class RedisCache {} public interface Repository {} public final class Service {}
  15. public interface Cache {} public final class Customer {} public

    final class CustomerEndpoint {} public interface CustomerEndpointFactory {} public final class DbRepository {} public final class FileSystemCache {} public final class HttpRepository {} public final class RedisCache {} public interface Repository {} public final class Service {}
  16. public interface Cache {} public final class Customer {} public

    final class CustomerEndpoint {} public interface CustomerEndpointFactory {} public final class DbRepository {} public final class FileSystemCache {} public final class HttpRepository {} public final class RedisCache {} public interface Repository {} public final class Service {}
  17. public interface Cache {} public final class Customer {} public

    final class CustomerEndpoint {} public interface CustomerEndpointFactory {} public final class DbRepository {} public final class FileSystemCache {} public final class HttpRepository {} public final class RedisCache {} public interface Repository {} public final class Service {}
  18. public class CustomerService { private final CustomerRepository repository; private final

    CustomerCache cache; @Inject public CustomerService(final CustomerRepository repository, final CustomerCache cache) { this.repository = repository; this.cache = cache; } public void create(final Customer customer) { this.repository.saveToDb(customer); this.cache.saveToCache(customer); } public Optional<Customer> find(final UUID id) { final Optional<Customer> cachedCustomer = this.cache.getFromCache(id); if (cachedCustomer.isPresent()) { return cachedCustomer; } else { final Optional<Customer> dbCustomer = this.repository.getFromDb(id); if (dbCustomer.isPresent()) { this.cache.saveToCache(dbCustomer.get()); return dbCustomer; } } return Optional.empty(); } }
  19. public class CustomerService { private final CustomerRepository repository; private final

    CustomerCache cache; @Inject public CustomerService(final CustomerRepository repository, final CustomerCache cache) { this.repository = repository; this.cache = cache; } public void create(final Customer customer) { this.repository.saveToDb(customer); this.cache.saveToCache(customer); } public Optional<Customer> find(final UUID id) { final Optional<Customer> cachedCustomer = this.cache.getFromCache(id); if (cachedCustomer.isPresent()) { return cachedCustomer; } else { final Optional<Customer> dbCustomer = this.repository.getFromDb(id); if (dbCustomer.isPresent()) { this.cache.saveToCache(dbCustomer.get()); return dbCustomer; } } return Optional.empty(); } }
  20. public class CustomerService { private final CustomerRepository repository; private final

    CustomerCache cache; @Inject public CustomerService(final CustomerRepository repository, final CustomerCache cache) { this.repository = repository; this.cache = cache; } public void create(final Customer customer) { this.repository.saveToDb(customer); this.cache.saveToCache(customer); } public Optional<Customer> find(final UUID id) { final Optional<Customer> cachedCustomer = this.cache.getFromCache(id); if (cachedCustomer.isPresent()) { return cachedCustomer; } else { final Optional<Customer> dbCustomer = this.repository.getFromDb(id); if (dbCustomer.isPresent()) { this.cache.saveToCache(dbCustomer.get()); return dbCustomer; } } return Optional.empty(); } }
  21. @Test @DisplayName("return customer from db when not present in cache")

    void return_customer_from_db_when_not_present_in_cache( @Mock CustomerRepository repository, @Mock CustomerCache cache) throws Exception { // Given var customer = Customer.create(id, id.toString()); when(cache.getFromCache(id)).thenReturn(Optional.empty()); when(repository.getFromDb(id)).thenReturn(Optional.of(customer)); // When var sut = new CustomerService(repository, cache); // Then assertThat(sut.find(id)).contains(Customer.create(id, id.toString())); verify(cache).saveToCache(customer); }
  22. @Test @DisplayName("return customer from db when not present in cache")

    void return_customer_from_db_when_not_present_in_cache( @Mock CustomerRepository repository, @Mock CustomerCache cache) throws Exception { // Given var customer = Customer.create(id, id.toString()); when(cache.getFromCache(id)).thenReturn(Optional.empty()); when(repository.getFromDb(id)).thenReturn(Optional.of(customer)); // When var sut = new CustomerService(repository, cache); // Then assertThat(sut.find(id)).contains(Customer.create(id, id.toString())); verify(cache).saveToCache(customer); }
  23. @Test @DisplayName("return customer from db when not present in cache")

    void return_customer_from_db_when_not_present_in_cache( @Mock CustomerRepository repository, @Mock CustomerCache cache) throws Exception { // Given var customer = Customer.create(id, id.toString()); when(cache.getFromCache(id)).thenReturn(Optional.empty()); when(repository.getFromDb(id)).thenReturn(Optional.of(customer)); // When var sut = new CustomerService(repository, cache); // Then assertThat(sut.find(id)).contains(Customer.create(id, id.toString())); verify(cache).saveToCache(customer); }
  24. @Test @DisplayName("return customer from db when not present in cache")

    void return_customer_from_db_when_not_present_in_cache( @Mock CustomerRepository repository, @Mock CustomerCache cache) throws Exception { // Given var customer = Customer.create(id, id.toString()); when(cache.getFromCache(id)).thenReturn(Optional.empty()); when(repository.getFromDb(id)).thenReturn(Optional.of(customer)); // When var sut = new CustomerService(repository, cache); // Then assertThat(sut.find(id)).contains(Customer.create(id, id.toString())); verify(cache).saveToCache(customer); }
  25. var cache = new Cache() { @Override public void saveToCache(Customer

    customer) {} @Override public Optional<Customer> getFromCache(UUID id) { return Optional.empty(); } @Override public void method1() {} @Override public String method2() { return null; } }; @Test @DisplayName("return customer from db when not present in cache") void return_customer_from_db_when_not_present_in_cache( @Mock CustomerRepository repository, @Mock CustomerCache cache) throws Exception { // Given var customer = Customer.create(id, id.toString()); when(cache.getFromCache(id)).thenReturn(Optional.empty()); when(repository.getFromDb(id)).thenReturn(Optional.of(customer)); // When var sut = new CustomerService(repository, cache); // Then assertThat(sut.find(id)).contains(Customer.create(id, id.toString())); verify(cache).saveToCache(customer); }
  26. var cache = new Cache() { @Override public void saveToCache(Customer

    customer) {} @Override public Optional<Customer> getFromCache(UUID id) { return Optional.empty(); } @Override public void method1() {} @Override public String method2() { return null; } }; @Test @DisplayName("return customer from db when not present in cache") void return_customer_from_db_when_not_present_in_cache( @Mock CustomerRepository repository, @Mock CustomerCache cache) throws Exception { // Given var customer = Customer.create(id, id.toString()); when(cache.getFromCache(id)).thenReturn(Optional.empty()); when(repository.getFromDb(id)).thenReturn(Optional.of(customer)); // When var sut = new CustomerService(repository, cache); // Then assertThat(sut.find(id)).contains(Customer.create(id, id.toString())); verify(cache).saveToCache(customer); }
  27. SAM

  28. public interface Function<T, R> { R apply(T t); } public

    interface Consumer<T> { void accept(T t); } public interface Supplier<T> { T get(); }
  29. public interface Function<T, R> { R apply(T t); } public

    interface Consumer<T> { void accept(T t); } public interface Supplier<T> { T get(); } Function<UUID, Optional<Customer>> getFromCache Function<UUID, Optional<Customer>> getFromDb Consumer<Customer> saveToCache Consumer<Customer> saveToDb
  30. package java.util.function; public interface Function<T, R> { R apply(T t);

    } public interface Consumer<T> { void accept(T t); } public interface Supplier<T> { T get(); } Function<UUID, Optional<Customer>> getFromCache Function<UUID, Optional<Customer>> getFromDb Consumer<Customer> saveToCache Consumer<Customer> saveToDb
  31. @Inject public CustomerService( CustomerRepository repository, CustomerCache cache) { this.repository =

    repository; this.cache = cache; } What is really required to build this class?
  32. @Inject CustomerService( Function<UUID, Optional<Customer>> getFromCache, Function<UUID, Optional<Customer>> getFromDb, Consumer<Customer> saveToDb,

    Consumer<Customer> saveToCache) { // Omitted } • No More Coupling with Repository / Cache • We know explicitly what is required from this class
  33. var sut = new Service( uuid -> Optional.empty(), uuid ->

    Optional.of(customer), cust -> {}, cust -> {} ); • No More Coupling with Repository / Cache • No more mocks • No more tests refactors because renames in production code when(cache.getFromCache(id)) .thenReturn(Optional.empty()); when(repo.getFromDb(id)) .thenReturn(Optional.of(cust));
  34. var sut = new Service( uuid -> Optional.empty(), uuid ->

    Optional.of(customer), cust -> {}, cust -> {} ); • No More Coupling with Repository / Cache • No more mocks • No more tests refactors because renames in production code when(cache.getFromCache(id)) .thenReturn(Optional.empty()); when(repo.getFromDb(id)) .thenReturn(Optional.of(cust));
  35. var sut = new Service( uuid -> Optional.empty(), uuid ->

    Optional.of(customer), cust -> {}, cust -> {} ); • No More Coupling with Repository / Cache • No more mocks • No more tests refactors because renames in production code when(cache.getFromCache(id)) .thenReturn(Optional.empty()); when(repo.getFromDb(id)) .thenReturn(Optional.of(cust));
  36. var sut = new Service( uuid -> Optional.empty(), uuid ->

    Optional.of(customer), cust -> {}, cust -> {} ); • No More Coupling with Repository / Cache • No more mocks • No more tests refactors because renames in production code when(cache.getFromCache(id)) .thenReturn(Optional.empty()); when(repo.getFromDb(id)) .thenReturn(Optional.of(cust));
  37. public interface AppFactory { static CustomerEndpoint endpoint() { var dbRepository

    = new DbRepository(); var fileSystemCache = new FileSystemCache(); var service = new Service( fileSystemCache::getFromCache, dbRepository::getFromDb, dbRepository::saveToDb, fileSystemCache::saveToCache ); return new CustomerEndpoint( service::find, service::create ); } }
  38. • Higher modularity • Easier code navigation • Higher level

    of abstraction • Separates both features and layers • More readable and maintainable structure • More cohesion • Much easier to scale • Less chance to accidentally modify unrelated classes or files • Much easier to add or remove application features By looking at the structure you can already tell what the app is all about
  39. ★ Replace your http client changing only 1 or 2

    lines ★ Rename an interface method changing only 1 or 2 lines ★ Have unit tests without a mocking framework ★ Rename an impl without changing mocks in tests ★ Hide your implementation details ★ Instantiate your application without a DI framework Yes you can...