0% found this document useful (0 votes)
14 views39 pages

LLD HLD

The document outlines the SOLID principles, which are five design principles aimed at improving software maintainability, flexibility, and scalability, particularly in object-oriented design. Each principle is explained with definitions, importance, and examples in Java, covering Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion principles. Additionally, it briefly introduces design patterns categorized into creational, structural, and behavioral types, emphasizing their role in solving common software design problems.

Uploaded by

shwetanshu.g92
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
14 views39 pages

LLD HLD

The document outlines the SOLID principles, which are five design principles aimed at improving software maintainability, flexibility, and scalability, particularly in object-oriented design. Each principle is explained with definitions, importance, and examples in Java, covering Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion principles. Additionally, it briefly introduces design patterns categorized into creational, structural, and behavioral types, emphasizing their role in solving common software design problems.

Uploaded by

shwetanshu.g92
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 39

The SOLID principles are a set of five design principles that help developers create more

maintainable, flexible, and scalable software. They are particularly important in object-
oriented design and can be applied to Java development. Here’s a simple breakdown of each
principle with examples in Java:

1. S - Single Responsibility Principle (SRP)

 Definition: A class should have only one reason to change, meaning it should only
have one job or responsibility.
 Why it's important: It makes the code easier to maintain and less prone to bugs
because changes in one responsibility don't affect others.

Example:

java
Copy code
// Bad Example (violating SRP)
public class UserService {
public void addUser(User user) {
// logic to add user
}

public void generateReport(User user) {


// logic to generate report
}
}

// Good Example (following SRP)


public class UserService {
public void addUser(User user) {
// logic to add user
}
}

public class ReportService {


public void generateReport(User user) {
// logic to generate report
}
}

In the bad example, the UserService class has two responsibilities: adding users and
generating reports. In the good example, each class has a single responsibility.

2. O - Open/Closed Principle (OCP)

 Definition: Software entities (classes, modules, functions) should be open for


extension but closed for modification. This means you can extend the behavior of a
class without modifying its source code.
 Why it's important: It allows your code to be easily extended without breaking
existing functionality.

Example:
java
Copy code
// Bad Example (violating OCP)
public class PaymentProcessor {
public void processPayment(String paymentType) {
if (paymentType.equals("CreditCard")) {
// process credit card payment
} else if (paymentType.equals("PayPal")) {
// process PayPal payment
}
}
}

// Good Example (following OCP)


public interface PaymentMethod {
void processPayment();
}

public class CreditCardPayment implements PaymentMethod {


@Override
public void processPayment() {
// process credit card payment
}
}

public class PayPalPayment implements PaymentMethod {


@Override
public void processPayment() {
// process PayPal payment
}
}

public class PaymentProcessor {


public void processPayment(PaymentMethod paymentMethod) {
paymentMethod.processPayment();
}
}

In the bad example, to add new payment methods, you'd have to modify the
PaymentProcessor class. In the good example, you can easily extend the system by creating
new classes (like ApplePayPayment) without changing the existing code.

3. L - Liskov Substitution Principle (LSP)

 Definition: Objects of a superclass should be replaceable with objects of a subclass


without affecting the correctness of the program.
 Why it's important: It ensures that inheritance is properly implemented, and
subclasses behave in a way that doesn’t surprise the user of the class.

Example:

java
Copy code
// Bad Example (violating LSP)
public class Bird {
public void fly() {
System.out.println("Flying...");
}
}

public class Ostrich extends Bird {


@Override
public void fly() {
throw new UnsupportedOperationException("Ostriches can't fly");
}
}

// Good Example (following LSP)


public class Bird {
public void move() {
System.out.println("Moving...");
}
}

public class Sparrow extends Bird {


@Override
public void move() {
System.out.println("Flying...");
}
}

public class Ostrich extends Bird {


@Override
public void move() {
System.out.println("Running...");
}
}

In the bad example, Ostrich cannot fly, so it breaks the expectation of the Bird class. In the
good example, both Sparrow and Ostrich override move() appropriately, following the
principle.

4. I - Interface Segregation Principle (ISP)

 Definition: A client should not be forced to implement interfaces it doesn't use.


Instead of having one large interface, create smaller, more specific interfaces.
 Why it's important: It makes the system easier to maintain and more flexible by
ensuring that classes only implement what they actually need.

Example:

java
Copy code
// Bad Example (violating ISP)
public interface Worker {
void work();
void eat();
}

public class HumanWorker implements Worker {


@Override
public void work() {
// working
}

@Override
public void eat() {
// eating
}
}

public class RobotWorker implements Worker {


@Override
public void work() {
// working
}

@Override
public void eat() {
throw new UnsupportedOperationException("Robot can't eat");
}
}

// Good Example (following ISP)


public interface Workable {
void work();
}

public interface Eatable {


void eat();
}

public class HumanWorker implements Workable, Eatable {


@Override
public void work() {
// working
}

@Override
public void eat() {
// eating
}
}

public class RobotWorker implements Workable {


@Override
public void work() {
// working
}
}

In the bad example, RobotWorker is forced to implement eat(), even though robots don’t
eat. The good example separates the interfaces into smaller, more focused ones, so classes
only implement what they need.

5. D - Dependency Inversion Principle (DIP)


 Definition: High-level modules should not depend on low-level modules. Both should
depend on abstractions (e.g., interfaces). Additionally, abstractions should not depend
on details. Details should depend on abstractions.
 Why it's important: It promotes loose coupling between modules and makes the
