Quickstart

Welcome to tech interview preparation guide

leetcode Quickstart

Welcome to tech interview preparation guide

Design Patterns

SOLID Patterns

Certainly! Let’s take the e-commerce example from above and explain how the SOLID principles can be applied in detail. This will demonstrate how each principle contributes to a well-structured and maintainable system.

1. Single Responsibility Principle (SRP)

Definition: A class should have one, and only one, reason to change.

Application in E-commerce Example:

  • ShoppingCart Class: This class is responsible solely for managing items in the shopping cart. It handles adding and removing items and calculating the total price. If the way items are managed changes (for example, changing from an ArrayList to a different collection), only this class needs to be modified.

    class ShoppingCart {
        private List<Item> items;
    
        public ShoppingCart() {
            this.items = new ArrayList<>();
        }
    
        public void addItem(Item item) {
            items.add(item);
        }
    
        public void removeItem(Item item) {
            items.remove(item);
        }
    
        public double calculateTotal() {
            return items.stream().mapToDouble(Item::getPrice).sum();
        }
    
        public List<Item> getItems() {
            return items;
        }
    }
    
  • EmailService Class: If you want to send confirmation emails after a purchase, you could create a separate EmailService class to handle that logic, which keeps the responsibilities distinct.

    class EmailService {
        public void sendConfirmationEmail(String email) {
            // Logic to send confirmation email
        }
    }
    

2. Open/Closed Principle (OCP)

Definition: Software entities should be open for extension but closed for modification.

Application in E-commerce Example:

  • PaymentProcessor Interface: By defining a PaymentProcessor interface, you can introduce new payment methods without changing the existing code. For instance, if you want to add a BitcoinProcessor, you just create a new class that implements PaymentProcessor.

    interface PaymentProcessor {
        void processPayment(double amount);
    }
    
    class PayPalProcessor implements PaymentProcessor {
        @Override
        public void processPayment(double amount) {
            System.out.println("Processing PayPal payment of " + amount);
        }
    }
    
    class BitcoinProcessor implements PaymentProcessor {
        @Override
        public void processPayment(double amount) {
            System.out.println("Processing Bitcoin payment of " + amount);
        }
    }
    

3. Liskov Substitution Principle (LSP)

Definition: Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.

Application in E-commerce Example:

  • Substitutable Payment Processors: If you have a class that uses a PaymentProcessor, you can replace it with any of its subclasses (like PayPalProcessor or BitcoinProcessor) without changing the logic of the CheckoutService.

    class CheckoutService {
        private PaymentProcessor paymentProcessor;
    
        public CheckoutService(PaymentProcessor paymentProcessor) {
            this.paymentProcessor = paymentProcessor;
        }
    
        public void checkout(ShoppingCart cart) {
            double total = cart.calculateTotal();
            paymentProcessor.processPayment(total);
        }
    }
    

4. Interface Segregation Principle (ISP)

Definition: No client should be forced to depend on methods it does not use.

Application in E-commerce Example:

  • Separation of Payment Types: Instead of having a single large interface for all payment-related functionalities, you can create smaller, focused interfaces. For instance, you might have a separate interface for processing refunds.

    interface PaymentProcessor {
        void processPayment(double amount);
    }
    
    interface Refundable {
        void processRefund(double amount);
    }
    
    class PayPalProcessor implements PaymentProcessor, Refundable {
        @Override
        public void processPayment(double amount) {
            // PayPal payment logic
        }
    
        @Override
        public void processRefund(double amount) {
            // PayPal refund logic
        }
    }
    
    class CreditCardProcessor implements PaymentProcessor {
        @Override
        public void processPayment(double amount) {
            // Credit Card payment logic
        }
        
        // No refund method here, thus adhering to ISP
    }
    

5. Dependency Inversion Principle (DIP)

Definition: High-level modules should not depend on low-level modules. Both should depend on abstractions.

Application in E-commerce Example:

  • Dependence on Abstractions: Instead of CheckoutService depending directly on concrete classes like PayPalProcessor, it depends on the PaymentProcessor interface. This allows you to swap out implementations without changing the CheckoutService.

    class CheckoutService {
        private PaymentProcessor paymentProcessor;
    
        public CheckoutService(PaymentProcessor paymentProcessor) {
            this.paymentProcessor = paymentProcessor;
        }
    
        public void checkout(ShoppingCart cart) {
            double total = cart.calculateTotal();
            paymentProcessor.processPayment(total);
        }
    }
    
  • Using Dependency Injection: You can use dependency injection frameworks (like Spring) to manage the creation and injection of PaymentProcessor instances, making it easier to manage dependencies and configurations.

