Dependency Inversion Principle

Intent/Definition

The principle states: 
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Abstractions should not depend on details. Details should depend on abstractions.

The Dependency Inversion Principle represents the “D” of the five SOLID Principles of object-oriented programming to write well-designed code that is more readable, maintainable, and easier to upgrade and modify.

Rules of Thumb?

  • Design by contract.
  • Every dependency in the design should target an interface or an abstract class. No dependency should target a concrete class.
  • Factories and Abstract Factories can be used as dependency frameworks, but there are specialized frameworks for that such as Spring IOC (Inversion of Control Container).

Benefits

  • Loose Coupling - don't ask for dependency it will be provided to you by the framework. This has been very well implemented in Spring framework, the beauty of this design principle is that any class which is injected by DI framework is easy to test with the mock object and easier to maintain because object creation code is centralized in the framework and client code is not littered with that.

Dependency Inversion Principle Example

Let's take an example of a simple Customer Management example. If you see the source code, we are connecting to MySQL and performing CRUD operations for the customer. The MySQLDatasourceclass object is created using new keyword in CustomerDaoImpl.
Here CustomerDaoImpl is hardly depended on MySQLDatasource so if we want to switch MySQL to Oracle database then we need to change CustomerDaoImpl for Oracle database connection.
Let's see how this design principle provides the solution to avoid hard dependencies.

Class Diagram

Source Code

Step 1: Create customer POJO

Customer.java

public class Customer {

  private int id;
  private String firstName;
  private String lastName;

  /**
   * Creates an instance of customer.
   */
  public Customer(final int id, final String firstName, final String lastName) {
    this.id = id;
    this.firstName = firstName;
    this.lastName = lastName;
  }

  public int getId() {
    return id;
  }

  public void setId(final int id) {
    this.id = id;
  }

  public String getFirstName() {
    return firstName;
  }

  public void setFirstName(final String firstName) {
    this.firstName = firstName;
  }

  public String getLastName() {
    return lastName;
  }

  public void setLastName(final String lastName) {
    this.lastName = lastName;
  }
}
Step 2: Create CustomerDao interface, which contains CRUD database operations for Customer.

CustomerDao.java

public interface CustomerDao {
  /**
    * @param id unique identifier of the customer.
    * @return an optional with customer if a customer with unique identifier <code>id</code>
    *     exists, empty optional otherwise.
    * @throws Exception if any error occurs.
    */
   Customer getById(int id) throws Exception;

   /**
    * @param customer the customer to be added.
    * @return true if customer is successfully added, false if customer already exists.
    * @throws Exception if any error occurs.
    */
   boolean add(Customer customer) throws Exception;

   /**
    * @param customer the customer to be updated.
    * @return true if customer exists and is successfully updated, false otherwise.
    * @throws Exception if any error occurs.
    */
   boolean update(Customer customer) throws Exception;

   /**
    * @param customer the customer to be deleted.
    * @return true if customer exists and is successfully deleted, false otherwise.
    * @throws Exception if any error occurs.
    */
   boolean delete(Customer customer) throws Exception;
}
Step 3: Create CustomerDaoImpl to implement CustomerDao interface. Here CustomerDaoImpl class depends on interface Datasource.

CustomerDaoImpl.java

public class CustomerDaoImpl implements CustomerDao {


    private DataSource dataSource;

    //It depends on abstraction(DataSource interface) that can be injected through its constructor
    public CustomerDaoImpl(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    private Connection getConnection() throws SQLException {
        return this.dataSource.getConnection();
    }

    private Customer createCustomer(ResultSet resultSet) throws SQLException {
        return new Customer(resultSet.getInt("ID"), resultSet.getString("first_name"), 
               resultSet.getString("last_name"));
    }

    @Override
    public Customer getById(int id) throws Exception {
        try {
            Connection connection = getConnection();
            PreparedStatement statement = connection.prepareStatement("SELECT * FROM CUSTOMERS WHERE ID = ?");
            statement.setInt(1, id);
            ResultSet resultSet = statement.executeQuery();
            if (resultSet.next()) {
                return createCustomer(resultSet);
            } else {
                return null;
            }
        } catch (SQLException ex) {
            throw new Exception(ex.getMessage(), ex);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean add(Customer customer) throws Exception {

        try {
            Connection connection = getConnection();
            PreparedStatement statement = connection.prepareStatement("INSERT INTO CUSTOMERS VALUES (?,?,?)");
            statement.setInt(1, customer.getId());
            statement.setString(2, customer.getFirstName());
            statement.setString(3, customer.getLastName());
            statement.execute();
            return true;
        } catch (SQLException ex) {
            throw new Exception(ex.getMessage(), ex);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean update(Customer customer) throws Exception {
        try {
            Connection connection = getConnection();
            PreparedStatement statement = connection
                .prepareStatement("UPDATE CUSTOMERS SET first_name = ?, last_name = ? WHERE ID = ?");
            statement.setString(1, customer.getFirstName());
            statement.setString(2, customer.getLastName());
            statement.setInt(3, customer.getId());
            return statement.executeUpdate() > 0;
        } catch (SQLException ex) {
            throw new Exception(ex.getMessage(), ex);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean delete(Customer customer) throws Exception {
        try {
            Connection connection = getConnection();
            PreparedStatement statement = connection.prepareStatement("DELETE FROM CUSTOMERS WHERE ID = ?");
            statement.setInt(1, customer.getId());
            return statement.executeUpdate() > 0;
        } catch (SQLException ex) {
            throw new Exception(ex.getMessage(), ex);
        }
    }

}
Step 4: Create a MySQLDatasource class to provide a connection to the MySQL database.

DataSource.java

public interface DataSource {
    void createConnection(DatabaseConfig config);
    Connection getConnection();

}

MySQLDataSource.java

public class MySQLDataSource implements DataSource {

    private MysqlDataSource dataSource;

    @Override
    public void createConnection(DatabaseConfig databaseConfig) {
        dataSource = new MysqlDataSource();
        dataSource.setUrl(databaseConfig.getUrl());
        dataSource.setUser(databaseConfig.getUserName());
        dataSource.setPassword(databaseConfig.getPassword());
    }

    @Override
    public Connection getConnection() {
        try {
            return dataSource.getConnection();
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return null;
    }
}
Step 5: In future, if we want to switch from MySQL to H2 database then we only need to pass this object from the client.

H2DataSource.java

public class H2DataSource implements DataSource {

    private JdbcDataSource dataSource;

    @Override
    public void createConnection(DatabaseConfig databaseConfig) {
        dataSource = new JdbcDataSource();
        dataSource.setURL(databaseConfig.getUrl());
        dataSource.setUser(databaseConfig.getUserName());
        dataSource.setPassword(databaseConfig.getPassword());
    }

    @Override
    public Connection getConnection() {
        try {
            return dataSource.getConnection();
        } catch (SQLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return null;
    }
}
Note that from the above code example, CustomerDaoImpl class depends on the DataSource interface, not the concrete class. Now due to abstraction Client can change implementation at any time. For example, now client using MySQL and in future client might interested use Oracle then simply provide the Oracle implementation as Constructor to CustomerDaoImpl.

Posts Related to SOLID Principles

Learn complete Oops concepts and SOLID Principles on Object Oriented Design in Java Tutorial
Learn beginners to expert Core Java on Java Tutorial (300 + Articles)
You can find all the top tutorials of this site on Java/J2EE Tutorials on JavaGuides

Comments