system easier to extend and test.

Example:

java
Copy code
// Bad Example (violating DIP)
public class LightBulb {
public void turnOn() {
System.out.println("Light is ON");
}

public void turnOff() {


System.out.println("Light is OFF");
}
}

public class Switch {


private LightBulb bulb;

public Switch(LightBulb bulb) {


this.bulb = bulb;
}

public void operate() {


// directly controlling the bulb
bulb.turnOn();
}
}

// Good Example (following DIP)


public interface Switchable {
void turnOn();
void turnOff();
}

public class LightBulb implements Switchable {


@Override
public void turnOn() {
System.out.println("Light is ON");
}

@Override
public void turnOff() {
System.out.println("Light is OFF");
}
}

public class Switch {


private Switchable device;

public Switch(Switchable device) {


this.device = device;
}
public void operate() {
device.turnOn();
}
}

In the bad example, the Switch class directly depends on the LightBulb class. In the good
example, the Switch depends on the Switchable interface, allowing for flexibility in
switching different devices (e.g., a fan or a TV) without changing the Switch class.

Summary of SOLID in Simple Terms:

 S: A class should only have one job.


 O: You can extend a class, but don't modify it.
 L: A subclass should behave like its parent class.
 I: Don’t force a class to implement methods it doesn’t need.
 D: Depend on abstractions (interfaces), not concrete classes.

Applying these principles will lead to cleaner, more modular, and maintainable code! Let me
know if you need more details or examples.

2/2
4o

You said:

In the LSP example, how will you implement fly ?


ChatGPT said:

ChatGPT

Good question! In the Liskov Substitution Principle (LSP) example, implementing a fly()
method must ensure that only birds that can actually fly use it. Here's a better approach using
Java:

Refactored Solution for fly()

To avoid forcing all Bird subclasses to implement fly(), you can separate the behavior into
its own interface for flying birds:

Example Code
java
Copy code
// General Bird Class
public class Bird {
public void move() {
System.out.println("This bird is moving...");
}
}

// Interface for flying capability


public interface Flyable {
void fly();
}

// Flying Bird Class


public class Sparrow extends Bird implements Flyable {
@Override
public void move() {
System.out.println("Sparrow is flying...");
}

@Override
public void fly() {
System.out.println("Sparrow is flying high!");
}
}

// Non-Flying Bird Class


public class Ostrich extends Bird {
@Override
public void move() {
System.out.println("Ostrich is running...");
}
}

// Test LSP Compliance


public class Main {
public static void main(String[] args) {
Bird sparrow = new Sparrow();
sparrow.move(); // Outputs: Sparrow is flying...

if (sparrow instanceof Flyable) {


((Flyable) sparrow).fly(); // Outputs: Sparrow is flying high!
}

Bird ostrich = new Ostrich();


ostrich.move(); // Outputs: Ostrich is running...
}
}

Why This Works

1. Separate Concerns: The Flyable interface ensures that only birds capable of flying
implement the fly() method.
2. Avoiding Unsupported Behaviors: Non-flying birds like Ostrich don't need to deal
with a fly() method they can’t support.
3. LSP Compliance: Both Sparrow and Ostrich are substitutable for the Bird type
without causing unexpected behavior.

Key Benefits
 You keep your code modular and maintainable.
 Future additions (like Penguin) are simpler to implement and don't break existing
logic.

Let me know if you’d like further clarification! 😊

4o

You said:

What does instanceof do here ?


ChatGPT said:

ChatGPT

The instanceof operator in Java checks whether an object is an instance of a specific class
or implements a specific interface. In the given code, the instanceof operator ensures that
we only call the fly() method on objects that implement the Flyable interface.

How it works in the example

Here’s the relevant part of the code:

java
Copy code
if (sparrow instanceof Flyable) {
((Flyable) sparrow).fly();
}

1. instanceof Flyable:
o It checks if the sparrow object implements the Flyable interface.
o This ensures that the fly() method exists for the object and can be safely
called.
2. ((Flyable) sparrow).fly();:
o If the check is true, the object is cast to the Flyable interface, allowing us to
call the fly() method.
o Casting tells the compiler that this object should be treated as a Flyable type,
enabling us to access fly().

Why instanceof is useful here

 Not all Bird objects can fly (e.g., Ostrich), so it’s necessary to ensure that we’re
only calling fly() on birds that support it.
 Without this check, trying to call fly() on a non-flying bird would result in a
ClassCastException or a compile-time error.
What happens without instanceof?
java
Copy code
Bird ostrich = new Ostrich();
((Flyable) ostrich).fly(); // Will throw a ClassCastException at runtime

This would fail at runtime because Ostrich does not implement the Flyable interface. Using
instanceof prevents such errors by ensuring that the cast is only attempted on compatible
objects.

Best Practices

 instanceof is useful for safety, but heavy use of it can indicate a need for better
design.
 For example, polymorphism can often eliminate the need for instanceof checks. In
this case, the move() method on Bird provides polymorphic behavior for both flying
and non-flying birds without requiring manual checks.

Design Patterns :

Relationship Type Dependency Example


Inheritance IS-A Strong Car extends Vehicle
HAS-A Very House HAS-A Room (Room cannot exist without
Composition
(Strong) Strong House)
HAS-A University HAS-A Student (Student can exist without
Aggregation Loose
(Weak) University)

Design patterns are proven solutions to common software design problems. They are
categorized into three main types:

1. Creational Patterns: Deal with object creation.


