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 properties, and the functions operating on this data serve as methods.
Encapsulation
ensures that data properties are accessed through methods that promote data integrity and prevent inadvertent anomalies. Here's a mini-code snippet to support this concept:
Kotlin1class Employees { 2 private val employees = mutableMapOf<Int, String>() // private data property 3 4 fun addEmployee(eid: Int, name: String) { // method to operate on private data 5 employees[eid] = name 6 } 7 8 fun updateEmployee(eid: Int, newName: String) { // method to operate on private data 9 if (employees.containsKey(eid)) { 10 employees[eid] = newName 11 } 12 } 13 14 fun getEmployee(eid: Int): String? { // getter method for private data 15 return employees[eid] 16 } 17} 18 19fun main() { 20 val employees = Employees() 21 employees.addEmployee(1, "John") 22 employees.addEmployee(2, "Mark") 23 24 employees.updateEmployee(2, "Jake") 25 26 println(employees.getEmployee(1)) // Outputs: John 27 println(employees.getEmployee(2)) // Outputs: Jake 28}
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 parent 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:
Kotlin1open class Control { 2 open fun click() { 3 // method that can be overridden 4 } 5} 6 7class Button : Control() { 8 override fun click() { 9 println("Button Clicked!") // overridden method 10 } 11} 12 13class CheckBox : Control() { 14 override fun click() { 15 println("CheckBox Clicked!") // overridden method 16 } 17} 18 19fun main() { 20 val b: Control = Button() 21 val c: Control = CheckBox() 22 23 // Click Controls 24 b.click() // Outputs: Button Clicked! 25 c.click() // Outputs: CheckBox Clicked! 26}
Despite sharing the common click
interface, different controls display unique responses. This characteristic demonstrates Polymorphism
.
Let's explore the Composition
design pattern through a Kotlin 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. This abstraction allows us to understand how composite objects work together to form a larger system.
Kotlin1interface WebPageElement { 2 fun render(): String 3} 4 5class Header(private val text: String) : WebPageElement { 6 override fun render(): String = "<h1>$text</h1>" 7} 8 9class Paragraph(private val text: String) : WebPageElement { 10 override fun render(): String = "<p>$text</p>" 11} 12 13class ListElement(private val items: List<String>) : WebPageElement { 14 override fun render(): String { 15 return items.joinToString(prefix = "<ul>", postfix = "</ul>") { "<li>$it</li>" } 16 } 17} 18 19class WebPage(private val title: String) { 20 private val elements = mutableListOf<WebPageElement>() 21 22 fun addElement(element: WebPageElement) { 23 elements.add(element) 24 } 25 26 fun display(): String { 27 val elementsStr = elements.joinToString("\n") { it.render() } 28 return """ 29 <html> 30 <head> 31 <title>$title</title> 32 </head> 33 <body> 34 $elementsStr 35 </body> 36 </html> 37 """.trimIndent() 38 } 39} 40 41fun main() { 42 val page = WebPage("My Web Page") 43 page.addElement(Header("Welcome to My Web Page")) 44 page.addElement(Paragraph("This is a paragraph of text on the web page.")) 45 val items = listOf("Item 1", "Item 2", "Item 3") 46 page.addElement(ListElement(items)) 47 48 println(page.display()) 49 /* 50 Outputs: 51 <html> 52 <head> 53 <title>My Web Page 54 </title> 55 </head> 56 <body> 57 <h1>Welcome to My Web Page</h1> 58 <p>This is a paragraph of text on the web page.</p> 59 <ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul> 60 </body> 61 </html> 62 */ 63}
In this code, we've designed a web page structure using the Composition
design pattern. Each web page element (Header
, Paragraph
, and ListElement
) 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 Kotlin. Here, Abstraction comes into play. You expose only the necessary functionality and abstract away the internal workings of the Vehicle
.
Let's see this in code:
Kotlin1abstract class Vehicle(protected val color: String, protected val engineType: String) { 2 protected var engineRunning: Boolean = false 3 4 abstract fun startEngine() 5 abstract fun stopEngine() 6 abstract fun drive() 7} 8 9class Car(color: String, engineType: String) : Vehicle(color, engineType) { 10 override fun startEngine() { 11 engineRunning = true 12 println("Car engine started!") 13 } 14 15 override fun stopEngine() { 16 engineRunning = false 17 println("Car engine stopped!") 18 } 19 20 override fun drive() { 21 if (engineRunning) { 22 println("$color car is driving on the $engineType engine type!") 23 } else { 24 println("Start the engine first!") 25 } 26 } 27} 28 29fun main() { 30 val car = Car("red", "gasoline") 31 car.startEngine() 32 car.drive() 33 /* 34 Output: 35 Car engine started! 36 Red car is driving on the gasoline engine type! 37 */ 38}
The Vehicle
abstract class exposes relevant and necessary functions such as startEngine()
, stopEngine()
, and drive()
, while the Car
class implements this abstract class and provides concrete implementations, abstracting away internal state management (engineRunning
). This is a basic instance of Abstraction, which simplifies the interaction with the class while hiding 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 importance 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!