Conclusion

Applying the SOLID principles to the e-commerce system not only leads to cleaner and more organized code but also enhances maintainability, flexibility, and testability. Each class and interface has a clear responsibility, making it easier to understand and extend the system as new requirements emerge. By following these principles, developers can create robust software that can adapt to changing business needs with minimal risk of introducing bugs.

Let’s create a complete example of a Hotel Management System that demonstrates the SOLID principles. This example will cover various aspects of a hotel management system, including room management, booking, and notification services.

Example: Hotel Management System

Step 1: Define the Domain Models

We’ll start with the core models: Room, Booking, and Customer.

// Room class
class Room {
    private int roomNumber;
    private String roomType; // e.g., "Single", "Double", "Suite"
    private boolean isAvailable;

    public Room(int roomNumber, String roomType) {
        this.roomNumber = roomNumber;
        this.roomType = roomType;
        this.isAvailable = true; // By default, a room is available
    }

    public int getRoomNumber() {
        return roomNumber;
    }

    public String getRoomType() {
        return roomType;
    }

    public boolean isAvailable() {
        return isAvailable;
    }

    public void setAvailable(boolean available) {
        isAvailable = available;
    }
}

// Customer class
class Customer {
    private String name;
    private String email;

    public Customer(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}

Step 2: Booking Class

The Booking class is responsible for managing room bookings.

import java.util.Date;

class Booking {
    private Room room;
    private Customer customer;
    private Date startDate;
    private Date endDate;

    public Booking(Room room, Customer customer, Date startDate, Date endDate) {
        this.room = room;
        this.customer = customer;
        this.startDate = startDate;
        this.endDate = endDate;
    }

    public Room getRoom() {
        return room;
    }

    public Customer getCustomer() {
        return customer;
    }

    public Date getStartDate() {
        return startDate;
    }

    public Date getEndDate() {
        return endDate;
    }
}

Step 3: Hotel Class

The Hotel class manages rooms and bookings and follows the Single Responsibility Principle (SRP).

import java.util.ArrayList;
import java.util.List;

class Hotel {
    private List<Room> rooms;
    private List<Booking> bookings;

    public Hotel() {
        this.rooms = new ArrayList<>();
        this.bookings = new ArrayList<>();
    }

    public void addRoom(Room room) {
        rooms.add(room);
    }

    public List<Room> getAvailableRooms() {
        List<Room> availableRooms = new ArrayList<>();
        for (Room room : rooms) {
            if (room.isAvailable()) {
                availableRooms.add(room);
            }
        }
        return availableRooms;
    }

    public void bookRoom(Room room, Customer customer, Date startDate, Date endDate) {
        if (room.isAvailable()) {
            room.setAvailable(false);
            Booking booking = new Booking(room, customer, startDate, endDate);
            bookings.add(booking);
        } else {
            System.out.println("Room is not available!");
        }
    }

    public List<Booking> getBookings() {
        return bookings;
    }
}

Step 4: NotificationService Interface and Implementations

To adhere to the Open/Closed Principle (OCP), we will create an interface for notifications.

interface NotificationService {
    void notifyCustomer(Customer customer, String message);
}

class EmailNotificationService implements NotificationService {
    @Override
    public void notifyCustomer(Customer customer, String message) {
        System.out.println("Sending email to " + customer.getEmail() + ": " + message);
    }
}

class SMSNotificationService implements NotificationService {
    @Override
    public void notifyCustomer(Customer customer, String message) {
        System.out.println("Sending SMS to " + customer.getName() + ": " + message);
    }
}

Step 5: BookingManager Class

The BookingManager class handles bookings and notifications. It encapsulates the logic related to booking management and adheres to the SOLID principles.

class BookingManager {
    private Hotel hotel;
    private NotificationService notificationService;

    public BookingManager(Hotel hotel, NotificationService notificationService) {
        this.hotel = hotel;
        this.notificationService = notificationService;
    }