2. Structural Patterns: Deal with object composition and structure.
3. Behavioral Patterns: Deal with communication between objects.

1. Creational Design Patterns

Focus: Deal with object creation mechanisms, trying to create objects in a manner suitable to
the situation.

1. Singleton: Ensures a class has only one instance and provides a global point of access
to it.
2. Factory Method: Defines an interface for creating an object but allows subclasses to
alter the type of objects that will be created.
3. Abstract Factory: Provides an interface for creating families of related or dependent
objects without specifying their concrete classes.
4. Builder: Separates the construction of a complex object from its representation so that
the same construction process can create different representations.
5. Prototype: Creates a new object by copying an existing object (prototype) rather than
instantiating a new one.

2. Structural Design Patterns

Focus: Deal with the composition of classes or objects to form larger structures.

1. Adapter: Provides a bridge between two incompatible interfaces.


2. Bridge: Decouples an abstraction from its implementation so that the two can vary
independently.
3. Composite: Composes objects into tree-like structures to represent part-whole
hierarchies, allowing clients to treat individual objects and compositions uniformly.
4. Decorator: Adds new functionality to an object dynamically without altering its
structure.
5. Facade: Provides a simplified interface to a larger body of code, hiding its
complexities.
6. Flyweight: Reduces memory usage by sharing as much data as possible with similar
objects.
7. Proxy: Provides a placeholder or surrogate to control access to another object.

3. Behavioral Design Patterns

Focus: Deal with communication and interaction between objects.

1. Chain of Responsibility: Passes a request along a chain of handlers until one of them
handles it.
2. Command: Encapsulates a request as an object, allowing for parameterization and
queuing of requests.
3. Interpreter: Defines a grammar for a language and interprets sentences in that
language.
4. Iterator: Provides a way to sequentially access elements in a collection without
exposing its underlying representation.
5. Mediator: Reduces coupling between objects by introducing a mediator object that
handles communication.
6. Memento: Captures and restores an object's internal state without violating
encapsulation.
7. Observer: Establishes a dependency between objects such that when one changes
state, all its dependents are notified.
8. State: Allows an object to alter its behavior when its internal state changes.
9. Strategy: Defines a family of algorithms and lets the client choose one at runtime.
10. Template Method: Defines the skeleton of an algorithm in a superclass, allowing
subclasses to override specific steps.
11. Visitor: Separates an algorithm from the object structure on which it operates, adding
new operations without modifying the objects.
3. Behavioral Design Patterns

Behavioral patterns focus on how objects communicate and interact, ensuring flexibility in
how tasks are executed. Let’s explore some of the most commonly used behavioral patterns
with examples in Java.

What Problem Does the Strategy Pattern Solve?

The Strategy Pattern solves the problem of implementing multiple algorithms or


behaviors dynamically without changing the client code. This is especially useful when
you have a family of algorithms, and you want the client to choose the appropriate one at
runtime.

The Problem Without Strategy Pattern

Suppose you are building a shopping cart system where users can pay using different
methods like Credit Card or PayPal. Without the Strategy Pattern, you might write a large,
monolithic checkout method with multiple conditional statements to handle each payment
type.

Example Without Strategy Pattern


java
Copy code
public class ShoppingCart {
public void checkout(String paymentMethod, int amount) {
if (paymentMethod.equalsIgnoreCase("CREDIT_CARD")) {
System.out.println("Paid $" + amount + " using Credit Card.");
} else if (paymentMethod.equalsIgnoreCase("PAYPAL")) {
System.out.println("Paid $" + amount + " using PayPal.");
} else {
System.out.println("Invalid payment method!");
}
}
}

// Main
public class Main {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
cart.checkout("CREDIT_CARD", 100); // Paid $100 using Credit Card.
cart.checkout("PAYPAL", 200); // Paid $200 using PayPal.
cart.checkout("BITCOIN", 300); // Invalid payment method!
}
}

Problems with This Approach

1. Violation of Open/Closed Principle:


o Adding a new payment method requires modifying the checkout method,
increasing the risk of breaking existing functionality.

2. Tight Coupling:
o The ShoppingCart class is tightly coupled with all payment methods, making it
harder to maintain.

3. Code Duplication:
o If other areas of the application need similar logic, the conditions will be repeated.

4. Testing Complexity:
o Each conditional branch must be tested, making testing cumbersome.

Solution Using Strategy Pattern

With the Strategy Pattern:

1. Each payment method is encapsulated in its own class, adhering to the Single Responsibility
Principle.
2. New payment methods can be added without modifying the client code, adhering to the
Open/Closed Principle.
3. The client dynamically selects the payment method at runtime.

Example Using Strategy Pattern

Here’s the refactored version using the Strategy Pattern:

java
Copy code
// Strategy Interface
public interface PaymentStrategy {
void pay(int amount);
}

// Concrete Strategies
public class CreditCardPayment implements PaymentStrategy {
private String cardNumber;

public CreditCardPayment(String cardNumber) {


this.cardNumber = cardNumber;
}

@Override
public void pay(int amount) {
System.out.println("Paid $" + amount + " using Credit Card: " +
cardNumber);
}
}

public class PayPalPayment implements PaymentStrategy {


private String email;

public PayPalPayment(String email) {


this.email = email;
}

@Override
public void pay(int amount) {
System.out.println("Paid $" + amount + " using PayPal: " + email);
}
}

