DEV Community

Muhammad Salem
Muhammad Salem

Posted on

How OOP principles and SOLID principles can be effectively applied across each layer of a layered architecture

Applying software design principles like OOP and SOLID across different layers of a layered architecture is crucial for building robust and maintainable software systems. This article delves into how Object-Oriented Programming (OOP) principles and SOLID principles can be effectively applied across each layer of a layered architecture – Domain Layer, Application Layer, Infrastructure Layer, and Presentation Layer. We'll explore how these principles translate into practical design choices, using clear examples to illustrate their benefits. By understanding how these principles interact within each layer, you'll gain valuable insights for crafting robust and maintainable software systems. Let's break this down by layer and discuss how to apply these principles effectively.

  1. Domain Layer

This layer contains the core business logic and entities.

Key Principles:

  • Single Responsibility Principle (SRP)
  • Open/Closed Principle (OCP)
  • Encapsulation
  • Domain-Driven Design (DDD) concepts

Example:

// Entity
public class Order {
    private String orderId;
    private List<OrderItem> items;
    private OrderStatus status;

    public void addItem(OrderItem item) {
        // Encapsulation: internal logic hidden
        if (status != OrderStatus.DRAFT) {
            throw new IllegalStateException("Cannot add items to non-draft order");
        }
        items.add(item);
    }

    public void submit() {
        // Business logic
        if (items.isEmpty()) {
            throw new IllegalStateException("Cannot submit empty order");
        }
        status = OrderStatus.SUBMITTED;
    }
}

// Value Object
public class Money {
    private final BigDecimal amount;
    private final Currency currency;

    // Immutable value object
    public Money(BigDecimal amount, Currency currency) {
        this.amount = amount;
        this.currency = currency;
    }

    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("Cannot add different currencies");
        }
        return new Money(this.amount.add(other.amount), this.currency);
    }
}
Enter fullscreen mode Exit fullscreen mode

Considerations:

  • Keep domain objects focused on business logic
  • Use value objects for immutable concepts
  • Apply domain-driven design patterns where appropriate
  1. Application Layer

This layer orchestrates the use of domain objects to perform specific application tasks.
The Application Layer contains application-specific business rules and use cases. It orchestrates the flow of data between the presentation layer and the domain layer.

Key Principles:

  • Interface Segregation Principle (ISP)
  • Dependency Inversion Principle (DIP)
  • Command Query Responsibility Segregation (CQRS)

Example:

public interface OrderService {
    void createOrder(CreateOrderCommand command);
    OrderDTO getOrder(String orderId);
}

public class OrderServiceImpl implements OrderService {
    private final OrderRepository orderRepository;
    private final PaymentGateway paymentGateway;

    // Constructor injection (DIP)
    public OrderServiceImpl(OrderRepository orderRepository, PaymentGateway paymentGateway) {
        this.orderRepository = orderRepository;
        this.paymentGateway = paymentGateway;
    }

    @Override
    public void createOrder(CreateOrderCommand command) {
        Order order = new Order(command.getCustomerId());
        for (OrderItemDTO item : command.getItems()) {
            order.addItem(new OrderItem(item.getProductId(), item.getQuantity()));
        }
        orderRepository.save(order);
        paymentGateway.processPayment(order.getTotalAmount(), command.getPaymentDetails());
    }

    @Override
    public OrderDTO getOrder(String orderId) {
        Order order = orderRepository.findById(orderId);
        return new OrderDTO(order); // Map domain object to DTO
    }
}
Enter fullscreen mode Exit fullscreen mode

Considerations:

  • Use interfaces to define service contracts
  • Implement CQRS by separating command and query operations
  • Use DTOs to transfer data between layers
  1. Infrastructure Layer

This layer handles external concerns like persistence, messaging, and external service integration.

Key Principles:

  • Dependency Inversion Principle (DIP)
  • Adapter Pattern
  • Repository Pattern

Example:

public interface OrderRepository {
    void save(Order order);
    Order findById(String orderId);
}

public class JpaOrderRepository implements OrderRepository {
    private final EntityManager entityManager;

    public JpaOrderRepository(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    @Override
    public void save(Order order) {
        entityManager.persist(order);
    }

    @Override
    public Order findById(String orderId) {
        return entityManager.find(Order.class, orderId);
    }
}

public class PaymentGatewayAdapter implements PaymentGateway {
    private final ExternalPaymentService externalService;

    public PaymentGatewayAdapter(ExternalPaymentService externalService) {
        this.externalService = externalService;
    }

    @Override
    public void processPayment(Money amount, PaymentDetails details) {
        // Adapt domain concepts to external service
        externalService.pay(amount.getAmount(), amount.getCurrency(), details.getCardNumber());
    }
}
Enter fullscreen mode Exit fullscreen mode

Considerations:

  • Use adapters to integrate external services
  • Implement repositories to abstract data access
  • Keep infrastructure concerns separate from domain logic
  1. Presentation Layer

The Presentation Layer handles user interactions and displays data. It should be as thin as possible, delegating business logic to the application layer.
This layer handles user interface and API concerns.

Key Principles:

  • Separation of Concerns: Keep UI logic separate from business logic. Use patterns like MVC or MVVM to achieve this.
  • Single Responsibility Principle (SOLID): Ensure that UI components (controllers, views) have a single responsibility.

Considerations:

  • Keep controllers thin, delegating business logic to the application layer
  • Use DTOs to define API contracts
  • Implement proper error handling and validation

General Considerations for an Elegant Design:

  1. Separation of Concerns: Each layer should have a clear and distinct responsibility.

  2. Dependency Management: Use dependency injection to manage dependencies between components and layers.

  3. Abstraction: Use interfaces to define contracts between layers, allowing for easier testing and future changes.

  4. Modularity: Design components to be modular and reusable where possible.

  5. Testability: Design with testing in mind, making it easy to unit test components in isolation.

  6. Scalability: Consider how the design will scale as the system grows.

  7. Consistency: Maintain a consistent design pattern and naming convention throughout the system.

  8. Error Handling: Implement proper error handling and propagation across layers.

  9. Security: Consider security implications in each layer, especially in the presentation and application layers.

  10. Performance: Be mindful of performance implications, especially in data access and external service calls.

By applying these principles and considerations, you can create a robust, maintainable, and scalable software system that leverages the strengths of OOP and layered architecture. Remember that good design often involves trade-offs and should be tailored to the specific needs of your system and team.

Top comments (1)

Collapse
 
der_gopher profile image
Alex Pliutau

Great write-up! Also wrote some thoughts about it but in context of Go. Although Golang is not a purely object-oriented language, we can still apply SOLID principles to improve our Go code - packagemain.tech/p/mastering-solid...