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

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

Tips & Tricks