// Context Class
public class ShoppingCart {
private PaymentStrategy paymentStrategy;

public void setPaymentStrategy(PaymentStrategy paymentStrategy) {


this.paymentStrategy = paymentStrategy;
}

public void checkout(int amount) {


if (paymentStrategy == null) {
System.out.println("Please select a payment method!");
} else {
paymentStrategy.pay(amount);
}
}
}

// Main
public class Main {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();

// Using Credit Card


cart.setPaymentStrategy(new CreditCardPayment("1234-5678-9876-
5432"));
cart.checkout(100); // Paid $100 using Credit Card: 1234-5678-9876-
5432

// Using PayPal
cart.setPaymentStrategy(new PayPalPayment("user@example.com"));
cart.checkout(200); // Paid $200 using PayPal: user@example.com
}
}

Benefits of Using Strategy Pattern

1. Open/Closed Principle:
o Adding a new payment method (e.g., Bitcoin) only requires creating a new class
implementing PaymentStrategy. The ShoppingCart remains unchanged.

2. Separation of Concerns:
o Each payment method is encapsulated in its own class, making the code easier to
understand and maintain.

3. Reusability:
o The PaymentStrategy interface and its implementations can be reused in other
parts of the application.

4. Flexibility:
o The payment method can be changed dynamically at runtime.

Adding a New Payment Method (Bitcoin)

Let’s say we need to add a new payment method, Bitcoin. Without changing existing code,
we can do this:

java
Copy code
// New Concrete Strategy
public class BitcoinPayment implements PaymentStrategy {
private String walletAddress;

public BitcoinPayment(String walletAddress) {


this.walletAddress = walletAddress;
}

@Override
public void pay(int amount) {
System.out.println("Paid $" + amount + " using Bitcoin: " +
walletAddress);
}
}

// Main
public class Main {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();

// Using Bitcoin
cart.setPaymentStrategy(new BitcoinPayment("1A2b3C4D5E6F"));
cart.checkout(300); // Paid $300 using Bitcoin: 1A2b3C4D5E6F
}
}

Summary

 Without Strategy: The code is tightly coupled, violates the Open/Closed Principle, and is
harder to maintain.
 With Strategy: The code is flexible, extensible, and easier to maintain, and adheres to SOLID
principles.

Observer Pattern

The Observer Pattern is a behavioral design pattern used when you need to notify multiple
objects (observers) about changes .in the state of another object (subject). It ensures a one-to-
many relationship between objects.
Why Do We Need the Observer Pattern?

1. Dynamic Communication: When the state of one object changes, multiple dependent
objects should be notified without tightly coupling them.
2. Decoupling: The subject doesn’t need to know the specifics of its observers, just that they
need to be notified of updates.
3. Real-time Updates: Ideal for use cases like UI components reacting to data changes, event-
driven systems, or live updates.

What Happens If We Don’t Use the Observer Pattern?

Without the Observer Pattern

Imagine a Stock Market Application where you track the price of a stock and notify
multiple users (via email, SMS, app notifications) about price changes.

Without the Observer Pattern, you might hardcode the notifications like this:

java
Copy code
public class Stock {
private String symbol;
private double price;

public Stock(String symbol, double price) {


this.symbol = symbol;
this.price = price;
}

public void setPrice(double price) {


this.price = price;
System.out.println(symbol + " price updated to $" + price);

// Notify users manually


sendEmail();
sendSMS();
pushNotification();
}

private void sendEmail() {


System.out.println("Sending email notification...");
}

private void sendSMS() {


System.out.println("Sending SMS notification...");
}

private void pushNotification() {


System.out.println("Sending app notification...");
}
}
Problems Without the Observer Pattern

1. Tight Coupling:
o The Stock class is tightly coupled with all notification types (email, SMS, push
notifications).
o Adding a new notification method (e.g., Slack alerts) requires modifying the Stock
class.

2. Violation of Open/Closed Principle:


o Changes to the notification logic require modifying the Stock class, increasing the
risk of bugs.

3. Limited Reusability:
o Notifications are embedded in the Stock class, making it harder to reuse or extend.

4. Scalability Issues:
o As the number of users or notification methods grows, the Stock class becomes
bloated and harder to manage.

Solution Using the Observer Pattern

The Observer Pattern separates the notification logic from the Stock class. Observers (e.g.,
EmailNotifier, SMSNotifier) are independent and dynamically attach/detach themselves
to/from the Stock.

Implementation of Observer Pattern

1. Subject Interface: Defines methods to attach, detach, and notify observers.


2. Observer Interface: Defines the update method that observers implement.
3. Concrete Subject: Maintains a list of observers and notifies them of state changes.
4. Concrete Observers: Implement the update method to react to notifications.

Code Example
java
Copy code
import java.util.ArrayList;
import java.util.List;

// Subject Interface
interface Subject {
void attach(Observer observer);
void detach(Observer observer);
void notifyObservers();
}
// Observer Interface
interface Observer {
void update(double price);
}

// Concrete Subject
class Stock implements Subject {
private String symbol;
private double price;
private List<Observer> observers = new ArrayList<>();

public Stock(String symbol, double price) {


this.symbol = symbol;
this.price = price;
}

public void setPrice(double price) {


this.price = price;
System.out.println(symbol + " price updated to $" + price);
notifyObservers();
}

@Override
public void attach(Observer observer) {
observers.add(observer);
}

@Override
public void detach(Observer observer) {
observers.remove(observer);
}

@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(price);
}
}
}

