Hello! Today, we'll venture into the realm of design patterns. Specifically, we'll tackle exercises that apply a single design pattern to problem-solving. Mastering these patterns is a surefire way to extend your coding skills.
Our goal today is to fortify your understanding of when and how to apply specific Object-Oriented Programming (OOP) design patterns. These patterns include Encapsulation
, Abstraction
, Polymorphism
, and Composition
.
We'll dissect four real-life scenarios and distinguish which pattern is applicable and why.
Let's get underway!
The Encapsulation
pattern proves beneficial for the development of a Database Management System (DBMS). Each DBMS table represents a class, the fields represent private data members, and the functions operating on this data serve as methods.
Encapsulation
ensures that data members are accessed through methods that promote data integrity and prevent inadvertent anomalies. Here's a mini-code snippet to support this concept:
TypeScript1class Employees { 2 private employees: { [key: number]: string } = {}; // private data member with type annotation 3 4 addEmployee(eid: number, name: string): void { // method to operate on private data 5 this.employees[eid] = name; 6 } 7 8 updateEmployee(eid: number, newName: string): void { // method to operate on private data 9 if (this.employees.hasOwnProperty(eid)) { 10 this.employees[eid] = newName; 11 } 12 } 13} 14 15const employees = new Employees(); 16employees.addEmployee(1, "John"); 17employees.addEmployee(2, "Mark"); 18 19employees.updateEmployee(2, "Jake");
In this context, Encapsulation
restricts direct access to employee data, presenting a protective layer via designated methods.
When transitioning to GUI development, consider the creation of controls like buttons or checkboxes. Despite belonging to the same class, each responds differently when clicked. This situation illustrates Polymorphism
, which allows us to handle different objects uniformly via a common interface.
Check out this illustrative example:
TypeScript1abstract class Control { // common base class 2 abstract click(): void; // abstract method to be overridden 3} 4 5class Button extends Control { // derived class 6 click(): void { 7 console.log("Button Clicked!"); // overridden method 8 } 9} 10 11class CheckBox extends Control { // derived class 12 click(): void { 13 console.log("CheckBox Clicked!"); // overridden method 14 } 15} 16 17// Create objects 18const b = new Button(); 19const c = new CheckBox(); 20 21// Click Controls 22b.click(); // Outputs: Button Clicked! 23c.click(); // Outputs: CheckBox Clicked!
Despite sharing the common click
interface, different controls display unique responses. This characteristic demonstrates Polymorphism
.
Let's explore the Composition
design pattern through a TypeScript approach to create a simple web page structure. Here, we'll build a fundamental structure representing a webpage composed of various elements like headers, paragraphs, and lists.
TypeScript1abstract class WebPageElement { 2 abstract render(): string; // Return type annotation 3} 4 5class Header extends WebPageElement { 6 constructor(private text: string) { // Type annotation for parameter 7 super(); 8 } 9 10 render(): string { 11 return `<h1>${this.text}</h1>`; 12 } 13} 14 15class Paragraph extends WebPageElement { 16 constructor(private text: string) { // Type annotation for parameter 17 super(); 18 } 19 20 render(): string { 21 return `<p>${this.text}</p>`; 22 } 23} 24 25class List extends WebPageElement { 26 constructor(private items: string[]) { // Type annotation for parameter 27 super(); 28 } 29 30 render(): string { 31 const itemsStr = this.items.map(item => `<li>${item}</li>`).join("\n"); 32 return `<ul>${itemsStr}</ul>`; 33 } 34} 35 36class WebPage { 37 private elements: WebPageElement[] = []; // Type annotation for array 38 39 constructor(private title: string) { // Type annotation for parameter 40 } 41 42 addElement(element: WebPageElement): void { // Type annotation for parameter 43 this.elements.push(element); 44 } 45 46 display(): string { // Return type annotation 47 const elementsStr = this.elements.map(element => element.render()).join("\n"); 48 return `<html>\n<head>\n <title>${this.title}</title>\n</head>\n<body>\n ${elementsStr}\n</body>\n</html>`; 49 } 50} 51 52// Example usage 53const page = new WebPage("My Web Page"); 54page.addElement(new Header("Welcome to My Web Page")); 55page.addElement(new Paragraph("This is a paragraph of text on the web page.")); 56page.addElement(new List(["Item 1", "Item 2", "Item 3"])); 57 58console.log(page.display());
In this code, we've designed a web page structure using the Composition
design pattern. Each web page element (Header
, Paragraph
, and List
) is a WebPageElement
, allowing for unified handling while maintaining their specific behaviors (rendering as HTML elements).
The WebPage
class acts as a composite object that can contain an arbitrary number of WebPageElement
instances, each representing different parts of a web page. By adding these elements to the WebPage
and invoking the display
method, we dynamically compose a complete web page structure in HTML format.
Consider creating a Vehicle
class in TypeScript. Here, Abstraction
comes into play. It allows you to expose only the necessary functionality and abstract away the internal workings of the Vehicle
.
Let's see this in code:
TypeScript1abstract class Vehicle { 2 protected engineRunning: boolean = false; // Property with type annotation 3 4 constructor(private color: string, private engineType: string) { // Type annotations for parameters 5 } 6 7 abstract startEngine(): void; // Abstract method with return type 8 abstract stopEngine(): void; // Abstract method with return type 9 abstract drive(): void; // Abstract method with return type 10} 11 12class Car extends Vehicle { 13 startEngine(): void { 14 this.engineRunning = true; 15 console.log("Car engine started!"); 16 } 17 18 stopEngine(): void { 19 this.engineRunning = false; 20 console.log("Car engine stopped!"); 21 } 22 23 drive(): void { 24 if (this.engineRunning) { 25 console.log("Car is driving!"); 26 } else { 27 console.log("Start the engine first!"); 28 } 29 } 30} 31 32// Example usage 33const car = new Car("red", "gasoline"); 34car.startEngine(); 35car.drive();
Here, the Vehicle
class defines the structure and necessary methods such as startEngine()
, stopEngine()
, and drive()
, while throwing errors if they are not overridden. The Car
class provides the concrete implementations of these methods. However, it hides or abstracts away internal state management (engineRunning
). This is a basic instance of Abstraction
, which simplifies the interaction with the class and hides underlying complexity.
Let's recap the major OOP patterns:
Encapsulation
: This pattern confines data and related methods into one unit, veiling direct data access.Abstraction
: This pattern offers a simplified interface, cloaking complexity.Polymorphism
: This pattern facilitates treating different objects as related objects of a common superclass.Composition
: This pattern builds elaborate systems by composing closely related objects.
Reflect on these principles and practice applying them to a variety of scenarios to better recognize suitable patterns.
Great job! You've poked and prodded at the practical applications of OOP design patterns. We've explored the use of Encapsulation
in Database Management Systems, the pivotal role of Polymorphism
in GUI development, the important role of Composition
when designing a web page builder, and how Abstraction
helps to build a vehicle structure.
Next up are hands-on exercises to reinforce these concepts. Remember, practice is the master key to understanding these concepts. So keep coding!