    public void bookRoom(Room room, Customer customer, Date startDate, Date endDate) {
        hotel.bookRoom(room, customer, startDate, endDate);
        notificationService.notifyCustomer(customer, "Your booking for room " + room.getRoomNumber() + " is confirmed.");
    }
}

Step 6: Main Application

Now, let’s put everything together in a main application to demonstrate the functionality.

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class HotelManagementApplication {
    public static void main(String[] args) throws ParseException {
        // Create hotel
        Hotel hotel = new Hotel();

        // Add rooms
        hotel.addRoom(new Room(101, "Single"));
        hotel.addRoom(new Room(102, "Double"));
        hotel.addRoom(new Room(103, "Suite"));

        // Create customers
        Customer alice = new Customer("Alice", "alice@example.com");
        Customer bob = new Customer("Bob", "bob@example.com");

        // Choose a notification service
        NotificationService notificationService = new EmailNotificationService(); // or new SMSNotificationService();

        // Create booking manager
        BookingManager bookingManager = new BookingManager(hotel, notificationService);

        // Simulate room booking
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        Date startDate = dateFormat.parse("2024-10-01");
        Date endDate = dateFormat.parse("2024-10-05");

        bookingManager.bookRoom(hotel.getAvailableRooms().get(0), alice, startDate, endDate); // Book room 101 for Alice
        bookingManager.bookRoom(hotel.getAvailableRooms().get(0), bob, startDate, endDate); // Attempt to book room 101 for Bob
    }
}

Summary of SOLID Principles Applied

  1. Single Responsibility Principle (SRP):

    • Room, Customer, and Booking classes each have a single responsibility.
    • Hotel manages room and booking logic.
    • BookingManager handles the booking process and notifications.
  2. Open/Closed Principle (OCP):

    • The NotificationService interface allows for new notification methods (like push notifications) to be added without modifying existing code.
  3. Liskov Substitution Principle (LSP):

    • Any implementation of the NotificationService can be used interchangeably without affecting the behavior of the BookingManager. For example, you can switch from EmailNotificationService to SMSNotificationService seamlessly.
  4. Interface Segregation Principle (ISP):

    • The NotificationService interface is focused, ensuring that clients only implement the methods they need. For instance, if we later create a PushNotificationService, it will only implement the notification method without being burdened with unrelated methods.
  5. Dependency Inversion Principle (DIP):

    • BookingManager depends on the NotificationService abstraction, not on concrete implementations. This decoupling allows for easier testing and swapping of notification services.

Conclusion

This complete example illustrates the application of all five SOLID principles in a hotel management system. By following these principles, the system is modular, maintainable, and extensible. Each component is responsible for a specific function, and the design allows for easy adaptation as new requirements emerge, such as adding different types of notifications or additional features related to room management and customer services.

Let's create a complete example of an Online Payment System similar to Google Pay or PayPal that demonstrates the SOLID principles. This example will cover various aspects of an online payment system, including user management, payment processing, and notification services.

Example: Online Payment System

Step 1: Define the Domain Models

We’ll start with the core models: User, Payment, and Account.

// User class
class User {
    private String username;
    private String email;

    public User(String username, String email) {
        this.username = username;
        this.email = email;
    }

    public String getUsername() {
        return username;
    }

    public String getEmail() {
        return email;
    }
}

// Account class
class Account {
    private User user;
    private double balance;

    public Account(User user, double initialBalance) {
        this.user = user;
        this.balance = initialBalance;
    }

    public User getUser() {
        return user;
    }

    public double getBalance() {
        return balance;
    }

    public void deposit(double amount) {
        balance += amount;
    }

    public void withdraw(double amount) {
        if (amount <= balance) {
            balance -= amount;
        } else {
            throw new IllegalArgumentException("Insufficient balance.");
        }
    }
}

// Payment class
class Payment {
    private Account sender;
    private Account receiver;
    private double amount;

    public Payment(Account sender, Account receiver, double amount) {
        this.sender = sender;
        this.receiver = receiver;
        this.amount = amount;
    }

    public Account getSender() {
        return sender;
    }

    public Account getReceiver() {
        return receiver;
    }