// Concrete Observers
class EmailNotifier implements Observer {
@Override
public void update(double price) {
System.out.println("Email: Stock price updated to $" + price);
}
}

class SMSNotifier implements Observer {


@Override
public void update(double price) {
System.out.println("SMS: Stock price updated to $" + price);
}
}

class PushNotifier implements Observer {


@Override
public void update(double price) {
System.out.println("Push Notification: Stock price updated to $" +
price);
}
}

// Main
public class Main {
public static void main(String[] args) {
Stock googleStock = new Stock("GOOGL", 1500);

Observer emailNotifier = new EmailNotifier();


Observer smsNotifier = new SMSNotifier();
Observer pushNotifier = new PushNotifier();

// Attach observers
googleStock.attach(emailNotifier);
googleStock.attach(smsNotifier);
googleStock.attach(pushNotifier);

// Update stock price


googleStock.setPrice(1600);
// Output:
// GOOGL price updated to $1600
// Email: Stock price updated to $1600
// SMS: Stock price updated to $1600
// Push Notification: Stock price updated to $1600

// Detach an observer
googleStock.detach(smsNotifier);
googleStock.setPrice(1700);
// Output:
// GOOGL price updated to $1700
// Email: Stock price updated to $1700
// Push Notification: Stock price updated to $1700
}
}

Advantages of Using the Observer Pattern

1. Decoupling:
o The Stock class has no knowledge of specific observers. It just notifies them.
2. Open/Closed Principle:
o Adding a new notification method (e.g., SlackNotifier) doesn’t require modifying the
Stock class.
3. Dynamic Behavior:
o Observers can be added or removed at runtime.
4. Scalability:
o Supports an arbitrary number of observers.

Summary

Without Observer With Observer

Tight coupling between Stock and Loose coupling; Stock only knows the Observer
Without Observer With Observer

notifications interface.

Easy to extend by creating new Observer


Hard to add new notification types
implementations.

Violates Open/Closed Principle Adheres to Open/Closed Principle.

Observer Pattern

 Purpose: Defines a one-to-many dependency between objects so that when one object
changes state, all its dependents are notified.
 Example: Notifications (email, SMS, push) when an event occurs.

Implementation:

java
Copy code
// Subject Interface
public interface Subject {
void attach(Observer observer);
void detach(Observer observer);
void notifyObservers();
}

// Concrete Subject
public class WeatherStation implements Subject {
private List<Observer> observers = new ArrayList<>();
private String weather;

public void setWeather(String weather) {


this.weather = weather;
notifyObservers();
}

@Override
public void attach(Observer observer) {
observers.add(observer);
}

@Override
public void detach(Observer observer) {
observers.remove(observer);
}

@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(weather);
}
}
}

// Observer Interface
public interface Observer {
void update(String weather);
}

// Concrete Observers
public class PhoneDisplay implements Observer {
@Override
public void update(String weather) {
System.out.println("Phone Display: Weather updated to " + weather);
}
}

public class TVDisplay implements Observer {


@Override
public void update(String weather) {
System.out.println("TV Display: Weather updated to " + weather);
}
}

// Main
public class Main {
public static void main(String[] args) {
WeatherStation station = new WeatherStation();
Observer phone = new PhoneDisplay();
Observer tv = new TVDisplay();

station.attach(phone);
station.attach(tv);

station.setWeather("Sunny"); // Notifies both phone and TV


station.setWeather("Rainy"); // Notifies both phone and TV
}
}

Key Points:

 The WeatherStation (Subject) notifies its observers when its state changes.
 Observers (PhoneDisplay, TVDisplay) respond dynamically to changes.

3.3 Command Pattern

 Purpose: Encapsulates a request as an object, thereby allowing parameterization of clients


with queues, requests, and operations.
 Example: Undo/Redo functionality.

Implementation:

java
Copy code
// Command Interface
public interface Command {
void execute();
void undo();
}

// Concrete Commands
public class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}

@Override
public void execute() {
light.turnOn();
}

@Override
public void undo() {
light.turnOff();
}
}

public class LightOffCommand implements Command {


private Light light;

public LightOffCommand(Light light) {


this.light = light;
}

@Override
public void execute() {
light.turnOff();
}

@Override
public void undo() {
light.turnOn();
}
}

// Receiver
public class Light {
public void turnOn() {
System.out.println("Light is ON");
}

public void turnOff() {


System.out.println("Light is OFF");
}
}

// Invoker
public class RemoteControl {
private Command command;

public void setCommand(Command command) {


this.command = command;
}

public void pressButton() {


if (command != null) {
command.execute();
}
}

public void pressUndo() {


if (command != null) {
command.undo();
}
}
}

// Main
public class Main {
public static void main(String[] args) {
Light light = new Light();

Command lightOn = new LightOnCommand(light);


Command lightOff = new LightOffCommand(light);

RemoteControl remote = new RemoteControl();

remote.setCommand(lightOn);
remote.pressButton(); // Light is ON
remote.pressUndo(); // Light is OFF

remote.setCommand(lightOff);
remote.pressButton(); // Light is OFF
remote.pressUndo(); // Light is ON
}
}

Key Points:

 Commands encapsulate requests as objects (LightOnCommand, LightOffCommand).


 The Invoker (RemoteControl) doesn’t know the details of the command or receiver.

What is the Decorator Design Pattern?

The Decorator Pattern is a structural design pattern that allows you to dynamically add new
behavior or responsibilities to an object without modifying its original code or using
inheritance. Instead of changing the object’s base class, you "wrap" the object with decorators
that add new functionality.

