Summary of items you need to know (at this point) regarding Polymorphism:
Class Inheritance
We can sub class from superclasses:
When we do that: All of the Super Classes data attributes and methods are available in the Sub Class.
In terms of data field subclasses: there is nothing to talk about!
In terms of METHOD subclasses:
We must address the considerations of: Polymorphism.
There are 2 formats of Poly Morphism:
Method Over Loading : Method name does not change: Method signature DOES change in method overloading: we can change the number of and type input parameters to the method. Method Over Riding : Change what the method is doing : the algorithm, what the method is doing. Study our examples until you develop an intuition on when it is the right time to use Overloading or Overriding. (In practice your UML diagram will tell you where to go with this.).
In Monday's session, all topics from the plan were covered:
What is inheritance?
Why is inheritance needed?
How is inheritance implemented?
Adding properties to a child class
Adding properties to a parent class
Passing initial property values to parent using super() in the child class constructor
Here is some Object Oriented Programming Theory Concepts to assist us in navigating today’s Labs.
Today → Class Plan for Tuesday October 29:
Quiz 5 will be posted at 8 am WEDNESDAY. It will be due at 10:00 pm
Today’s Topics:
Polymorphism
Exception Handling
Interfaces
This lab workbook provides a comprehensive introduction to polymorphism in Kotlin, starting with basic concepts and building up to complex real-world examples.
The e-commerce system example demonstrates both types of polymorphism in a practical context that students can relate to.
Lab Exercise: Understanding Polymorphism in Kotlin
Introduction to polymorphism using a horticultural example.
```kotlin
// Botanical Gardens Management System
// Demonstrating how polymorphism naturally models real-world hierarchies
// Base class representing fundamental plant characteristics
open class Plant(
val scientificName: String,
val commonName: String,
protected var heightInCm: Double,
protected var healthStatus: String
) {
open fun water() {
println("Basic watering procedure for $commonName")
}
open fun prune() {
println("Basic pruning procedure for $commonName")
}
open fun fertilize() {
println("Basic fertilizing procedure for $commonName")
}
open fun displayCareInstructions() {
println("Basic care instructions for $commonName ($scientificName)")
}
}
// Specialized class for tropical plants
class TropicalPlant(
scientificName: String,
commonName: String,
heightInCm: Double,
healthStatus: String,
private val humidityRequired: Int,
private val minimumTemp: Double
) : Plant(scientificName, commonName, heightInCm, healthStatus) {
override fun water() {
println("Misting $commonName to maintain $humidityRequired% humidity")
println("Ensuring soil remains consistently moist but not waterlogged")
}
override fun displayCareInstructions() {
println("=== Tropical Plant Care: $commonName ===")
println("Scientific Name: $scientificName")
println("Required Humidity: $humidityRequired%")
println("Minimum Temperature: $minimumTemp°C")
println("Needs regular misting and humid conditions")
}
}
// Specialized class for desert plants
class DesertPlant(
scientificName: String,
commonName: String,
heightInCm: Double,
healthStatus: String,
private val droughtTolerance: String,
private val sandContent: Int
) : Plant(scientificName, commonName, heightInCm, healthStatus) {
override fun water() {
println("Minimal watering for $commonName - checking soil dryness first")
println("Using specialized desert plant watering technique")
}
override fun displayCareInstructions() {
println("=== Desert Plant Care: $commonName ===")
println("Scientific Name: $scientificName")
println("Drought Tolerance: $droughtTolerance")
println("Required Sand Content: $sandContent%")
println("Allow soil to dry completely between waterings")
}
}
// Specialized class for alpine plants
class AlpineFlora(
scientificName: String,
commonName: String,
heightInCm: Double,
healthStatus: String,
private val minimumAltitude: Int,
private val frostResistant: Boolean
) : Plant(scientificName, commonName, heightInCm, healthStatus) {
override fun water() {
println("Careful watering for $commonName - ensuring good drainage")
println("Using specialized alpine watering technique to prevent root rot")
}
override fun displayCareInstructions() {
println("=== Alpine Plant Care: $commonName ===")
println("Scientific Name: $scientificName")
println("Minimum Altitude: ${minimumAltitude}m")
println("Frost Resistant: $frostResistant")
println("Requires excellent drainage and protection from excessive moisture")
}
}
// Horticulturist class demonstrating polymorphic plant care
class Horticulturist(val name: String, val specialization: String) {
fun providePlantCare(plant: Plant) {
println("\n$name ($specialization) is tending to the plant:")
plant.displayCareInstructions()
plant.water()
plant.prune()
plant.fertilize()
}
}
// Botanical Garden management demonstrating the power of polymorphism
class BotanicalGarden {
private val plants = mutableListOf<Plant>()
private val horticulturists = mutableListOf<Horticulturist>()
fun addPlant(plant: Plant) {
plants.add(plant)
}
fun addHorticulturist(horticulturist: Horticulturist) {
horticulturists.add(horticulturist)
}
fun performDailyCare() {
println("\n=== Daily Care Routine Starting ===\n")
plants.forEach { plant ->
// Find appropriate specialist for the plant type
val specialist = when (plant) {
is TropicalPlant -> horticulturists.find { it.specialization == "Tropical" }
is DesertPlant -> horticulturists.find { it.specialization == "Desert" }
is AlpineFlora -> horticulturists.find { it.specialization == "Alpine" }
else -> horticulturists.firstOrNull()
}
specialist?.providePlantCare(plant) ?: println("No specialist available for this plant type")
}
}
}
fun main() {
println("""
Welcome to the Polymorphic Botanical Gardens!
In our gardens, we demonstrate how object-oriented programming naturally
mirrors the real world. Just as plants have evolved to thrive in different
environments, our code evolves to handle different types of plants efficiently.
Watch as our specialized horticulturists care for various plants:
""".trimIndent())
val garden = BotanicalGarden()
// Adding different types of plants
garden.addPlant(TropicalPlant(
"Strelitzia reginae",
"Bird of Paradise",
150.0,
"Healthy",
80,
18.0
))
garden.addPlant(DesertPlant(
"Ferocactus wislizeni",
"Fishhook Barrel Cactus",
60.0,
"Excellent",
"High",
90
))
garden.addPlant(AlpineFlora(
"Leontopodium alpinum",
"Edelweiss",
15.0,
"Good",
2000,
true
))
// Adding specialized horticulturists
garden.addHorticulturist(Horticulturist("Maria", "Tropical"))
garden.addHorticulturist(Horticulturist("Ahmed", "Desert"))
garden.addHorticulturist(Horticulturist("Hans", "Alpine"))
// Perform daily care routine
garden.performDailyCare()
}
```
This example demonstrates several key benefits of polymorphism:
1. **Natural Modeling**: Just as real plants share common characteristics but require specialized care, our Plant hierarchy models this through inheritance and polymorphism.
2. **Code Reusability**: The base Plant class provides common functionality that specialized plants can inherit and modify as needed.
3. **Extensibility**: New plant types can be easily added by creating new classes that inherit from Plant, without changing existing code.
4. **Simplified Management**: The BotanicalGarden class can treat all plants uniformly through their common interface while still providing specialized care.
5. **Type Safety**: The compiler ensures that all plant types implement required functionality, preventing errors.
The example shows how polymorphism allows us to:
- Write code that works with objects at different levels of abstraction
- Handle specialized cases without complex conditional logic
- Create maintainable and scalable systems
- Model real-world relationships effectively
Challenge thinking for students:
1. Add more specialized plant types with unique care requirements
2. Include additional garden management features
3. Add more complex interactions between plants and horticulturists
Learning Objectives
By the end of this lab, students will be able to:
1. Understand the fundamental concepts of objects, types, and classes in Kotlin
2. Explain and implement polymorphism in Kotlin applications
3. Differentiate between method overloading and method overriding
4. Create flexible and reusable code using polymorphic principles
5. Apply polymorphism to solve real-world business problems
Part 1: Foundation Concepts
Objects, Types, and Classes
In Kotlin, everything starts with objects. An object is a self-contained unit that contains:
- Data (properties)
- Behaviors (methods)
- A specific type (class)
```kotlin
// Basic class definition
class Smartphone {
var brand: String = ""
var model: String = ""
fun makeCall() {
println("Calling from $brand $model")
}
}
// Creating an object
val myPhone = Smartphone()
```
Part 2: Understanding Polymorphism
Software systems are constructed by connecting OBJECTS together:
Polymorphism belongs to the side of this equation which relates to inheritance.
Inheritance means to “sub class” (create a small scale version of a parent class).
In the subclass, we can access the parent’s data fields and methods.
In general, we won’t know at design time how the sub class might be used.
Therefore the programmers is responsible for provide situationally appropriate method implementations for the subclasses.
Polymorphism means "many forms" and allows objects to take on multiple forms while sharing common characteristics.
In Kotlin, we have two main types of polymorphism:
1. Static Polymorphism (Compile-time) - Method Overloading
2. Dynamic Polymorphism (Runtime) - Method Overriding
Exercise 1: Method Overloading
Method overloading allows multiple methods with the same name but different parameters.
```kotlin
class Calculator {
// Add two numbers
fun add(a: Int, b: Int): Int {
return a + b
}
// Add three numbers
fun add(a: Int, b: Int, c: Int): Int {
return a + b + c
}
// Add two doubles
fun add(a: Double, b: Double): Double {
return a + b
}
}
// Usage
val calc = Calculator()
println(calc.add(5, 3)) // Calls first method
println(calc.add(5, 3, 2)) // Calls second method
println(calc.add(5.5, 3.2)) // Calls third method
```
Exercise 2: Method Overriding
Method overriding allows a subclass to provide a specific implementation of a method already defined in its parent class.
open class Vehicle {
open fun startEngine() {
println("Generic vehicle engine starting")
}
open fun stopEngine() {
println("Engine stopping")
}
}
class ElectricCar : Vehicle() {
override fun startEngine() {
println("Electric motor powering up silently")
}
override fun stopEngine() {
println("Electric motor shutting down")
}
fun chargeBattery() {
println("Charging battery...")
}
}
class DieselTruck : Vehicle() {
override fun startEngine() {
println("Diesel engine starting with a rumble")
}
override fun stopEngine() {
println("Diesel engine cooling down and stopping")
}
fun loadCargo() {
println("Loading cargo into truck")
}
}
// Fleet management class to demonstrate polymorphic collections
class FleetManager {
private val vehicles = mutableListOf<Vehicle>()
fun addVehicle(vehicle: Vehicle) {
vehicles.add(vehicle)
}
fun startAllVehicles() {
println("\nStarting all vehicles in fleet:")
vehicles.forEach { it.startEngine() }
}
fun stopAllVehicles() {
println("\nStopping all vehicles in fleet:")
vehicles.forEach { it.stopEngine() }
}
fun performSpecializedTasks() {
println("\nPerforming specialized tasks:")
vehicles.forEach { vehicle ->
when (vehicle) {
is ElectricCar -> vehicle.chargeBattery()
is DieselTruck -> vehicle.loadCargo()
else -> println("No specialized task for this vehicle type")
}
}
}
}
fun main() {
println("Vehicle Polymorphism Demo")
println("-------------------------")
// Creating vehicles
val genericVehicle = Vehicle()
val tesla = ElectricCar()
val volvoTruck = DieselTruck()
// Demonstration 1: Direct usage
println("\nDirect vehicle usage:")
println("Generic Vehicle:")
genericVehicle.startEngine()
println("\nElectric Car:")
tesla.startEngine()
tesla.chargeBattery()
println("\nDiesel Truck:")
volvoTruck.startEngine()
volvoTruck.loadCargo()
// Demonstration 2: Polymorphic array
println("\nPolymorphic array demonstration:")
val vehicles = arrayOf(genericVehicle, tesla, volvoTruck)
vehicles.forEach { it.startEngine() }
// Demonstration 3: Fleet management
val fleet = FleetManager()
fleet.addVehicle(genericVehicle)
fleet.addVehicle(tesla)
fleet.addVehicle(volvoTruck)
fleet.startAllVehicles()
fleet.performSpecializedTasks()
fleet.stopAllVehicles()
// Demonstration 4: Safe casting and type checking
println("\nType checking and safe casting demonstration:")
vehicles.forEach { vehicle ->
when (vehicle) {
is ElectricCar -> {
println("Found Electric Car:")
vehicle.chargeBattery()
}
is DieselTruck -> {
println("Found Diesel Truck:")
vehicle.loadCargo()
}
else -> println("Found generic vehicle")
}
}
// Demonstration 5: Polymorphic function parameter
fun processVehicle(vehicle: Vehicle) {
println("\nProcessing vehicle:")
vehicle.startEngine()
if (vehicle is ElectricCar) {
println("Processing electric car specifically:")
vehicle.chargeBattery()
}
}
println("\nTesting polymorphic function:")
processVehicle(tesla)
processVehicle(volvoTruck)
}
Vehicle Polymorphism Demo
-------------------------
Direct vehicle usage:
Generic Vehicle:
Generic vehicle engine starting
Electric Car:
Electric motor powering up silently
Charging battery...
Diesel Truck:
Diesel engine starting with a rumble
Loading cargo into truck
Polymorphic array demonstration:
Generic vehicle engine starting
Electric motor powering up silently
Diesel engine starting with a rumble
Starting all vehicles in fleet:
Generic vehicle engine starting
Electric motor powering up silently
Diesel engine starting with a rumble
Performing specialized tasks:
No specialized task for this vehicle type
Charging battery...
Loading cargo into truck
Stopping all vehicles in fleet:
Engine stopping
Electric motor shutting down
Diesel engine cooling down and stopping
Type checking and safe casting demonstration:
Found generic vehicle
Found Electric Car:
Charging battery...
Found Diesel Truck:
Loading cargo into truck
Testing polymorphic function:
Processing vehicle:
Electric motor powering up silently
Processing electric car specifically:
Charging battery...
Processing vehicle:
Diesel engine starting with a rumble
Process finished with exit code 0
Part 3: Real-World Application - E-Commerce System
Let's create a more complex example modeling an e-commerce system:
// Base Product class
open class Product(
val id: String,
val name: String,
open val price: Double
) {
open fun calculateTax(): Double {
return price * 0.1 // Basic 10% tax
}
open fun display() {
println("Product: $name - $${price}")
}
}
// Digital Product
class DigitalProduct(
id: String,
name: String,
price: Double,
val downloadSize: Double
) : Product(id, name, price) {
override fun calculateTax(): Double {
return price * 0.05 // Digital goods taxed at 5%
}
override fun display() {
println("Digital Product: $name - $${price.format()} (${downloadSize}GB)")
}
}
// Physical Product
class PhysicalProduct(
id: String,
name: String,
price: Double,
val weight: Double,
val dimensions: String
) : Product(id, name, price) {
override fun calculateTax(): Double {
return price * 0.15 // Physical goods taxed at 15%
}
override fun display() {
println("Physical Product: $name - $${price.format()} (${weight}kg)")
}
// Overloaded method for shipping calculation
fun calculateShipping(distance: Int): Double {
return weight * 0.1 * distance
}
fun calculateShipping(distance: Int, express: Boolean): Double {
val baseShipping = calculateShipping(distance)
return if (express) baseShipping * 1.5 else baseShipping
}
}
// Subscription Product
class SubscriptionProduct(
id: String,
name: String,
private val monthlyPrice: Double,
private val durationMonths: Int
) : Product(id, name, monthlyPrice * durationMonths) {
override fun calculateTax(): Double {
return price * 0.08 // Subscription goods taxed at 8%
}
override fun display() {
println("Subscription: $name - $${monthlyPrice.format()}/month for $durationMonths months (Total: $${price.format()})")
}
}
// Shopping Cart demonstrating polymorphism
class ShoppingCart {
private val items = mutableListOf<Product>()
fun addItem(product: Product) {
items.add(product)
}
fun removeItem(product: Product) {
items.remove(product)
}
fun calculateTotal(): Double {
return items.sumOf { it.price + it.calculateTax() }
}
fun calculateTotalTax(): Double {
return items.sumOf { it.calculateTax() }
}
fun displayCart() {
if (items.isEmpty()) {
println("Shopping Cart is empty")
return
}
println("\nShopping Cart Contents:")
println("----------------------")
items.forEach { it.display() }
println("----------------------")
println("Subtotal: $${items.sumOf { it.price }.format()}")
println("Total Tax: $${calculateTotalTax().format()}")
println("Final Total: $${calculateTotal().format()}")
}
}
// Extension function to format currency
fun Double.format(): String = String.format("%.2f", this)
// Order processor to demonstrate polymorphic handling
class OrderProcessor {
fun processOrder(cart: ShoppingCart, shippingAddress: String? = null) {
println("\nProcessing Order:")
println("----------------")
cart.displayCart()
if (shippingAddress != null) {
println("\nShipping to: $shippingAddress")
}
println("\nOrder processed successfully!")
}
}
fun main() {
println("E-Commerce System Demonstration")
println("==============================")
// Create sample products
val ebook = DigitalProduct("D1", "Programming Kotlin", 29.99, 2.5)
val physicalBook = PhysicalProduct("P1", "Kotlin in Action", 49.99, 0.8, "24x18x3cm")
val laptop = PhysicalProduct("P2", "MacBook Pro", 1299.99, 2.1, "31x22x1.5cm")
val streaming = SubscriptionProduct("S1", "Video Streaming", 14.99, 12)
val software = SubscriptionProduct("S2", "Cloud Storage", 9.99, 6)
// Create shopping cart
val cart = ShoppingCart()
// Demonstration 1: Adding items and displaying cart
println("\nAdding items to cart...")
cart.addItem(ebook)
cart.addItem(physicalBook)
cart.displayCart()
// Demonstration 2: Calculating shipping for physical items
println("\nShipping Calculations:")
println("Standard shipping for '${physicalBook.name}': $${physicalBook.calculateShipping(100).format()}")
println("Express shipping for '${physicalBook.name}': $${physicalBook.calculateShipping(100, true).format()}")
// Demonstration 3: Adding subscription products
println("\nAdding subscription products...")
cart.addItem(streaming)
cart.addItem(software)
cart.displayCart()
// Demonstration 4: Order processing
val orderProcessor = OrderProcessor()
orderProcessor.processOrder(cart, "123 Main St, Anytown, USA")
// Demonstration 5: Creating a digital-only cart
println("\nCreating a digital-only cart...")
val digitalCart = ShoppingCart()
digitalCart.addItem(ebook)
digitalCart.addItem(streaming)
digitalCart.addItem(software)
println("\nProcessing digital order...")
orderProcessor.processOrder(digitalCart)
// Demonstration 6: Polymorphic type checking
println("\nAnalyzing cart contents:")
val allProducts = listOf(ebook, physicalBook, laptop, streaming, software)
println("\nProduct Analysis:")
allProducts.forEach { product ->
when (product) {
is DigitalProduct -> println("Digital - ${product.name} (${product.downloadSize}GB)")
is PhysicalProduct -> println("Physical - ${product.name} (${product.weight}kg)")
is SubscriptionProduct -> println("Subscription - ${product.name}")
else -> println("Unknown product type")
}
}
}
Lab Exercise Tasks
1. done: Create a main function that demonstrates the use of the e-commerce system:
```kotlin
fun main() {
val cart = ShoppingCart()
// Add different types of products
cart.addItem(DigitalProduct("D1", "E-book: Kotlin Programming", 29.99, 0.5))
cart.addItem(PhysicalProduct("P1", "Mechanical Keyboard", 149.99, 1.2, "45x15x5"))
// Display cart and total
cart.displayCart()
// Demonstrate shipping calculation with overloaded methods
val keyboard = PhysicalProduct("P1", "Mechanical Keyboard", 149.99, 1.2, "45x15x5")
println("Standard shipping: $${keyboard.calculateShipping(100)}")
println("Express shipping: $${keyboard.calculateShipping(100, true)}")
}
E-Commerce System Demonstration
==============================
Adding items to cart...
Shopping Cart Contents:
----------------------
Digital Product: Programming Kotlin - $29.99 (2.5GB)
Physical Product: Kotlin in Action - $49.99 (0.8kg)
----------------------
Subtotal: $79.98
Total Tax: $9.00
Final Total: $88.98
Shipping Calculations:
Standard shipping for 'Kotlin in Action': $8.00
Express shipping for 'Kotlin in Action': $12.00
Adding subscription products...
Shopping Cart Contents:
----------------------
Digital Product: Programming Kotlin - $29.99 (2.5GB)
Physical Product: Kotlin in Action - $49.99 (0.8kg)
Subscription: Video Streaming - $14.99/month for 12 months (Total: $179.88)
Subscription: Cloud Storage - $9.99/month for 6 months (Total: $59.94)
----------------------
Subtotal: $319.80
Total Tax: $28.18
Final Total: $347.98
Processing Order:
----------------
Shopping Cart Contents:
----------------------
Digital Product: Programming Kotlin - $29.99 (2.5GB)
Physical Product: Kotlin in Action - $49.99 (0.8kg)
Subscription: Video Streaming - $14.99/month for 12 months (Total: $179.88)
Subscription: Cloud Storage - $9.99/month for 6 months (Total: $59.94)
----------------------
Subtotal: $319.80
Total Tax: $28.18
Final Total: $347.98
Shipping to: 123 Main St, Anytown, USA
Order processed successfully!
Creating a digital-only cart...
Processing digital order...
Processing Order:
----------------
Shopping Cart Contents:
----------------------
Digital Product: Programming Kotlin - $29.99 (2.5GB)
Subscription: Video Streaming - $14.99/month for 12 months (Total: $179.88)
Subscription: Cloud Storage - $9.99/month for 6 months (Total: $59.94)
----------------------
Subtotal: $269.81
Total Tax: $20.69
Final Total: $290.50
Order processed successfully!
Analyzing cart contents:
Product Analysis:
Digital - Programming Kotlin (2.5GB)
Physical - Kotlin in Action (0.8kg)
Physical - MacBook Pro (2.1kg)
Subscription - Video Streaming
Subscription - Cloud Storage
Process finished with exit code 0
2. Add a new type of product called `SubscriptionProduct` that has:
- A subscription duration in months
- A monthly fee
- Override the price calculation to return the total cost for the subscription period
- Override the tax calculation to handle subscription-based pricing
This implementation provides:
Complete SubscriptionProduct Class Comprehensive price and tax calculations Handles subscription products Calculates monthly recurring costs Groups items by type for display Provides detailed summaries Remaining months calculation Formatted currency display Various subscription types The code demonstrates how to:
Calculate total subscription costs Handle different subscription durations Apply appropriate tax rates Support auto-renewal options // Base Product class
open class Product(
val id: String,
val name: String,
open val price: Double
) {
open fun calculateTax(): Double {
return price * 0.1 // Basic 10% tax
}
open fun display() {
println("Product: $name - $${price.format()}")
}
}
// Subscription Product implementation
class SubscriptionProduct(
id: String,
name: String,
private val monthlyFee: Double,
private val durationMonths: Int,
private val setupFee: Double = 0.0,
private val hasAutoRenewal: Boolean = false
) : Product(id, name, 0.0) { // Initial price set to 0, will be calculated
// Override price to calculate total subscription cost
override val price: Double
get() = (monthlyFee * durationMonths) + setupFee
// Special tax calculation for subscriptions
override fun calculateTax(): Double {
// Different tax rates for different components
val monthlyTax = monthlyFee * durationMonths * 0.08 // 8% tax on subscription
val setupTax = setupFee * 0.10 // 10% tax on setup fee
return monthlyTax + setupTax
}
// Enhanced display method for subscription details
override fun display() {
println("""
|Subscription: $name
|Monthly Fee: $${monthlyFee.format()}
|Duration: $durationMonths months
|Setup Fee: $${setupFee.format()}
|Auto-Renewal: ${if (hasAutoRenewal) "Yes" else "No"}
|Total Cost: $${price.format()}
|Total Tax: $${calculateTax().format()}
|Grand Total: $${(price + calculateTax()).format()}
""".trimMargin())
}
// Additional subscription-specific methods
fun getRemainingMonths(): Int {
// In a real application, this would compare against subscription start date
return durationMonths
}
fun getMonthlyFee(): Double = monthlyFee
fun estimateRenewalCost(): Double {
return if (hasAutoRenewal) {
monthlyFee * durationMonths // No setup fee on renewal
} else {
0.0
}
}
}
// Shopping Cart to handle all product types
class ShoppingCart {
private val items = mutableListOf<Product>()
fun addItem(product: Product) {
items.add(product)
}
fun removeItem(product: Product) {
items.remove(product)
}
fun calculateTotal(): Double {
return items.sumOf { it.price + it.calculateTax() }
}
fun calculateTotalTax(): Double {
return items.sumOf { it.calculateTax() }
}
// Calculate total monthly recurring cost
fun calculateMonthlyRecurring(): Double {
return items
.filterIsInstance<SubscriptionProduct>()
.sumOf { it.getMonthlyFee() }
}
fun displayCart() {
if (items.isEmpty()) {
println("Shopping Cart is empty")
return
}
println("\nShopping Cart Contents:")
println("======================")
// Group items by type for better display
val subscriptions = items.filterIsInstance<SubscriptionProduct>()
val otherProducts = items.filter { it !is SubscriptionProduct }
if (subscriptions.isNotEmpty()) {
println("\nSubscriptions:")
println("--------------")
subscriptions.forEach { it.display() }
}
if (otherProducts.isNotEmpty()) {
println("\nOther Products:")
println("--------------")
otherProducts.forEach { it.display() }
}
println("\nCart Summary:")
println("-------------")
println("Subtotal: $${items.sumOf { it.price }.format()}")
println("Total Tax: $${calculateTotalTax().format()}")
println("Final Total: $${calculateTotal().format()}")
println("Monthly Recurring: $${calculateMonthlyRecurring().format()}")
}
}
// Utility extension function for currency formatting
fun Double.format(): String = String.format("%.2f", this)
fun main() {
println("Subscription Product Demonstration")
println("================================")
// Create various subscription products
val streamingService = SubscriptionProduct(
id = "SUB001",
name = "Premium Streaming Service",
monthlyFee = 14.99,
durationMonths = 12,
setupFee = 0.0,
hasAutoRenewal = true
)
val gymMembership = SubscriptionProduct(
id = "SUB002",
name = "Fitness Club Membership",
monthlyFee = 49.99,
durationMonths = 6,
setupFee = 99.99,
hasAutoRenewal = false
)
val softwareLicense = SubscriptionProduct(
id = "SUB003",
name = "Professional Software Suite",
monthlyFee = 29.99,
durationMonths = 12,
setupFee = 49.99,
hasAutoRenewal = true
)
// Create and populate shopping cart
val cart = ShoppingCart()
// Demonstration 1: Adding single subscription
println("\nAdding streaming service to cart:")
cart.addItem(streamingService)
cart.displayCart()
// Demonstration 2: Multiple subscriptions
println("\nAdding gym membership and software license:")
cart.addItem(gymMembership)
cart.addItem(softwareLicense)
cart.displayCart()
// Demonstration 3: Analyzing subscription costs
println("\nSubscription Analysis:")
println("=====================")
val subscriptions = listOf(streamingService, gymMembership, softwareLicense)
subscriptions.forEach { subscription ->
println("\n${subscription.name}:")
println("- Monthly Fee: $${subscription.getMonthlyFee().format()}")
println("- Total Cost: $${subscription.price.format()}")
println("- Tax: $${subscription.calculateTax().format()}")
println("- Renewal Cost: $${subscription.estimateRenewalCost().format()}")
println("- Remaining Months: ${subscription.getRemainingMonths()}")
}
// Demonstration 4: Cart operations
println("\nCart Operations:")
println("===============")
println("Monthly Recurring Total: $${cart.calculateMonthlyRecurring().format()}")
println("Total Tax: $${cart.calculateTotalTax().format()}")
println("Cart Total: $${cart.calculateTotal().format()}")
// Demonstration 5: Removing items
println("\nRemoving gym membership from cart:")
cart.removeItem(gymMembership)
cart.displayCart()
}
3. Create a reporting system that can generate different types of reports using polymorphism:
- Create a base `Report` class
- Create specific report types (Sales, Inventory, Customer)
- Use method overriding to customize how each report is generated and displayed
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
// Data classes for our business entities
data class Product(
val id: String,
val name: String,
val price: Double,
val stockLevel: Int,
val category: String
)
data class Customer(
val id: String,
val name: String,
val email: String,
val joinDate: LocalDateTime,
val totalPurchases: Double
)
data class Sale(
val id: String,
val customerId: String,
val productId: String,
val quantity: Int,
val saleDate: LocalDateTime,
val totalAmount: Double
)
// Base Report class
abstract class Report(val title: String) {
protected val dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
// Template method pattern
fun generateReport() {
printHeader()
printContent()
printFooter()
}
private fun printHeader() {
println("\n============================================")
println("Report: $title")
println("Generated: ${LocalDateTime.now().format(dateFormatter)}")
println("============================================")
}
protected abstract fun printContent()
private fun printFooter() {
println("============================================")
println("End of Report")
println("============================================\n")
}
protected fun formatCurrency(amount: Double): String {
return String.format("$%.2f", amount)
}
protected fun formatPercentage(value: Double): String {
return String.format("%.1f%%", value * 100)
}
}
// Sales Report
class SalesReport(
private val sales: List<Sale>,
private val products: Map<String, Product>,
private val customers: Map<String, Customer>
) : Report("Sales Analysis Report") {
override fun printContent() {
val totalRevenue = sales.sumOf { it.totalAmount }
val totalSales = sales.size
val averageOrderValue = if (totalSales > 0) totalRevenue / totalSales else 0.0
println("\nSummary Statistics:")
println("------------------")
println("Total Revenue: ${formatCurrency(totalRevenue)}")
println("Total Orders: $totalSales")
println("Average Order Value: ${formatCurrency(averageOrderValue)}")
// Top selling products
println("\nTop Selling Products:")
println("--------------------")
sales.groupBy { it.productId }
.mapValues { it.value.sumOf { sale -> sale.quantity } }
.entries.sortedByDescending { it.value }
.take(3)
.forEach { entry ->
val product = products[entry.key]
println("${product?.name}: ${entry.value} units")
}
// Daily sales trend
println("\nDaily Sales Trend:")
println("-----------------")
sales.groupBy { it.saleDate.toLocalDate() }
.entries.sortedBy { it.key }
.forEach { (date, dailySales) ->
println("$date: ${formatCurrency(dailySales.sumOf { it.totalAmount })}")
}
}
}
// Inventory Report
class InventoryReport(
private val products: List<Product>
) : Report("Inventory Status Report") {
override fun printContent() {
val totalItems = products.sumOf { it.stockLevel }
val totalValue = products.sumOf { it.price * it.stockLevel }
println("\nInventory Summary:")
println("-----------------")
println("Total Items in Stock: $totalItems")
println("Total Inventory Value: ${formatCurrency(totalValue)}")
// Stock levels by category
println("\nStock Levels by Category:")
println("----------------------")
products.groupBy { it.category }
.forEach { (category, products) ->
val categoryValue = products.sumOf { it.price * it.stockLevel }
println("$category:")
println(" Items: ${products.sumOf { it.stockLevel }}")
println(" Value: ${formatCurrency(categoryValue)}")
}
// Low stock alerts
println("\nLow Stock Alerts (< 10 units):")
println("-----------------------------")
products.filter { it.stockLevel < 10 }
.sortedBy { it.stockLevel }
.forEach { product ->
println("${product.name}: ${product.stockLevel} units remaining")
}
}
}
// Customer Report
class CustomerReport(
private val customers: List<Customer>,
private val sales: List<Sale>
) : Report("Customer Analysis Report") {
override fun printContent() {
val totalCustomers = customers.size
val totalRevenue = sales.sumOf { it.totalAmount }
val averageCustomerValue = if (totalCustomers > 0) totalRevenue / totalCustomers else 0.0
println("\nCustomer Base Summary:")
println("---------------------")
println("Total Customers: $totalCustomers")
println("Average Customer Value: ${formatCurrency(averageCustomerValue)}")
// Customer segments by purchase value
println("\nCustomer Segments:")
println("-----------------")
val segments = customers.groupBy { customer ->
when (customer.totalPurchases) {
in 0.0..100.0 -> "Bronze"
in 100.0..500.0 -> "Silver"
in 500.0..1000.0 -> "Gold"
else -> "Platinum"
}
}
segments.forEach { (segment, customers) ->
val percentage = customers.size.toDouble() / totalCustomers
println("$segment: ${customers.size} customers (${formatPercentage(percentage)})")
}
// Recent customers
println("\nRecent Customers (Last 30 days):")
println("-------------------------------")
val thirtyDaysAgo = LocalDateTime.now().minusDays(30)
customers.filter { it.joinDate.isAfter(thirtyDaysAgo) }
.sortedByDescending { it.joinDate }
.forEach { customer ->
println("${customer.name} - Joined: ${customer.joinDate.format(dateFormatter)}")
}
}
}
// Report Generator to demonstrate polymorphic report handling
class ReportGenerator {
fun generateReports(vararg reports: Report) {
reports.forEach { report ->
report.generateReport()
}
}
}
fun main() {
// Create sample data
val products = listOf(
Product("P1", "Laptop", 999.99, 15, "Electronics"),
Product("P2", "Smartphone", 699.99, 8, "Electronics"),
Product("P3", "Headphones", 99.99, 25, "Accessories"),
Product("P4", "Mouse", 29.99, 5, "Accessories"),
Product("P5", "Keyboard", 59.99, 12, "Accessories")
).associateBy { it.id }
val customers = listOf(
Customer("C1", "John Doe", "john@example.com",
LocalDateTime.now().minusMonths(2), 1500.0),
Customer("C2", "Jane Smith", "jane@example.com",
LocalDateTime.now().minusDays(15), 750.0),
Customer("C3", "Bob Johnson", "bob@example.com",
LocalDateTime.now().minusDays(5), 250.0)
).associateBy { it.id }
val sales = listOf(
Sale("S1", "C1", "P1", 1, LocalDateTime.now().minusDays(5), 999.99),
Sale("S2", "C2", "P2", 1, LocalDateTime.now().minusDays(3), 699.99),
Sale("S3", "C3", "P3", 2, LocalDateTime.now().minusDays(1), 199.98),
Sale("S4", "C1", "P4", 1, LocalDateTime.now(), 29.99)
)
// Create reports
val salesReport = SalesReport(sales, products, customers)
val inventoryReport = InventoryReport(products.values.toList())
val customerReport = CustomerReport(customers.values.toList(), sales)
// Generate all reports
val reportGenerator = ReportGenerator()
reportGenerator.generateReports(salesReport, inventoryReport, customerReport)
}
This implementation provides:
Complete Report Hierarchy Abstract base Report class with template method pattern Specialized report types for different business aspects Polymorphic report generation Products with inventory tracking Customer information and history Formatting and Presentation
Key Concepts to Remember
1. **Method Overloading**:
- Same method name, different parameters
- Resolved at compile-time
- Based on method signature
- Cannot be based on return type alone
2. **Method Overriding**:
- Same method signature in parent and child classes
- Resolved at runtime
- Requires `open` keyword on parent class/method
- Uses `override` keyword in child class
3. **Benefits of Polymorphism**:
- Code reusability
- Flexibility in design
- Easier maintenance
- Better organization of code
- Runtime dynamic behavior
Challenge Exercise (*** this is your homework exercise: Paste your screen shot in Slack)
Create a notification system for the e-commerce platform that demonstrates both types of polymorphism:
1. Create a base `Notification` class
2. Implement different notification types (Email, SMS, Push)
3. Use method overloading for different notification formats
4. Use method overriding for different delivery mechanisms
Document your solution highlighting the use of both types of polymorphism in action.
Advanced Topics in Polymorphism:
Additional Topics for Android Development Students
1. Sealed Classes
# When to Use Sealed Classes: A Practical Guide
## The Power of Sealed Classes
Sealed classes in Kotlin shine brightest when you need to represent a restricted set of types that are known at compile time.
Think of them as "enums on steroids" – they provide type safety and exhaustive checking while allowing each subtype to carry its own data.
## Best Use Cases
### 1. State Management
```kotlin
sealed class UiState {
object Loading : UiState()
data class Success(val data: List<Item>) : UiState()
data class Error(val message: String) : UiState()
object Empty : UiState()
}
```
Perfect for UI state management because:
- All possible states are known
- Each state can carry relevant data
- Compiler ensures you handle all cases
- Makes state transitions explicit and safe
### 2. Network Response Handling
```kotlin
sealed class ApiResponse<out T> {
data class Success<T>(val data: T) : ApiResponse<T>()
data class Error(
val code: Int,
val message: String,
val cause: Exception? = null
) : ApiResponse<Nothing>()
object Loading : ApiResponse<Nothing>()
data class NetworkError(val exception: IOException) : ApiResponse<Nothing>()
}
class UserRepository {
fun fetchUser(id: String): ApiResponse<User> {
return try {
// Network call
ApiResponse.Success(user)
} catch (e: IOException) {
ApiResponse.NetworkError(e)
} catch (e: Exception) {
ApiResponse.Error(500, "Unknown error", e)
}
}
}
```
### 3. Event Handling
```kotlin
sealed class NavigationEvent {
data class NavigateToDetail(val id: String) : NavigationEvent()
object NavigateBack : NavigationEvent()
data class NavigateToExternalUrl(val url: String) : NavigationEvent()
data class ShowDialog(val message: String) : NavigationEvent()
}
```
### 4. Domain-Specific Result Types
```kotlin
sealed class PaymentResult {
data class Success(
val transactionId: String,
val amount: Double
) : PaymentResult()
sealed class Failure : PaymentResult() {
data class InsufficientFunds(val missing: Double) : Failure()
data class CardDeclined(val reason: String) : Failure()
object NetworkError : Failure()
data class SecurityCheck(val required: List<String>) : Failure()
}
}
```
## Why Sealed Classes Are Superior in These Cases
1. **Type Safety**
```kotlin
fun handlePayment(result: PaymentResult) {
when (result) {
is PaymentResult.Success -> processSuccess(result.transactionId)
is PaymentResult.Failure.InsufficientFunds -> requestMoreFunds(result.missing)
is PaymentResult.Failure.CardDeclined -> showError(result.reason)
is PaymentResult.Failure.NetworkError -> retryPayment()
is PaymentResult.Failure.SecurityCheck -> initiateVerification(result.required)
}
// Compiler ensures all cases are handled
}
```
2. **Hierarchical Organization**
```kotlin
sealed class ValidationResult {
sealed class Valid : ValidationResult() {
object Perfect : Valid()
data class WithWarnings(val warnings: List<String>) : Valid()
}
sealed class Invalid : ValidationResult() {
data class FieldErrors(val errors: Map<String, String>) : Invalid()
data class BusinessRuleViolation(val rule: String) : Invalid()
}
}
```
3. **Pattern Matching Excellence**
```kotlin
val message = when (validationResult) {
is ValidationResult.Valid.Perfect -> "All good!"
is ValidationResult.Valid.WithWarnings -> "Passed with ${result.warnings.size} warnings"
is ValidationResult.Invalid.FieldErrors -> "Please correct the fields"
is ValidationResult.Invalid.BusinessRuleViolation -> "Business rule violated: ${result.rule}"
}
```
## When NOT to Use Sealed Classes
1. **Simple Enumerations**
```kotlin
// Don't use sealed class for this
sealed class Direction {
object North : Direction()
object South : Direction()
object East : Direction()
object West : Direction()
}
// Instead, use enum
enum class Direction {
NORTH, SOUTH, EAST, WEST
}
```
2. **Open Type Hierarchies**
```kotlin
// Don't use sealed class if new types can be added later
sealed class Animal {
class Dog : Animal()
class Cat : Animal()
}
// Instead, use regular inheritance
open class Animal
class Dog : Animal()
class Cat : Animal()
```
## Best Practices
1. **Keep it Focused**
```kotlin
sealed class AuthenticationResult {
data class Success(
val user: User,
val token: String
) : AuthenticationResult()
sealed class Failure : AuthenticationResult() {
object InvalidCredentials : Failure()
object AccountLocked : Failure()
data class TwoFactorRequired(val method: String) : Failure()
}
}
```
2. **Use Data Classes for States with Data**
```kotlin
sealed class SearchState {
object Initial : SearchState()
object Searching : SearchState()
data class Results(
val items: List<Item>,
val totalCount: Int
) : SearchState()
data class Error(
val message: String,
val retryable: Boolean
) : SearchState()
}
```
3. **Leverage Sealed Class Hierarchies**
```kotlin
sealed class ViewEvent {
sealed class User : ViewEvent() {
data class Clicked(val id: String) : User()
data class LongPressed(val id: String) : User()
}
sealed class System : ViewEvent() {
object LowMemory : System()
data class Error(val message: String) : System()
}
}
```
## Real-World Example: Document Processing
```kotlin
sealed class Document {
abstract val id: String
abstract val created: LocalDateTime
data class Invoice(
override val id: String,
override val created: LocalDateTime,
val amount: Double,
val dueDate: LocalDate
) : Document()
data class Report(
override val id: String,
override val created: LocalDateTime,
val title: String,
val content: String
) : Document()
data class Contract(
override val id: String,
override val created: LocalDateTime,
val parties: List<String>,
val terms: List<String>
) : Document()
}
class DocumentProcessor {
fun process(document: Document) {
val result = when (document) {
is Document.Invoice -> processInvoice(document)
is Document.Report -> processReport(document)
is Document.Contract -> processContract(document)
}
// Compiler ensures all document types are handled
}
}
```
## Conclusion
Sealed classes are most appropriate when:
1. You have a finite set of possible types
2. Different types need to carry different data
3. You want compile-time exhaustiveness checking
4. You need to represent hierarchical state machines
5. You're handling domain-specific results or errors
They provide a powerful tool for creating robust, type-safe applications while maintaining code readability and maintainability.
Sealed classes represent restricted class hierarchies where all subtypes must be defined within the same file.
They're especially useful for representing limited sets of possibilities.
```kotlin
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val message: String) : Result()
object Loading : Result()
}
fun handleResult(result: Result) {
when (result) {
is Result.Success -> println("Success: ${result.data}")
is Result.Error -> println("Error: ${result.message}")
is Result.Loading -> println("Loading...")
// No 'else' needed - compiler knows all possibilities
}
}
This implementation demonstrates:
Complete Sealed Class Hierarchy Simulated network operations