    public double getAmount() {
        return amount;
    }
}

Step 2: PaymentProcessor Class

The PaymentProcessor class is responsible for processing payments and follows the Single Responsibility Principle (SRP).

class PaymentProcessor {
    public void processPayment(Payment payment) {
        // Withdraw from sender's account
        payment.getSender().withdraw(payment.getAmount());
        
        // Deposit to receiver's account
        payment.getReceiver().deposit(payment.getAmount());
        
        System.out.println("Payment of $" + payment.getAmount() + " processed from " +
                payment.getSender().getUser().getUsername() + " to " +
                payment.getReceiver().getUser().getUsername());
    }
}

Step 3: NotificationService Interface and Implementations

To adhere to the Open/Closed Principle (OCP), we will create an interface for notifications.

interface NotificationService {
    void notifyUser(User user, String message);
}

class EmailNotificationService implements NotificationService {
    @Override
    public void notifyUser(User user, String message) {
        System.out.println("Sending email to " + user.getEmail() + ": " + message);
    }
}

class SMSNotificationService implements NotificationService {
    @Override
    public void notifyUser(User user, String message) {
        System.out.println("Sending SMS to " + user.getUsername() + ": " + message);
    }
}

Step 4: TransactionManager Class

The TransactionManager class handles payment transactions and notifications. It encapsulates the logic related to transaction management.

class TransactionManager {
    private PaymentProcessor paymentProcessor;
    private NotificationService notificationService;

    public TransactionManager(PaymentProcessor paymentProcessor, NotificationService notificationService) {
        this.paymentProcessor = paymentProcessor;
        this.notificationService = notificationService;
    }

    public void transferFunds(Account sender, Account receiver, double amount) {
        Payment payment = new Payment(sender, receiver, amount);
        paymentProcessor.processPayment(payment);

        // Notify both sender and receiver
        notificationService.notifyUser(sender.getUser(), "You sent $" + amount + " to " + receiver.getUser().getUsername());
        notificationService.notifyUser(receiver.getUser(), "You received $" + amount + " from " + sender.getUser().getUsername());
    }
}

Step 5: Main Application

Now, let’s put everything together in a main application to demonstrate the functionality.

public class PaymentApplication {
    public static void main(String[] args) {
        // Create users
        User alice = new User("Alice", "alice@example.com");
        User bob = new User("Bob", "bob@example.com");

        // Create accounts
        Account aliceAccount = new Account(alice, 1000.00);
        Account bobAccount = new Account(bob, 500.00);

        // Create notification service
        NotificationService notificationService = new EmailNotificationService(); // or new SMSNotificationService();

        // Create payment processor and transaction manager
        PaymentProcessor paymentProcessor = new PaymentProcessor();
        TransactionManager transactionManager = new TransactionManager(paymentProcessor, notificationService);

        // Simulate fund transfer
        transactionManager.transferFunds(aliceAccount, bobAccount, 200.00);
        
        // Check balances
        System.out.println("Alice's balance: $" + aliceAccount.getBalance());
        System.out.println("Bob's balance: $" + bobAccount.getBalance());
        
        // Attempt to transfer more than available balance
        try {
            transactionManager.transferFunds(bobAccount, aliceAccount, 600.00); // Should throw an exception
        } catch (IllegalArgumentException e) {
            System.out.println(e.getMessage());
        }
    }
}

Summary of SOLID Principles Applied

  1. Single Responsibility Principle (SRP):

    • User, Account, and Payment classes each have a single responsibility.
    • PaymentProcessor is responsible for processing payments.
    • TransactionManager handles the transaction process and notifications.
  2. Open/Closed Principle (OCP):

    • The NotificationService interface allows for new notification methods (like push notifications) to be added without modifying existing code.
  3. Liskov Substitution Principle (LSP):

    • Any implementation of the NotificationService can be used interchangeably without affecting the behavior of the TransactionManager. For example, you can switch from EmailNotificationService to SMSNotificationService seamlessly.
  4. Interface Segregation Principle (ISP):

    • The NotificationService interface is focused, ensuring that clients only implement the methods they need. For instance, if we later create a PushNotificationService, it will only implement the notification method without being burdened with unrelated methods.
  5. Dependency Inversion Principle (DIP):