Why is it used?

1. Open-Closed Principle: It promotes extending functionality without altering existing code.


2. Dynamic Behavior: You can combine multiple behaviors at runtime by chaining decorators.
3. Avoids Inheritance Overhead: Instead of creating numerous subclasses to handle variations,
you can compose behavior using decorators.
4. Reusability: Each decorator can be used independently or in combination with others.

How it works

1. Start with a base Component interface or abstract class.


2. Implement Concrete Components that provide core functionality.
3. Create a Decorator Class that implements the same interface and wraps a component,
delegating tasks while adding extra behavior.

Real-Life Examples

1. Coffee Shop Example

// Component

interface Coffee {

String getDescription();

double getCost();

// Concrete Component 1: Plain Coffee

class PlainCoffee implements Coffee {

@Override

public String getDescription() {

return "Plain Coffee";

@Override

public double getCost() {

return 50.0;

// Concrete Component 2: Latte


class Latte implements Coffee {

@Override

public String getDescription() {

return "Latte";

@Override

public double getCost() {

return 100.0;

// Concrete Component 3: Espresso

class Espresso implements Coffee {

@Override

public String getDescription() {

return "Espresso";

@Override

public double getCost() {

return 120.0;

}
// Decorator

abstract class CoffeeDecorator implements Coffee {

protected Coffee coffee;

public CoffeeDecorator(Coffee coffee) {

this.coffee = coffee;

@Override

public String getDescription() {

return coffee.getDescription();

@Override

public double getCost() {

return coffee.getCost();

// Concrete Decorator 1: Milk

class Milk extends CoffeeDecorator {

public Milk(Coffee coffee) {

super(coffee);

}
@Override

public String getDescription() {

return coffee.getDescription() + ", Milk";

@Override

public double getCost() {

return coffee.getCost() + 10.0;

// Concrete Decorator 2: Sugar

class Sugar extends CoffeeDecorator {

public Sugar(Coffee coffee) {

super(coffee);

@Override

public String getDescription() {

return coffee.getDescription() + ", Sugar";

@Override

public double getCost() {

return coffee.getCost() + 5.0;


}

// Concrete Decorator 3: Whipped Cream

class WhippedCream extends CoffeeDecorator {

public WhippedCream(Coffee coffee) {

super(coffee);

@Override

public String getDescription() {

return coffee.getDescription() + ", Whipped Cream";

@Override

public double getCost() {

return coffee.getCost() + 20.0;

// Usage

public class CoffeeShop {

public static void main(String[] args) {

// Ordering a Latte with Milk and Sugar

Coffee latte = new Latte();


latte = new Milk(latte);

latte = new Sugar(latte);

System.out.println("Order: " + latte.getDescription());

System.out.println("Total Cost: " + latte.getCost());

// Ordering an Espresso with Whipped Cream

Coffee espresso = new Espresso();

espresso = new WhippedCream(espresso);

System.out.println("Order: " + espresso.getDescription());

System.out.println("Total Cost: " + espresso.getCost());

// Ordering a Plain Coffee with Milk

Coffee plainCoffee = new PlainCoffee();

plainCoffee = new Milk(plainCoffee);

System.out.println("Order: " + plainCoffee.getDescription());

System.out.println("Total Cost: " + plainCoffee.getCost());

Output:
Order: Latte, Milk, Sugar

Total Cost: 115.0

Order: Espresso, Whipped Cream

Total Cost: 140.0

Order: Plain Coffee, Milk

Total Cost: 60.0

Hi, Coffee latte = new Latte(); latte = new Milk(latte); latte = new Sugar(latte);
System.out.println("Order: " + latte.getDescription()); System.out.println("Total Cost: " +
latte.getCost()); In the 2nd line is not it latte will be now the object of the Milk but we are passing
it in sugar that is taking input as coffee ? How this all working ?

ChatGPT said:

ChatGPT

This is a great question, and it goes straight to the core of how polymorphism and
composition work in object-oriented programming! Let’s break it down step by step to see
how latte is updated and remains compatible with the Coffee interface throughout.

Code Context
java
Copy code
Coffee latte = new Latte(); // Step 1
latte = new Milk(latte); // Step 2
latte = new Sugar(latte); // Step 3

Line-by-Line Explanation

Step 1: Coffee latte = new Latte();

Here, you create an object of the class Latte, which implements the Coffee interface:

java
Copy code
class Latte implements Coffee { ... }

 Type: The type of latte is Coffee (the interface), but the actual object is an instance of
Latte.
 Polymorphism: Since Latte implements Coffee, the object latte can be referenced using
the Coffee interface.
 This means you can call methods defined in the Coffee interface (like getDescription()
and getCost()) on the latte object.

Step 2: latte = new Milk(latte);

Here, you wrap the existing latte object with a Milk decorator:

java
Copy code
public class Milk extends CoffeeDecorator { ... }

 The Milk constructor expects an argument of type Coffee:

java
Copy code
public Milk(Coffee coffee) {
super(coffee);
}

 The current latte object (of type Latte, implementing Coffee) is passed into the Milk
constructor:
o super(coffee); assigns the Latte object to the coffee field in
CoffeeDecorator.
o Now, the Milk object wraps the Latte object.

Polymorphism in Action:

 The variable latte now points to a Milk object.


 However, since Milk is a subclass of CoffeeDecorator (which implements Coffee), the
Milk object is still compatible with the Coffee type.

Step 3: latte = new Sugar(latte);

Here, you wrap the Milk object (which already wraps the Latte object) with a Sugar
decorator:

java
Copy code
public class Sugar extends CoffeeDecorator { ... }

 The Sugar constructor also expects an argument of type Coffee:

java
Copy code
public Sugar(Coffee coffee) {
super(coffee);
}
 The latte object (now pointing to a Milk object) is passed into the Sugar constructor.

Why This Works:

 The Milk object is of type Coffee (due to inheritance and polymorphism).


 This allows it to be passed into the Sugar constructor without any issue.

Now, latte points to a Sugar object, which wraps a Milk object, which in turn wraps the
original Latte object.

How the Chain Works

1. Dynamic Composition:
o Each decorator (like Milk and Sugar) wraps another object of type Coffee.
o This chain can go on indefinitely, as long as the object being wrapped implements
Coffee.

2. Polymorphism at Play:
o Each decorator class (Milk, Sugar) implements the Coffee interface. This ensures
that the object remains compatible with Coffee at every step, regardless of the
actual concrete class (like Latte, Milk, or Sugar).

3. Delegation:
o When you call latte.getCost() or latte.getDescription() on the Sugar
object, it:
1. Calls the wrapped Milk object’s getCost() or getDescription()
method.
2. The Milk object, in turn, delegates the call to the wrapped Latte object.
3. The Latte object finally provides the base cost or description.

Key Concept: Interface Compatibility

The decorators (Milk, Sugar) take a Coffee object as input, and they themselves implement
the Coffee interface. This makes them compatible with one another:

 At every step, the latte variable refers to an object that implements the Coffee interface,
even if the actual object changes (e.g., Latte → Milk → Sugar).

Visualization of the Chain

At the end of:

java
Copy code
Coffee latte = new Latte(); // Base object
latte = new Milk(latte); // Wrap with Milk
latte = new Sugar(latte); // Wrap with Sugar

The structure looks like this:

markdown
Copy code
Sugar
└── Milk
└── Latte

Each layer delegates calls to the next layer.

What Happens When You Call latte.getCost()?

1. Sugar.getCost():
o Adds 5.0 to the result of Milk.getCost().

2. Milk.getCost():
o Adds 10.0 to the result of Latte.getCost().

3. Latte.getCost():
o Returns 100.0.

So, the total cost is:

scss
Copy code
100.0 (Latte) + 10.0 (Milk) + 5.0 (Sugar) = 115.0

Summary

 Polymorphism: Every decorator and base coffee implements the Coffee interface, ensuring
compatibility at all levels.
 Dynamic Wrapping: Each decorator dynamically wraps another object implementing the
Coffee interface, creating a chain.
 Delegation: Each decorator delegates method calls (getCost() or getDescription()) to
the wrapped object.

Real-Life Example: Text Editor

Think of a text editor like Microsoft Word:

 Base Component: A simple text.


 Decorators: Add formatting like bold, italics, or underline dynamically.
Where is it commonly used?

1. Java I/O Streams:


o Wrapping streams for additional functionality (e.g., BufferedReader over a
FileReader).

java
Copy code
BufferedReader br = new BufferedReader(new FileReader("file.txt"));

2. GUI Frameworks:
o Adding scrollbars, borders, or colors to UI elements dynamically.

Factory Design Pattern

The Factory Design Pattern is a creational design pattern that provides a way to create
objects without specifying their exact class. Instead of instantiating objects directly using
new, you use a factory class or method that decides which object to create based on certain
inputs or conditions.

Why Use the Factory Pattern?

1. Encapsulation of Object Creation:


o The logic for creating objects is centralized in one place, making code easier
to maintain.
2. Decoupling:
o The client code (code using the objects) doesn't need to know the specific
classes it is working with, only the interface or abstract type.
3. Scalability:
o Adding new object types or classes becomes easier, as you only need to update
the factory without changing the client code.

Real-Life Example: Shape Factory

Imagine you're building a drawing application where users can create different shapes (e.g.,
Circle, Rectangle, Square).

Without a factory, you would write:

java
Copy code
Shape shape = new Circle();
Shape shape = new Rectangle();
Shape shape = new Square();

What if the type of shape is determined dynamically (e.g., based on user input or a
configuration file)? Hardcoding classes would be inconvenient and require changes in
multiple places.

With a factory, you centralize the creation logic.

Implementation: Shape Factory

Step 1: Define a Shape Interface


java
Copy code
public interface Shape {
void draw();
}

Step 2: Create Concrete Implementations


java
Copy code
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("Drawing a Circle.");
}
}

public class Rectangle implements Shape {


@Override
public void draw() {
System.out.println("Drawing a Rectangle.");
}
}

public class Square implements Shape {


@Override
public void draw() {
System.out.println("Drawing a Square.");
}
}

Step 3: Create a Factory Class

The factory will decide which shape to create based on a parameter:

java
Copy code
public class ShapeFactory {
public Shape getShape(String shapeType) {
if (shapeType == null) {
return null;
}
switch (shapeType.toLowerCase()) {
case "circle":
return new Circle();
case "rectangle":
return new Rectangle();
case "square":
return new Square();
default:
throw new IllegalArgumentException("Unknown shape type: " +
shapeType);
}
}
}

Step 4: Use the Factory in Client Code


java
Copy code
public class Main {
public static void main(String[] args) {
ShapeFactory shapeFactory = new ShapeFactory();

// Get Circle object


Shape circle = shapeFactory.getShape("circle");
circle.draw();

// Get Rectangle object


Shape rectangle = shapeFactory.getShape("rectangle");
rectangle.draw();

// Get Square object


Shape square = shapeFactory.getShape("square");
square.draw();
}
}

Output:
css
Copy code
Drawing a Circle.
Drawing a Rectangle.
Drawing a Square.

Where is Factory Used?

1. Java Libraries:
o java.util.Calendar.getInstance()
o java.sql.Connection (retrieved using a factory in JDBC).
2. Frameworks:
o Spring Framework uses factories to create and manage beans.
3. Real-Life Applications:
o Payment gateways (e.g., creating CreditCardPayment, PayPalPayment, etc.).
o Notification systems (e.g., creating SMSNotification, EmailNotification,
etc.).

Factory vs Abstract Factory: A Clear Analogy


Factory Pattern: A Single Factory

Think of a car factory that produces only one type of car, such as electric cars:

 It could create Tesla, Rivian, etc.

java
Copy code
public class CarFactory {
public Car createCar(String type) {
switch (type.toLowerCase()) {
case "tesla": return new Tesla();
case "rivian": return new Rivian();
default: throw new IllegalArgumentException("Unknown car
type.");
}
}
}

Abstract Factory: Factory of Factories

Now, imagine there are multiple types of car factories, such as electric car factories and
gasoline car factories.

 Each car factory produces different types of cars.

The Abstract Factory creates the appropriate factory (e.g., ElectricCarFactory or


GasolineCarFactory), which then creates the specific cars (Tesla, Rivian, Ford, etc.).

java
Copy code
public interface CarFactory {
Car createCar(String type);
}

public class ElectricCarFactory implements CarFactory {


public Car createCar(String type) {
switch (type.toLowerCase()) {
case "tesla": return new Tesla();
case "rivian": return new Rivian();
default: throw new IllegalArgumentException("Unknown electric
car type.");
}
}
}

public class GasolineCarFactory implements CarFactory {


public Car createCar(String type) {
switch (type.toLowerCase()) {
case "ford": return new Ford();
case "chevrolet": return new Chevrolet();
default: throw new IllegalArgumentException("Unknown gasoline
car type.");
}
}
}
// Abstract Factory
public class CarAbstractFactory {
public static CarFactory getCarFactory(String fuelType) {
if (fuelType.equalsIgnoreCase("electric")) {
return new ElectricCarFactory();
} else if (fuelType.equalsIgnoreCase("gasoline")) {
return new GasolineCarFactory();
} else {
throw new IllegalArgumentException("Unknown fuel type.");
}
}
}

Client Code Using Abstract Factory


java
Copy code
public class Main {
public static void main(String[] args) {
// Get a factory for electric cars
CarFactory electricCarFactory =
CarAbstractFactory.getCarFactory("electric");

Car tesla = electricCarFactory.createCar("tesla");


tesla.drive(); // Output: Driving Tesla...

// Get a factory for gasoline cars


CarFactory gasolineCarFactory =
CarAbstractFactory.getCarFactory("gasoline");

Car ford = gasolineCarFactory.createCar("ford");


ford.drive(); // Output: Driving Ford...
}
}

Why is it Called Factory of Factories?

1. The Abstract Factory doesn’t directly create objects (e.g., Tesla or Ford).
Instead, it creates factories (e.g., ElectricCarFactory or GasolineCarFactory).
2. Those factories, in turn, create the actual objects (e.g., Tesla, Ford).

Visual Representation

Here’s a breakdown of how it works:

 Abstract Factory → Concrete Factories → Concrete Products

Example:

1. CarAbstractFactory → Returns ElectricCarFactory or GasolineCarFactory.


2. ElectricCarFactory → Creates Tesla or Rivian.
3. GasolineCarFactory → Creates Ford or Chevrolet.

Real-Life Applications

1. UI Toolkit (as explained earlier):


o Abstract Factory: UIFactory.
o Concrete Factories: WindowsFactory, MacOSFactory.
o Products: WindowsButton, MacOSButton.

2. Database Connections:
o Abstract Factory: DatabaseFactory.
o Concrete Factories: MySQLFactory, PostgreSQLFactory.
o Products: Database connections, queries, etc.

Beginner Level (Basic OOP Concepts)

1. Design a Library System (IS-A & HAS-A)


o A Book IS-A Item.
o A Library HAS-A collection of Books.
2. Design a Bank Account System (Inheritance)
o SavingAccount and CurrentAccount inherit from Account.
3. Design a Car Rental System (Composition)
o Car HAS-A Engine and Owner.
4. Design a Shopping Cart (Aggregation)
o Cart HAS-A list of Items.

Intermediate Level (Real-World Scenarios)

5. Design an Employee Management System


o Manager and Developer inherit from Employee.
o Employee HAS-A Address.
6. Design an Online Movie Booking System
o Customer HAS-A Ticket.
o Movie HAS-A multiple Shows.
7. Design an ATM System
o ATM HAS-A CashDispenser.
o BankAccount IS-A Account.
8. Design a Food Ordering System
o Order HAS-A multiple FoodItems.
o Customer places Order.
Advanced Level (More Complexity)

9. Design a Chess Game


o Player HAS-A ChessPiece.
o Pawn, Knight, Bishop inherit from Piece.
10. Design a Ride-Sharing System (e.g., Uber)

 Driver and Passenger inherit from User.


 Ride HAS-A Vehicle.

You might also like

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy