Skip to content

Programming to Interfaces


Definition

Programming to Interfaces Definition


Concrete vs Interface

// BAD: Depending on concrete implementation
public class OrderService {
    // Tied to specific implementation
    private MySQLOrderRepository repository;
    private StripePaymentProcessor payment;
    private SendGridEmailSender email;

    public OrderService() {
        // Hardcoded dependencies
        this.repository = new MySQLOrderRepository();
        this.payment = new StripePaymentProcessor();
        this.email = new SendGridEmailSender();
    }

    public void createOrder(Order order) {
        repository.save(order);          // Can't switch DB
        payment.process(order);          // Can't switch payment
        email.sendConfirmation(order);   // Can't switch email
    }
}

// GOOD: Depending on interfaces
public class OrderService {
    // Depend on abstractions
    private final OrderRepository repository;
    private final PaymentProcessor payment;
    private final EmailSender email;

    // Any implementation can be injected
    public OrderService(OrderRepository repository,
                       PaymentProcessor payment,
                       EmailSender email) {
        this.repository = repository;
        this.payment = payment;
        this.email = email;
    }

    public void createOrder(Order order) {
        repository.save(order);          // Any DB works
        payment.process(order);          // Any payment works
        email.sendConfirmation(order);   // Any email works
    }
}

// Now we can:
// - Use MySQL in production, H2 in tests
// - Switch from Stripe to PayPal
// - Mock everything for unit tests

Collections Example

// BAD: Using concrete types
public class UserManager {
    private ArrayList<User> users = new ArrayList<>();
    private HashMap<String, User> userIndex = new HashMap<>();

    public ArrayList<User> getActiveUsers() {
        ArrayList<User> active = new ArrayList<>();
        // ...
        return active;
    }

    // What if we need LinkedList for better insertion?
    // What if we need TreeMap for sorted access?
    // Must change method signatures!
}

// GOOD: Using interface types
public class UserManager {
    private List<User> users = new ArrayList<>();
    private Map<String, User> userIndex = new HashMap<>();

    public List<User> getActiveUsers() {
        List<User> active = new ArrayList<>();
        // ...
        return active;
    }

    // Can easily switch to:
    // List<User> users = new LinkedList<>();
    // Map<String, User> userIndex = new TreeMap<>();
    // No changes to method signatures or callers!
}

// BEST: Return unmodifiable where appropriate
public List<User> getActiveUsers() {
    return Collections.unmodifiableList(activeUsers);
    // Or in Java 10+:
    return List.copyOf(activeUsers);
}

Design for Testability

// Interface enables easy mocking

public interface Clock {
    Instant now();
}

// Production implementation
public class SystemClock implements Clock {
    public Instant now() {
        return Instant.now();
    }
}

// Service using interface
public class SubscriptionService {
    private final Clock clock;

    public SubscriptionService(Clock clock) {
        this.clock = clock;
    }

    public boolean isExpired(Subscription sub) {
        return sub.getExpiryDate().isBefore(clock.now());
    }
}

// Test with controllable time
@Test
void subscriptionShouldExpireAfterEndDate() {
    // Fixed clock for predictable testing
    Clock fixedClock = () -> Instant.parse("2024-01-15T00:00:00Z");

    SubscriptionService service = new SubscriptionService(fixedClock);

    Subscription sub = new Subscription(
        Instant.parse("2024-01-14T00:00:00Z")  // Expired yesterday
    );

    assertTrue(service.isExpired(sub));
}

// Without interface, testing time-based logic is nightmare!

Interface Design Guidelines

Interface Design Guidelines


Dependency Injection

// Interfaces + DI = Powerful combination

// Define contracts
public interface UserRepository {
    Optional<User> findById(String id);
    void save(User user);
}

public interface PasswordEncoder {
    String encode(String rawPassword);
    boolean matches(String raw, String encoded);
}

// Service depends on interfaces
@Service
public class UserService {
    private final UserRepository repository;
    private final PasswordEncoder encoder;

    @Autowired  // Spring injects implementations
    public UserService(UserRepository repository, PasswordEncoder encoder) {
        this.repository = repository;
        this.encoder = encoder;
    }
}

// Production configuration
@Configuration
public class ProductionConfig {
    @Bean
    public UserRepository userRepository(DataSource ds) {
        return new JdbcUserRepository(ds);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

// Test configuration
@TestConfiguration
public class TestConfig {
    @Bean
    public UserRepository userRepository() {
        return new InMemoryUserRepository();  // Fast for tests
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new NoOpPasswordEncoder();  // Simple for tests
    }
}

When NOT to Use Interfaces

When Interfaces May Not Be Needed


Tips & Tricks

Programming to Interfaces Tips and Tricks