    • TransactionManager depends on the NotificationService abstraction, not on concrete implementations. This decoupling allows for easier testing and swapping of notification services.

Conclusion

This complete example illustrates the application of all five SOLID principles in an online payment system. By following these principles, the system is modular, maintainable, and extensible. Each component is responsible for a specific function, and the design allows for easy adaptation as new requirements emerge, such as adding different types of notifications or additional features related to payment processing and user management.

Creating a Share Market Portal involves various functionalities, such as user management, stock trading, order management, and notifications. Below, I'll provide a structured example that adheres to the SOLID principles.

Example: Share Market Portal

This example includes the following components:

  1. User Management: Managing users and their portfolios.
  2. Stock Management: Managing stocks and their prices.
  3. Order Management: Placing buy/sell orders.
  4. Notification Services: Notifying users about order statuses and stock updates.

Step 1: Define Domain Models

User Class

class User {
    private String id;
    private String name;
    private double balance;

    public User(String id, String name, double balance) {
        this.id = id;
        this.name = name;
        this.balance = balance;
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public double getBalance() {
        return balance;
    }

    public void deposit(double amount) {
        balance += amount;
    }

    public void withdraw(double amount) {
        if (amount <= balance) {
            balance -= amount;
        } else {
            throw new IllegalArgumentException("Insufficient balance.");
        }
    }
}

Stock Class

class Stock {
    private String symbol;
    private double price;

    public Stock(String symbol, double price) {
        this.symbol = symbol;
        this.price = price;
    }

    public String getSymbol() {
        return symbol;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }
}

Step 2: Order Class

Order Class

abstract class Order {
    protected User user;
    protected Stock stock;
    protected int quantity;

    public Order(User user, Stock stock, int quantity) {
        this.user = user;
        this.stock = stock;
        this.quantity = quantity;
    }

    public abstract void execute();
}

Step 3: Concrete Order Classes

BuyOrder Class

class BuyOrder extends Order {
    public BuyOrder(User user, Stock stock, int quantity) {
        super(user, stock, quantity);
    }

    @Override
    public void execute() {
        double totalCost = stock.getPrice() * quantity;
        if (user.getBalance() >= totalCost) {
            user.withdraw(totalCost);
            System.out.println("Buy order executed for " + quantity + " shares of " + stock.getSymbol());
        } else {
            throw new IllegalArgumentException("Insufficient balance for buy order.");
        }
    }
}

SellOrder Class

class SellOrder extends Order {
    public SellOrder(User user, Stock stock, int quantity) {
        super(user, stock, quantity);
    }

    @Override
    public void execute() {
        double totalRevenue = stock.getPrice() * quantity;
        user.deposit(totalRevenue);
        System.out.println("Sell order executed for " + quantity + " shares of " + stock.getSymbol());
    }
}

Step 4: OrderService Class

The OrderService class handles order execution and adheres to the Single Responsibility Principle (SRP).

class OrderService {
    public void processOrder(Order order) {
        order.execute(); // Delegate executing to the specific order type
    }
}

Step 5: NotificationService Interface and Implementations

To adhere to the Open/Closed Principle (OCP), we will create an interface for notifications.

interface NotificationService {
    void notifyUser(User user, String message);
}

class EmailNotificationService implements NotificationService {
    @Override
    public void notifyUser(User user, String message) {
        System.out.println("Sending Email Notification to " + user.getName() + ": " + message);
    }
}

class SMSNotificationService implements NotificationService {
    @Override
    public void notifyUser(User user, String message) {
        System.out.println("Sending SMS Notification to " + user.getName() + ": " + message);
    }
}

Step 6: TradingManager Class

The TradingManager class handles trading operations and notifications. It encapsulates the logic related to managing trades.

class TradingManager {
    private OrderService orderService;
    private NotificationService notificationService;

    public TradingManager(OrderService orderService, NotificationService notificationService) {
        this.orderService = orderService;
        this.notificationService = notificationService;
    }

