samedi 9 février 2019

Inheritance or delegation for Repositories

Say I have a Repository (a kind of object for accessing data) interface:

interface UserRepository {
   void insert(final User user);
   void update(final User user);
   List<User> findAll();
}

Now, because I manage different kinds of DMBS, this repository is implemented by:

class MySqlUserRepository implements UserRepository { ... }
class OracleUserRepository implements UserRepository { ... }
class SqlServerUserRepository implements UserRepository { ... }

Being that most of the SQL statements can be shared among those implementations, the first thing I'd do is add an Abstract base class, which provides all the basic implementations.

abstract class AbstractUserRepository implements UserRepository {
   // Provide access to statements
   private final Configuration configuration;

   // Provides access to JDBC operations
   private final NamedParameterJdbcTemplate jdbcTemplate;

   AbstractUserRepository(
         final Configuration configuration,
         final NamedParameterJdbcTemplate jdbcTemplate) {
      this.configuration = configuration;
      this.jdbcTemplate = jdbcTemplate;
   }

   ... Implementations
}

So all the implementations now extends AbstractUserRepository. However, I have to override a method on OracleUserRepository to use a different statement...

class OracleUserRepository extends AbstractUserRepository {
   private final Configuration configuration;
   private final NamedParameterJdbcTemplate jdbcTemplate;

   OracleUserRepository(
         final Configuration configuration,
         final NamedParameterJdbcTemplate jdbcTemplate) {
      super(configuration, jdbcTemplate);
      this.configuration = configuration.subset("user.sql.oracle");
      this.jdbcTemplate = jdbcTemplate;
   }

   @Override
   public void update(final User user) {
      // Use Configuration and JdbcTemplate
   }

   ...
}

... and a couple of methods on SqlServerUserRepository, using the same approach, obviously.


Now, you can see we are already duplicating class properties, and maybe the methods are basically the same:

AbstractUserRepository#update:

void update(final User user) {
   final var parameters =
                new MapSqlParameterSource()
                        .addValue("field1", user.field1)
                        .addValue("field2", user.field2);

   jdbcTemplate.update(configuration.getString("user.sql.update"), parameters);
}

OracleUserRepository#update:

void update(final User user) {
   final var parameters =
                new MapSqlParameterSource()
                        .addValue("field1", user.field1)
                        .addValue("field2", user.field2);

   // Note the subtle difference. We got a "subset" (user.sql.oracle) in the constructor
   jdbcTemplate.update(configuration.getString("update"), parameters);
}

This doesn't feel right to me, too much duplication already. Don't you think too?


A different approach would be using delegation. This mean having a detached object, which does not inherit the UserRepository interface, and which has methods that acceps also SQL statements instead of only an object.

class UserRepositoryDelegate {
   private final NamedParameterJdbcTemplate jdbcTemplate,

   UserRepositoryDelegate(final NamedParameterJdbcTemplate jdbcTemplate) {
      this.jdbcTemplate = jdbcTemplate;
   } 

   public void update(
          final String statement,
          final User user) {
       final var parameters =
                    new MapSqlParameterSource()
                            .addValue("field1", user.field1)
                            .addValue("field2", user.field2);

       jdbcTemplate.update(statement, parameters);
   }

   ...
}

This object would be injected in the other Repositories:

class OracleUserRepository implements UserRepository {
   private final Configuration configuration;
   private final UserRepositoryDelegate delegate;

   OracleUserRepository(
      final Configuration configuration,
      final UserRepositoryDelegate delegate
   ) {
      super(configuration, jdbcTemplate);
      this.configuration = configuration.subset("user.sql.oracle");
      this.delegate = delegate;
   }

   @Override
   public void update(final User user) {
      delegate.update(configuration.get("update"), user);
   }

   ...
}

And the AbstractUserRepository could become a GenericUserRepository (notice I don't get a subset of the properties for a specific DBMS, but instead use the generic ones):

class GenericUserRepository implements UserRepository {
   private final Configuration configuration;
   private final UserRepositoryDelegate delegate;

   GenericUserRepository(
         final Configuration configuration,
         final UserRepositoryDelegate delegate) {
      this.configuration = configuration;
      this.delegate = delegate;
   }

   @Override
   public void update(final User user) {
      delegate.update(configuration.get("user.sql.update"), user);
   }

   ...
}

Is the delegate approach a better strategy for this specific case?
Would you have solved this "problem" in a different way?

Edit: to let you understand more, the only one and correct Repository implementation is choosen at start-up time, and injected in a single, concrete, UserService.

Aucun commentaire:

Enregistrer un commentaire