OCP and LSP
Chapter 8: Open-Closed Principle (OCP)
Key Concept:
Software entities (classes, modules, etc.) should be open for extension but closed for modification. New functionality should be added by extending existing code, not modifying it.
1. Problem: Violating OCP
Imagine a system that generates reports in HTML. Adding a PDF report forces changes to existing code.
Violation Example:
#include <iostream>
#include <string>
class ReportGenerator {
public:
void generateHTML(const std::string& data) {
std::cout << "Generating HTML: " << data << std::endl;
}
};
class ReportController {
public:
void createReport(ReportGenerator& generator, const std::string& data) {
generator.generateHTML(data); // Hardcoded to HTML
}
};
int main() {
ReportGenerator generator;
ReportController controller;
controller.createReport(generator, "Sales Data");
return 0;
}
Output:
Generating HTML: Sales Data
Issue: Adding PDF support requires modifying ReportController
.
2. Solution: Adhering to OCP
Use abstractions (interfaces) to decouple high-level logic from low-level details.
Refactored Code:
#include <iostream>
#include <string>
#include <memory>
// Abstract interface
class ReportGenerator {
public:
virtual ~ReportGenerator() = default;
virtual void generate(const std::string& data) = 0;
};
// Concrete implementations
class HTMLGenerator : public ReportGenerator {
public:
void generate(const std::string& data) override {
std::cout << "Generating HTML: " << data << std::endl;
}
};
class PDFGenerator : public ReportGenerator {
public:
void generate(const std::string& data) override {
std::cout << "Generating PDF: " << data << std::endl;
}
};
// High-level controller depends on abstraction
class ReportController {
public:
void createReport(ReportGenerator& generator, const std::string& data) {
generator.generate(data);
}
};
int main() {
HTMLGenerator html;
PDFGenerator pdf;
ReportController controller;
controller.createReport(html, "Sales Data"); // HTML
controller.createReport(pdf, "Sales Data"); // PDF (no code change!)
return 0;
}
Output:
Generating HTML: Sales Data
Generating PDF: Sales Data
Key Points:
ReportController
depends onReportGenerator
(abstraction).- New formats (e.g., PDF) are added by extending
ReportGenerator
.
Chapter 9: Liskov Substitution Principle (LSP)
Key Concept:
Subtypes must be substitutable for their base types without altering program correctness.
1. Problem: Violating LSP
A Square
subclassing Rectangle
breaks behavior when dimensions change.
Violation Example:
#include <iostream>
#include <cassert>
class Rectangle {
protected:
int width, height;
public:
Rectangle(int w, int h) : width(w), height(h) {}
virtual void setWidth(int w) { width = w; }
virtual void setHeight(int h) { height = h; }
int area() const { return width * height; }
};
class Square : public Rectangle {
public:
Square(int size) : Rectangle(size, size) {}
void setWidth(int w) override {
width = height = w; // Forced to keep square
}
void setHeight(int h) override {
width = height = h;
}
};
void testArea(Rectangle& rect) {
rect.setWidth(5);
rect.setHeight(2);
assert(rect.area() == 10); // Fails for Square!
}
int main() {
Rectangle rect(5, 2);
Square square(5);
testArea(rect); // Passes
// testArea(square); // Fails assertion (violates LSP)
return 0;
}
Issue: Square
alters expected behavior of Rectangle
.
2. Solution: Adhering to LSP
Avoid inheritance hierarchies where subtypes change base behavior. Use composition or interfaces.
Refactored Code:
#include <iostream>
#include <memory>
// Shape interface
class Shape {
public:
virtual ~Shape() = default;
virtual int area() const = 0;
};
// Concrete implementations
class Rectangle : public Shape {
int width, height;
public:
Rectangle(int w, int h) : width(w), height(h) {}
void setWidth(int w) { width = w; }
void setHeight(int h) { height = h; }
int area() const override { return width * height; }
};
class Square : public Shape {
int size;
public:
Square(int s) : size(s) {}
void setSize(int s) { size = s; }
int area() const override { return size * size; }
};
void printArea(const Shape& shape) {
std::cout << "Area: " << shape.area() << std::endl;
}
int main() {
Rectangle rect(5, 2);
Square square(5);
printArea(rect); // Area: 10
printArea(square); // Area: 25
return 0;
}
Output:
Area: 10
Area: 25
Key Points:
Rectangle
andSquare
implementShape
without inheritance.- No unexpected side effects when substituting types.
Summary
- OCP: Depend on abstractions to avoid modifying existing code.
- LSP: Ensure substitutability by preserving behavioral contracts.
- Use interfaces and composition to decouple components.
Chapter 8 & 9 Focus Areas:
- Open-Closed Principle (OCP): Architectural strategies for extension vs. modification
- Liskov Substitution Principle (LSP): Interface contracts and substitution validity
- Dependency inversion patterns
- Component hierarchy design
- Interface segregation techniques
Multiple-Choice Questions on OCP and LSP
Question 1
Which of the following code snippets violate the Open-Closed Principle (OCP)?
// Option A
interface Shape { double area(); }
class Circle implements Shape { /*...*/ }
class Square implements Shape { /*...*/ }
// Option B
class ReportGenerator {
void generatePDF(Data data) { /*...*/ }
void generateCSV(Data data) { /*...*/ } // Added later
}
// Option C
interface Report { void generate(Data data); }
class PDFReport implements Report { /*...*/ }
class CSVReport implements Report { /*...*/ }
// Option D
class PaymentProcessor {
void process(Payment payment) {
if (payment.type == "CreditCard") { /*...*/ }
else if (payment.type == "PayPal") { /*...*/ }
}
}
Question 2
Which scenarios describe a valid application of the Liskov Substitution Principle (LSP)?
// Option A: Square extends Rectangle
class Rectangle {
void setWidth(int w) { /*...*/ }
void setHeight(int h) { /*...*/ }
}
class Square extends Rectangle {
void setWidth(int w) { setHeight(w); }
}
// Option B: Ostrich extends Bird (Bird has fly())
class Ostrich extends Bird {
void fly() { throw new UnsupportedOperationException(); }
}
// Option C: ReadOnlyList implements List
class ReadOnlyList implements List {
void add(Object item) { throw new UnsupportedException(); }
}
// Option D: ElectricCar extends Car
class Car { void refuel() { /*...*/ } }
class ElectricCar extends Car { void refuel() { recharge(); } }
Question 3
Which design patterns help enforce the OCP?
- Strategy Pattern
- Singleton Pattern
- Template Method Pattern
- Facade Pattern
Question 4
Which code snippets preserve LSP when substituting a subclass for its superclass?
// Option A
class Animal { void speak() { /* default silence */ } }
class Dog extends Animal { void speak() { bark(); } }
// Option B
class Stack extends ArrayList {
void push(Object item) { add(item); }
Object pop() { return remove(size()-1); }
}
// Option C
class ImmutableList extends List {
void add(Object item) { throw new UnsupportedException(); }
}
// Option D
class Bird { void fly() { /*...*/ } }
class Penguin extends Bird { void fly() { /* do nothing */ } }
Question 5
How does violating LSP impact architectural boundaries?
- Increases modularity
- Introduces unexpected runtime errors
- Forces changes across multiple layers
- Reduces test coverage
Question 6
Which principles are directly related to OCP?
- Dependency Inversion Principle
- Interface Segregation Principle
- Single Responsibility Principle
- Composite Reuse Principle
Question 7
Which code changes adhere to OCP?
// Original
class Logger {
void logToFile(String msg) { /*...*/ }
}
// Option A: Add a new method logToDatabase()
class Logger {
void logToFile(String msg) { /*...*/ }
void logToDatabase(String msg) { /*...*/ }
}
// Option B: Extract an interface
interface Logger { void log(String msg); }
class FileLogger implements Logger { /*...*/ }
class DatabaseLogger implements Logger { /*...*/ }
// Option C: Modify logToFile() to support encryption
class Logger {
void logToFile(String msg, boolean encrypt) { /*...*/ }
}
Question 8
Which statements about LSP violations are true?
- They always cause compile-time errors.
- They can lead to broken preconditions/postconditions.
- They are acceptable if the subclass is rarely used.
- They complicate polymorphism.
Question 9
Which code uses abstractions to enforce OCP?
// Option A
class PaymentProcessor {
void process(PaymentStrategy strategy) { strategy.execute(); }
}
// Option B
class ReportService {
private PDFGenerator pdfGenerator = new PDFGenerator();
void generate() { pdfGenerator.generate(); }
}
// Option C
class DataExporter {
void export(Format format) {
if (format == Format.CSV) { /*...*/ }
else if (format == Format.XML) { /*...*/ }
}
}
Question 10
Which examples preserve LSP?
// Option A: Covariant return types
class Super { Animal getPet() { return new Animal(); } }
class Sub extends Super { Dog getPet() { return new Dog(); } }
// Option B: Strengthened preconditions
class Super { void save(int value) { /*...*/ } }
class Sub extends Super { void save(int value) {
if (value < 0) throw new Exception();
super.save(value);
}}
// Option C: Weakened postconditions
class Super { List<String> getItems() { return unmodifiableList; } }
class Sub extends Super { List<String> getItems() { return mutableList; } }
// Option D: No exception throwing in subclass
class Super { void load() throws IOException { /*...*/ } }
class Sub extends Super { void load() { /*...*/ } }
Answers and Explanations
B, D
- B violates OCP by modifying
ReportGenerator
to addgenerateCSV()
. - D violates OCP by using conditional logic for payment types.
- B violates OCP by modifying
A, D
- A:
Square
violates LSP ifsetWidth()
changes height (breaks invariants). - D:
ElectricCar
safely substitutesCar
ifrecharge()
is equivalent torefuel()
.
- A:
1, 3
- Strategy and Template Method patterns allow extending behavior without modifying existing code.
A
- A:
Dog
strengthensAnimal
without violating contracts. B/C/D violate LSP by breaking superclass invariants.
- A:
2, 3
- LSP violations propagate errors across layers and force cascading changes.
1, 2
- Dependency Inversion and Interface Segregation directly support OCP.
B
- Extracting an interface (
Logger
) allows new implementations without modifying existing code.
- Extracting an interface (
2, 4
- LSP violations break contracts and complicate polymorphic behavior.
A
PaymentProcessor
depends on thePaymentStrategy
abstraction.
A, D
- A: Covariant return types are allowed. D: Subclass removes exception (weaker postcondition).
Test Cases (Java)
LSP Test for ElectricCar
:
public class Main {
public static void main(String[] args) {
Car car = new ElectricCar();
car.refuel(); // Should call recharge()
}
}
OCP Test for Logger
:
public class Main {
public static void main(String[] args) {
Logger logger = new DatabaseLogger();
logger.log("test"); // No changes to existing code
}
}