    public void executeTrade(User user, Stock stock, int quantity, String orderType) {
        Order order;

        if ("BUY".equalsIgnoreCase(orderType)) {
            order = new BuyOrder(user, stock, quantity);
        } else if ("SELL".equalsIgnoreCase(orderType)) {
            order = new SellOrder(user, stock, quantity);
        } else {
            throw new IllegalArgumentException("Unsupported order type");
        }

        orderService.processOrder(order);
        notificationService.notifyUser(user, "Your " + orderType + " order for " + quantity + " shares of " + stock.getSymbol() + " has been executed.");
    }
}

Step 7: Main Application

Now, let’s put everything together in a main application to demonstrate the functionality.

public class ShareMarketPortalApplication {
    public static void main(String[] args) {
        // Create users
        User alice = new User("1", "Alice", 10000.00);
        User bob = new User("2", "Bob", 5000.00);

        // Create stocks
        Stock appleStock = new Stock("AAPL", 150.00);
        Stock googleStock = new Stock("GOOGL", 2800.00);

        // Create services
        OrderService orderService = new OrderService();
        NotificationService emailNotificationService = new EmailNotificationService();
        NotificationService smsNotificationService = new SMSNotificationService();

        // Create trading managers
        TradingManager tradingManagerEmail = new TradingManager(orderService, emailNotificationService);
        TradingManager tradingManagerSMS = new TradingManager(orderService, smsNotificationService);

        // Simulate buying and selling stocks
        tradingManagerEmail.executeTrade(alice, appleStock, 20, "BUY");
        tradingManagerSMS.executeTrade(bob, googleStock, 2, "SELL");

        // Print remaining balances
        System.out.println("Alice's remaining balance: $" + alice.getBalance());
        System.out.println("Bob's remaining balance: $" + bob.getBalance());
    }
}

Summary of SOLID Principles Applied

  1. Single Responsibility Principle (SRP):

    • User, Stock, Order, BuyOrder, SellOrder, OrderService, and TradingManager classes each have a single responsibility.
    • NotificationService implementations are focused on sending notifications.
  2. Open/Closed Principle (OCP):

    • The Order class can be extended to support new order types (e.g., limit orders) without modifying existing code.
    • The NotificationService interface allows for new notification methods to be added without changing existing services.
  3. Liskov Substitution Principle (LSP):

    • Any implementation of the Order class can be used interchangeably in the OrderService without affecting its behavior.
  4. Interface Segregation Principle (ISP):

    • The NotificationService interface is focused, ensuring that clients only implement the methods they need.
  5. Dependency Inversion Principle (DIP):

    • TradingManager depends on the OrderService and NotificationService abstractions, not on concrete implementations. This decoupling allows for easier testing and swapping of services.

Conclusion

This complete example illustrates the application of all five SOLID principles in a share market portal. By following these principles, the system is modular, maintainable, and extensible. Each component is responsible for a specific function, and the design allows for easy adaptation as new requirements emerge, such as adding new order types, notifications, or additional features related to stock trading and user management.

Creating a complete example of a communication platform similar to Twilio using SOLID design principles involves multiple aspects, such as message handling, user management, and notification services. Below is a structured approach that adheres to the SOLID principles.

Example: Communication Platform (like Twilio)

This example includes the following components:

  1. User Management: Managing users and their contact information.
  2. Messaging: Sending and receiving messages.
  3. Notification Services: Different methods to notify users.
  4. Message Types: Different types of messages (SMS, Email, etc.).

Step 1: Define Domain Models

User Class

class User {
    private String id;
    private String name;
    private String phoneNumber;
    private String email;

    public User(String id, String name, String phoneNumber, String email) {
        this.id = id;
        this.name = name;
        this.phoneNumber = phoneNumber;
        this.email = email;
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getPhoneNumber() {
        return phoneNumber;
    }

    public String getEmail() {
        return email;
    }
}

Message Class

abstract class Message {
    protected User sender;
    protected User recipient;
    protected String content;

    public Message(User sender, User recipient, String content) {
        this.sender = sender;
        this.recipient = recipient;
        this.content = content;
    }

    public abstract void send();
}

Step 2: Concrete Message Classes

SMSMessage Class

class SMSMessage extends Message {
    public SMSMessage(User sender, User recipient, String content) {
        super(sender, recipient, content);
    }

    @Override
    public void send() {
        // Logic to send SMS
        System.out.println("Sending SMS from " + sender.getPhoneNumber() +
                " to " + recipient.getPhoneNumber() + ": " + content);
    }
}

EmailMessage Class

class EmailMessage extends Message {
    public EmailMessage(User sender, User recipient, String content) {
        super(sender, recipient, content);
    }

