Lesson 6
Applying Object-Oriented Programming Principles in TypeScript
Introduction and Lesson Goal

Today's mission involves using multiple Object-Oriented Programming (OOP) principles to tackle complex tasks in TypeScript. When principles like Encapsulation, Abstraction, Polymorphism, and Composition are integrated, the resulting code becomes streamlined and easier to manage.

Our goal is to dissect two real-world examples, gaining insights into how these principles can seamlessly orchestrate solutions.

Real-life Example 1: Building an Online Library System

Let's design an online library system to reinforce our understanding of Encapsulation and Polymorphism. Encapsulation will help us protect the attributes of books, members, and transactions, ensuring they are accessible in a controlled manner using TypeScript's features. Polymorphism will demonstrate its power by enabling a single interface to represent different underlying forms, such as digital and print versions of books.

TypeScript
1// Base class for different types of library users 2class Member { 3 private name: string; 4 5 constructor(name: string) { 6 this.name = name; 7 } 8 9 checkOutBook(book: Book): void { 10 console.log(`${this.name} checked out ${book.getBookType()} book ${book.getTitle()}.`); 11 } 12} 13 14// Base class for different types of books 15abstract class Book { 16 protected title: string; 17 18 constructor(title: string) { 19 this.title = title; 20 } 21 22 abstract getBookType(): string; 23} 24 25// Inherits from Book, represents a digital book 26class DigitalBook extends Book { 27 getBookType(): string { 28 return "Digital"; 29 } 30} 31 32// Inherits from Book, represents a physical book 33class PhysicalBook extends Book { 34 getBookType(): string { 35 return "Physical"; 36 } 37} 38 39// Library class that manages members and books 40class Library { 41 private members: Member[] = []; 42 private books: Book[] = []; 43 44 addMember(member: Member): void { 45 this.members.push(member); 46 } 47 48 addBook(book: Book): void { 49 this.books.push(book); 50 } 51} 52 53const myLibrary = new Library(); 54 55const alice = new Member("Alice"); 56const bob = new Member("Bob"); 57 58myLibrary.addMember(alice); 59myLibrary.addMember(bob); 60 61const digitalBook = new DigitalBook("The TypeScript Handbook"); 62const physicalBook = new PhysicalBook("Learning TypeScript Design Patterns"); 63 64myLibrary.addBook(digitalBook); 65myLibrary.addBook(physicalBook); 66 67alice.checkOutBook(digitalBook); // Prints: Alice checked out Digital book The TypeScript Handbook. 68bob.checkOutBook(physicalBook); // Prints: Bob checked out Physical book Learning TypeScript Design Patterns.

In this code snippet, Encapsulation is clearly demonstrated through the class structures and the controlled access to their attributes using TypeScript's private keyword. Polymorphism is vividly illustrated by how both DigitalBook and PhysicalBook classes inherit from the Book class but provide their own implementations of the getBookType method. This setup allows objects of DigitalBook and PhysicalBook to be used interchangeably when a book's type needs to be identified, demonstrating polymorphism's capability to work with objects of different classes through a common interface.

  • Encapsulation ensures that details about members and books are well-contained within their respective classes.
  • Polymorphism showcases flexibility by treating different book types uniformly, making the system more adaptive and scalable.
Real-life Example 2: Building a Shape Drawing Application

Next, we'll develop a shape-drawing application capable of drawing various shapes. For this, we'll employ the principles of Abstraction and Composition.

  • Abstraction simplifies the complexity associated with drawing different shapes.
  • Composition takes care of composite shapes.

Here's how we translate these principles into our shape-drawing application:

TypeScript
1// Define the basic Shape class 2abstract class Shape { 3 // Abstract method that will be implemented in each subclass 4 abstract draw(): void; 5} 6 7// Define the Circle class 8class Circle extends Shape { 9 // Implement the draw method for circle 10 draw(): void { 11 console.log("Drawing a circle."); 12 } 13} 14 15// Define the Square class 16class Square extends Shape { 17 // Implement the draw method for square 18 draw(): void { 19 console.log("Drawing a square."); 20 } 21} 22 23// Define the ShapeComposite class 24class ShapeComposite extends Shape { 25 private shapes: Shape[] = []; 26 27 // Add a new shape to the composite 28 addShape(shape: Shape): void { 29 this.shapes.push(shape); 30 } 31 32 // Implement the draw method to draw each shape in composite 33 draw(): void { 34 for (const shape of this.shapes) { 35 shape.draw(); 36 } 37 } 38} 39 40const circle = new Circle(); 41const square = new Square(); 42 43// Drawing individual shapes 44circle.draw(); // Output: Drawing a circle. 45square.draw(); // Output: Drawing a square. 46 47// Create a ShapeComposite instance for composite shapes 48const compositeShape = new ShapeComposite(); 49 50// Add individual shapes to the composite 51compositeShape.addShape(circle); 52compositeShape.addShape(square); 53 54// Drawing the composite shape 55compositeShape.draw(); 56/* 57Output: 58Drawing a circle. 59Drawing a square. 60*/

This example unveils how Abstraction streamlines the process of drawing different shapes, and Composition handles complex shapes.

Lesson Summary and Practice

Well done! You combined multiple OOP principles to respond to complex tasks. By dissecting real-world examples, you understood how these principles found their applications in TypeScript. Now, it's time to put this knowledge to work. Practice fortifies concepts, transforming knowledge into expertise. So, let's get coding!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.