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. Modularity and
    @CarlosChacin
    Decoupling
    The Right Way
    https://goo.gl/fsmi85

    View Slide

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

    View Slide

  3. Am I in troubles
    with this new
    codebase?

    View Slide

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

    View Slide

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

    View Slide

  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”);
    }
    }

    View Slide

  7. Decoupling

    View Slide

  8. One class depends on
    another

    View Slide

  9. We need to test one class

    View Slide

  10. Now we have ...

    View Slide

  11. How we usually
    “decouple”?

    View Slide

  12. How we usually
    “decouple”?

    View Slide

  13. View Slide

  14. 3 classes instead of 2?

    View Slide

  15. And to test it ...

    View Slide

  16. Example App

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  21. Request key
    In Cache? Get from DB
    Return value Save on Cache
    YES
    NO

    View Slide

  22. Modularizing
    Packaging

    View Slide

  23. View Slide

  24. View Slide

  25. View Slide

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

    View Slide

  27. public interface CustomerCache {
    void saveToCache(final Customer customer);
    Optional getFromCache(final UUID id);
    void method1();
    String method2();
    }

    View Slide

  28. public interface CustomerCache {
    void saveToCache(final Customer customer);
    Optional 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 getFromCache(UUID id) {
    System.out.println("RETRIEVED FROM FILE SYSTEM");
    return Optional.empty();
    }
    @Override
    public void method1() {}
    @Override
    public String method2() { return null; }
    }

    View Slide

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

    View Slide

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

    View Slide

  31. public interface CustomerRepository {
    public void saveToDb(final Customer customer);
    public Optional 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 getFromDb(UUID id) {
    System.out.println("RETRIEVED FROM DATABASE");
    return Optional.empty();
    }
    @Override
    public void method1() {}
    @Override
    public String method2() { return null; }
    }

    View Slide

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

    View Slide

  33. public Optional find(final UUID id) {
    Optional cachedCustomer = this.cache.getFromCache(id);
    if (cachedCustomer.isPresent()) {
    return cachedCustomer;
    } else {
    Optional dbCustomer = this.repository.getFromDb(id);
    if (dbCustomer.isPresent()) {
    this.cache.saveToCache(dbCustomer.get());
    return dbCustomer;
    }
    }
    return Optional.empty();
    }

    View Slide

  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 find(UUID id) {
    return service.find(id);
    }
    }

    View Slide

  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);
    }

    View Slide

  36. Issues

    View Slide

  37. View Slide

  38. View Slide

  39. View Slide

  40. View Slide

  41. View Slide

  42. And how do we solve it?

    View Slide

  43. View Slide

  44. Unit Test

    View Slide

  45. View Slide

  46. View Slide

  47. View Slide

  48. Layers
    Package by Layer

    View Slide

  49. Data
    Layers
    Package by Layer

    View Slide

  50. Domain
    Data
    Layers
    Package by Layer

    View Slide

  51. Presentation
    Domain
    Data
    Layers
    Package by Layer

    View Slide

  52. Presentation
    Domain
    Data
    Layers
    Package by Layer

    View Slide

  53. Presentation
    Domain
    Data
    Layers
    Package by Layer

    View Slide

  54. Presentation
    Domain
    Data
    Layers
    Package by Layer

    View Slide

  55. Presentation
    Domain
    Data
    Layers
    Package by Layer Modularized by Speculation
    Splitted before is too big

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  62. Layers
    Package by Feature
    Modularized by cohesiveness
    Splitted after is too big

    View Slide

  63. Presentation
    Domain
    Data

    View Slide

  64. Let’s fix it

    View Slide

  65. Package by Feat.

    View Slide

  66. Do we know
    what is this app
    about?

    View Slide

  67. View Slide

  68. View Slide

  69. View Slide

  70. View Slide

  71. View Slide

  72. View Slide

  73. Fix Visibility

    View Slide

  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 {}

    View Slide

  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 {}

    View Slide

  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 {}

    View Slide

  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 {}

    View Slide

  78. View Slide

  79. Decoupling

    View Slide

  80. View Slide

  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 find(final UUID id) {
    final Optional cachedCustomer = this.cache.getFromCache(id);
    if (cachedCustomer.isPresent()) {
    return cachedCustomer;
    } else {
    final Optional dbCustomer = this.repository.getFromDb(id);
    if (dbCustomer.isPresent()) {
    this.cache.saveToCache(dbCustomer.get());
    return dbCustomer;
    }
    }
    return Optional.empty();
    }
    }

    View Slide

  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 find(final UUID id) {
    final Optional cachedCustomer = this.cache.getFromCache(id);
    if (cachedCustomer.isPresent()) {
    return cachedCustomer;
    } else {
    final Optional dbCustomer = this.repository.getFromDb(id);
    if (dbCustomer.isPresent()) {
    this.cache.saveToCache(dbCustomer.get());
    return dbCustomer;
    }
    }
    return Optional.empty();
    }
    }

    View Slide

  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 find(final UUID id) {
    final Optional cachedCustomer = this.cache.getFromCache(id);
    if (cachedCustomer.isPresent()) {
    return cachedCustomer;
    } else {
    final Optional dbCustomer = this.repository.getFromDb(id);
    if (dbCustomer.isPresent()) {
    this.cache.saveToCache(dbCustomer.get());
    return dbCustomer;
    }
    }
    return Optional.empty();
    }
    }

    View Slide

  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);
    }

    View Slide

  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);
    }

    View Slide

  86. View Slide

  87. Why do we need it?

    View Slide

  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);
    }

    View Slide

  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);
    }

    View Slide

  90. var cache = new Cache() {
    @Override
    public void saveToCache(Customer customer) {}
    @Override
    public Optional 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);
    }

    View Slide

  91. var cache = new Cache() {
    @Override
    public void saveToCache(Customer customer) {}
    @Override
    public Optional 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);
    }

    View Slide

  92. Are we really
    decoupled?

    View Slide

  93. View Slide

  94. View Slide

  95. SAM

    View Slide

  96. public interface Function {
    R apply(T t);
    }
    public interface Consumer {
    void accept(T t);
    }
    public interface Supplier {
    T get();
    }

    View Slide

  97. public interface Function {
    R apply(T t);
    }
    public interface Consumer {
    void accept(T t);
    }
    public interface Supplier {
    T get();
    }
    Function>
    getFromCache
    Function>
    getFromDb
    Consumer
    saveToCache
    Consumer
    saveToDb

    View Slide

  98. package java.util.function;
    public interface Function {
    R apply(T t);
    }
    public interface Consumer {
    void accept(T t);
    }
    public interface Supplier {
    T get();
    }
    Function>
    getFromCache
    Function>
    getFromDb
    Consumer
    saveToCache
    Consumer
    saveToDb

    View Slide

  99. @Inject
    public CustomerService(
    CustomerRepository repository,
    CustomerCache cache) {
    this.repository = repository;
    this.cache = cache;
    }
    What is really required to build this class?

    View Slide

  100. @Inject
    CustomerService(
    Function> getFromCache,
    Function> getFromDb,
    Consumer saveToDb,
    Consumer saveToCache) {
    // Omitted
    }
    ● No More Coupling with Repository / Cache
    ● We know explicitly what is required from this class

    View Slide

  101. Removing Mocks

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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
    );
    }
    }

    View Slide

  108. Recap

    View Slide

  109. View Slide

  110. View Slide

  111. View Slide

  112. View Slide

  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

    View Slide

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

    View Slide

  115. And it’s easier than you think

    View Slide

  116. Gracias...

    View Slide

  117. github.com/cchacin/nomo

    View Slide

  118. Questions?
    Comments?

    View Slide