Applying the Open-Closed Principle and Law of Demeter in Object-Oriented Design
Open-Closed Principle
Core Concept
- The Open-Closed Principle (OCP) is a foundational tenet of maintainable object-oriented design.
- Software components—classes, modules, or functions—should be open for extension but closed for modification. Bheavior changes should occur by adding new code, not altering existing logic.
- Abstractions define stable contracts; concrete implementations supply variability. This decouples clients from volatile details.
- Most design patterns and SOLID-aligned practices aim to uphold OCP by anabling evolution without destabilizing existing functionality.
Problematic Implementation
public class ShapeRenderer {
public static void main(String[] args) {
Renderer engine = new Renderer();
engine.render(new Box());
engine.render(new Oval());
engine.render(new Polygon());
}
}
class Renderer {
void render(Shape shape) {
if (shape.kind == Shape.KIND_BOX) {
drawBox(shape);
} else if (shape.kind == Shape.KIND_OVAL) {
drawOval(shape);
} else if (shape.kind == Shape.KIND_POLYGON) {
drawPolygon(shape);
}
}
void drawBox(Shape s) { System.out.println("Rendering box"); }
void drawOval(Shape s) { System.out.println("Rendering oval"); }
void drawPolygon(Shape s) { System.out.println("Rendering polygon"); }
}
class Shape {
static final int KIND_BOX = 1;
static final int KIND_OVAL = 2;
static final int KIND_POLYGON = 3;
int kind;
}
class Box extends Shape {
Box() { this.kind = KIND_BOX; }
}
class Oval extends Shape {
Oval() { this.kind = KIND_OVAL; }
}
class Polygon extends Shape {
Polygon() { this.kind = KIND_POLYGON; }
}
This version violates OCP: adding a new shape (e.g., Star) forces edits to Renderer.render()—a fragile, error-prone change.
Refactored with OCP Compliance
public class ShapeRenderer {
public static void main(String[] args) {
Renderer engine = new Renderer();
engine.render(new Box());
engine.render(new Oval());
engine.render(new Polygon());
engine.render(new Star()); // No changes to Renderer needed
}
}
class Renderer {
void render(Shape shape) {
shape.display(); // Delegated behavior
}
}
abstract class Shape {
abstract void display();
}
class Box extends Shape {
@Override
void display() { System.out.println("Rendering box"); }
}
class Oval extends Shape {
@Override
void display() { System.out.println("Rendering oval"); }
}
class Polygon extends Shape {
@Override
void display() { System.out.println("Rendering polygon"); }
}
class Star extends Shape {
@Override
void display() { System.out.println("Rendering star"); }
}
Now Renderer depends only on the Shape abstraction. New shapes integrate seamlessly via inheritance and method override—no modifications required.
Law of Demeter
Core Concept
- A unit (e.g., class or method) should interact only with its immediate collaborators.
- Overly tight coupling increases ripple effects during change and reduces testability.
- Also known as the Principle of Least Knowledge, it mandates that an object must not reach into the internals of another object. Encapsulate complexity behind clean interfaces.
- A concise phrasing: "Only talk to your direct friends."
- Direct friends include classes appearing as:
- Fields (instance variables),
- Method parameters,
- Return types. Classes instantiated or referenced only within method bodies (local variables) are not direct friends—and accessing their internals violates the law.
Practical Implication
The Law of Demeter prioritizes loose coupling—not zero coupling. It discourages chained navigation, such as a.getB().getC().doSomething(), which exposes internal structure and creates brittle dependencies. Instead, delegate responsibility: let a expose a method like a.doSomethingOnC() that internally orchestrates the call chain.