    @Override
    public void send() {
        // Logic to send Email
        System.out.println("Sending Email from " + sender.getEmail() +
                " to " + recipient.getEmail() + ": " + content);
    }
}

Step 3: MessageService Class

The MessageService class handles sending messages and adheres to the Single Responsibility Principle (SRP).

class MessageService {
    public void sendMessage(Message message) {
        message.send(); // Delegate sending to the specific message type
    }
}

Step 4: NotificationService Interface and Implementations

To adhere to the Open/Closed Principle (OCP), we will create an interface for notifications.

interface NotificationService {
    void notifyUser(User user, String message);
}

class SMSNotificationService implements NotificationService {
    @Override
    public void notifyUser(User user, String message) {
        System.out.println("Sending SMS Notification to " + user.getPhoneNumber() + ": " + message);
    }
}

class EmailNotificationService implements NotificationService {
    @Override
    public void notifyUser(User user, String message) {
        System.out.println("Sending Email Notification to " + user.getEmail() + ": " + message);
    }
}

Step 5: CommunicationManager Class

The CommunicationManager class handles communication between users and notifications. It encapsulates the logic related to managing communications.

class CommunicationManager {
    private MessageService messageService;
    private NotificationService notificationService;

    public CommunicationManager(MessageService messageService, NotificationService notificationService) {
        this.messageService = messageService;
        this.notificationService = notificationService;
    }

    public void sendMessage(User sender, User recipient, String content, String messageType) {
        Message message;

        if ("SMS".equalsIgnoreCase(messageType)) {
            message = new SMSMessage(sender, recipient, content);
        } else if ("EMAIL".equalsIgnoreCase(messageType)) {
            message = new EmailMessage(sender, recipient, content);
        } else {
            throw new IllegalArgumentException("Unsupported message type");
        }

        messageService.sendMessage(message);
        notificationService.notifyUser(recipient, "You have a new message from " + sender.getName());
    }
}

Step 6: Main Application

Now, let’s put everything together in a main application to demonstrate the functionality.

public class CommunicationPlatformApplication {
    public static void main(String[] args) {
        // Create users
        User alice = new User("1", "Alice", "1234567890", "alice@example.com");
        User bob = new User("2", "Bob", "0987654321", "bob@example.com");

        // Create services
        MessageService messageService = new MessageService();
        NotificationService smsNotificationService = new SMSNotificationService();
        NotificationService emailNotificationService = new EmailNotificationService();

        // Create communication managers
        CommunicationManager smsCommunicationManager = new CommunicationManager(messageService, smsNotificationService);
        CommunicationManager emailCommunicationManager = new CommunicationManager(messageService, emailNotificationService);

        // Simulate sending messages
        smsCommunicationManager.sendMessage(alice, bob, "Hello Bob! This is a test SMS.", "SMS");
        emailCommunicationManager.sendMessage(alice, bob, "Hello Bob! This is a test Email.", "EMAIL");
    }
}

Summary of SOLID Principles Applied

  1. Single Responsibility Principle (SRP):

    • User, Message, SMSMessage, EmailMessage, and MessageService classes each have a single responsibility.
    • CommunicationManager is responsible for handling message sending and notifications.
  2. Open/Closed Principle (OCP):

    • The Message class can be extended to support new message types (e.g., push notifications) without modifying existing code.
    • The NotificationService interface allows for new notification methods to be added without changing existing services.
  3. Liskov Substitution Principle (LSP):

    • Any implementation of the Message class can be used interchangeably in the MessageService without affecting its behavior.
  4. Interface Segregation Principle (ISP):

    • The NotificationService interface is focused, ensuring that clients only implement the methods they need.
  5. Dependency Inversion Principle (DIP):

    • CommunicationManager depends on the MessageService and NotificationService abstractions, not on concrete implementations. This decoupling allows for easier testing and swapping of services.

Conclusion

This complete example illustrates the application of all five SOLID principles in a communication platform similar to Twilio. By following these principles, the system is modular, maintainable, and extensible. Each component is responsible for a specific function, and the design allows for easy adaptation as new requirements emerge, such as adding new message types or notification methods.

Let’s create a complete example of a Chat-Based System that demonstrates the SOLID principles. In this example, we’ll cover the components of a chat application, including user management, message handling, and notification services.

Example: Chat-Based System

Step 1: Define the Domain Models

We’ll start with the core models: User, Message, and ChatRoom.

// User class
class User {
    private String username;

    public User(String username) {
        this.username = username;
    }

    public String getUsername() {
        return username;
    }
}

// Message class
class Message {
    private User sender;
    private String content;

