Pro Yearly is on sale from $80 to $50! »

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

66059d4f17b6b7a6888d62caa736a663?s=128

Carlos Chacin

July 10, 2019
Tweet

Transcript

  1. Modularity and @CarlosChacin Decoupling The Right Way https://goo.gl/fsmi85

  2. • 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...
  3. Am I in troubles with this new codebase?

  4. find . -name \*.java -exec grep 'public class' {} \;

    | wc -l
  5. find . -name \*.java -exec grep 'void' {} \; |

    wc -l
  6. 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”); } }
  7. Decoupling

  8. One class depends on another

  9. We need to test one class

  10. Now we have ...

  11. How we usually “decouple”?

  12. How we usually “decouple”?

  13. None
  14. 3 classes instead of 2?

  15. And to test it ...

  16. Example App

  17. View Logic Cache Persistence Redis File System PostgreSQL Json API

    CustomerDTO
  18. View Logic Cache Persistence Redis File System PostgreSQL Json API

    CustomerDTO
  19. View Logic Cache Persistence Redis File System PostgreSQL Json API

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

    CustomerDTO @Inject @Inject @Inject @Inject @Inject @Inject
  21. Request key In Cache? Get from DB Return value Save

    on Cache YES NO
  22. Modularizing Packaging

  23. None
  24. None
  25. None
  26. Device Logic Cache Persistence Redis File System PostgreSQL Json API

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

    UUID id); void method1(); String method2(); }
  28. 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; } }
  29. Device Logic Cache Persistence Redis File System PostgreSQL Json API

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

    Optional<Customer> getFromDb(final UUID id); public void method1(); public String method2(); }
  31. 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; } }
  32. Device Logic Cache Persistence Redis File System PostgreSQL Json API

    CustomerDTO @Inject @Inject @Inject @Inject @Inject @Inject
  33. 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(); }
  34. 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); } }
  35. @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); }
  36. Issues

  37. None
  38. None
  39. None
  40. None
  41. None
  42. And how do we solve it?

  43. None
  44. Unit Test

  45. None
  46. None
  47. None
  48. Layers Package by Layer

  49. Data Layers Package by Layer

  50. Domain Data Layers Package by Layer

  51. Presentation Domain Data Layers Package by Layer

  52. Presentation Domain Data Layers Package by Layer

  53. Presentation Domain Data Layers Package by Layer

  54. Presentation Domain Data Layers Package by Layer

  55. Presentation Domain Data Layers Package by Layer Modularized by Speculation

    Splitted before is too big
  56. Presentation Domain Data Layers Layers Package by Feature Package by

    Layer
  57. Presentation Domain Data Layers Layers Package by Feature Package by

    Layer
  58. Presentation Domain Data Layers Layers Package by Feature Package by

    Layer
  59. Presentation Domain Data Layers Layers Package by Feature Package by

    Layer
  60. Presentation Domain Data Layers Layers Package by Feature Package by

    Layer
  61. Presentation Domain Data Layers Layers Package by Feature Package by

    Layer
  62. Layers Package by Feature Modularized by cohesiveness Splitted after is

    too big
  63. Presentation Domain Data

  64. Let’s fix it

  65. Package by Feat.

  66. Do we know what is this app about?

  67. None
  68. None
  69. None
  70. None
  71. None
  72. None
  73. Fix Visibility

  74. 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 {}
  75. 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 {}
  76. 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 {}
  77. 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 {}
  78. None
  79. Decoupling

  80. None
  81. 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(); } }
  82. 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(); } }
  83. 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(); } }
  84. @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); }
  85. @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); }
  86. None
  87. Why do we need it?

  88. @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); }
  89. @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); }
  90. 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); }
  91. 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); }
  92. Are we really decoupled?

  93. None
  94. None
  95. SAM

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

    interface Consumer<T> { void accept(T t); } public interface Supplier<T> { T get(); }
  97. 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
  98. 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
  99. @Inject public CustomerService( CustomerRepository repository, CustomerCache cache) { this.repository =

    repository; this.cache = cache; } What is really required to build this class?
  100. @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
  101. Removing Mocks

  102. when(cache.getFromCache(id)) .thenReturn(Optional.empty()); when(repo.getFromDb(id)) .thenReturn(Optional.of(cust));

  103. 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));
  104. 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));
  105. 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));
  106. 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));
  107. 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 ); } }
  108. Recap

  109. None
  110. None
  111. None
  112. None
  113. • 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
  114. ★ 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...
  115. And it’s easier than you think

  116. Gracias...

  117. github.com/cchacin/nomo

  118. Questions? Comments?