    public Message(User sender, String content) {
        this.sender = sender;
        this.content = content;
    }

    public User getSender() {
        return sender;
    }

    public String getContent() {
        return content;
    }
}

Step 2: ChatRoom Class

The ChatRoom class is responsible for managing users and messages. It follows the Single Responsibility Principle (SRP).

import java.util.ArrayList;
import java.util.List;

class ChatRoom {
    private List<User> users;
    private List<Message> messages;

    public ChatRoom() {
        this.users = new ArrayList<>();
        this.messages = new ArrayList<>();
    }

    public void addUser(User user) {
        users.add(user);
    }

    public void removeUser(User user) {
        users.remove(user);
    }

    public void sendMessage(Message message) {
        messages.add(message);
        // Notify users about the new message
        // This can be a separate notification service
    }

    public List<Message> getMessages() {
        return messages;
    }
}

Step 3: NotificationService Interface and Implementations

To adhere to the Open/Closed Principle (OCP), we will create an interface for notifications.

interface NotificationService {
    void notifyUser(User user, Message message);
}

class EmailNotificationService implements NotificationService {
    @Override
    public void notifyUser(User user, Message message) {
        System.out.println("Sending email to " + user.getUsername() + ": " + message.getContent());
    }
}

class SMSNotificationService implements NotificationService {
    @Override
    public void notifyUser(User user, Message message) {
        System.out.println("Sending SMS to " + user.getUsername() + ": " + message.getContent());
    }
}

Step 4: MessageHandler Class

The MessageHandler will handle sending messages and notifying users. It encapsulates the logic related to message processing.

class MessageHandler {
    private ChatRoom chatRoom;
    private NotificationService notificationService;

    public MessageHandler(ChatRoom chatRoom, NotificationService notificationService) {
        this.chatRoom = chatRoom;
        this.notificationService = notificationService;
    }

    public void handleMessage(User user, String content) {
        Message message = new Message(user, content);
        chatRoom.sendMessage(message);

        // Notify all users about the new message
        for (User recipient : chatRoom.getUsers()) {
            if (!recipient.equals(user)) { // Don't notify the sender
                notificationService.notifyUser(recipient, message);
            }
        }
    }
}

Step 5: Main Application

Now, let’s put everything together in a main application to demonstrate the functionality.

public class ChatApplication {
    public static void main(String[] args) {
        // Create chat room
        ChatRoom chatRoom = new ChatRoom();

        // Create users
        User alice = new User("Alice");
        User bob = new User("Bob");

        // Add users to chat room
        chatRoom.addUser(alice);
        chatRoom.addUser(bob);

        // Choose a notification service
        NotificationService notificationService = new EmailNotificationService(); // or new SMSNotificationService();

        // Create message handler
        MessageHandler messageHandler = new MessageHandler(chatRoom, notificationService);

        // Simulate sending messages
        messageHandler.handleMessage(alice, "Hello, Bob!");
        messageHandler.handleMessage(bob, "Hi, Alice! How are you?");
    }
}

Summary of SOLID Principles Applied

  1. Single Responsibility Principle (SRP):

    • ChatRoom manages users and messages.
    • MessageHandler is responsible for processing messages and notifying users.
    • Each class has a single responsibility.
  2. Open/Closed Principle (OCP):

    • The NotificationService interface allows for new notification methods to be added (like push notifications) without modifying existing code.
  3. Liskov Substitution Principle (LSP):

    • Any implementation of the NotificationService can be used interchangeably without affecting the behavior of the MessageHandler. For example, you can switch from EmailNotificationService to SMSNotificationService seamlessly.
  4. Interface Segregation Principle (ISP):

    • The NotificationService interface is focused, ensuring that clients only implement the methods they need. For instance, if we later create a PushNotificationService, it will only implement the notification method without being burdened with unrelated methods.
  5. Dependency Inversion Principle (DIP):

    • MessageHandler depends on the NotificationService abstraction, not on concrete implementations. This decoupling allows for easier testing and swapping of notification services.

Conclusion

This complete example illustrates the application of all five SOLID principles in a chat-based system. By following these principles, the system is modular, maintainable, and extensible. Each component is responsible for a specific function, and the design allows for easy adaptation as new requirements emerge, such as adding different types of notifications or additional features to the chat functionality.

DESIGN Patterns