7  OOP in Dart

7.1 Overview Dart vs Python Classes

Creation:

class Student {
  // Instance properties (belong to each object)
  String name;
  int age;
  double gpa;
  
  // Static (class) property (shared across all instances)
  static String school = "Medical University";
  static int studentCount = 0;
  
  // Constructor
  Student(this.name, this.age, this.gpa) {
    // Increment the student count when a new instance is created
    Student.studentCount++;
  }
  
  // Instance method (operates on instance data)
  String getDetails() {
    return "Name: $name, Age: $age, GPA: $gpa";
  }
  
  // Instance method
  bool isHonorStudent() {
    return gpa >= 3.5;
  }
  
  // Static (class) method (doesn't need an instance to be called)
  static String getSchoolMotto() {
    return "Learning for a healthier tomorrow";
  }
  
  // Static method that uses the class property
  static int getTotalStudents() {
    return studentCount;
  }
}
class Student:
    # Class properties (shared across all instances)
    school = "Medical University"
    student_count = 0
    
    # Constructor
    def __init__(self, name, age, gpa):
        # Instance properties (belong to each object)
        self.name = name
        self.age = age
        self.gpa = gpa
        
        # Increment the student count when a new instance is created
        Student.student_count += 1
    
    # Instance method (operates on instance data)
    def get_details(self):
        return f"Name: {self.name}, Age: {self.age}, GPA: {self.gpa}"
    
    # Instance method
    def is_honor_student(self):
        return self.gpa >= 3.5
    
    # Class method (doesn't need an instance to be called)
    @classmethod
    def get_school_motto(cls):
        return "Learning for a healthier tomorrow"
    
    # Class method that uses the class property
    @classmethod
    def get_total_students(cls):
        return cls.student_count

Usage:

void main() {
  // Accessing class property and method before creating any instances
  print("School: ${Student.school}");
  print("Motto: ${Student.getSchoolMotto()}");
  
  // Creating instances
  var student1 = Student("John", 22, 3.7);
  var student2 = Student("Sarah", 24, 3.9);
  
  // Accessing instance properties and methods
  print(student1.getDetails());
  print("Is honor student: ${student1.isHonorStudent()}");
  
  print(student2.getDetails());
  print("Is honor student: ${student2.isHonorStudent()}");
  
  // Accessing class method that uses class property
  print("Total students: ${Student.getTotalStudents()}");
}

# Main execution
if __name__ == "__main__":
    # Accessing class property and method before creating any instances
    print(f"School: {Student.school}")
    print(f"Motto: {Student.get_school_motto()}")
    
    # Creating instances
    student1 = Student("John", 22, 3.7)
    student2 = Student("Sarah", 24, 3.9)
    
    # Accessing instance properties and methods
    print(student1.get_details())
    print(f"Is honor student: {student1.is_honor_student()}")
    
    print(student2.get_details())
    print(f"Is honor student: {student2.is_honor_student()}")
    
    # Accessing class method that uses class property
    print(f"Total students: {Student.get_total_students()}")

7.2 Classes and Objects in Dart

In Dart, everything revolves around classes and objects, much like in Python. A class is a blueprint for creating objects, and it encapsulates data (properties) and behavior (methods).

Here’s a basic example of a class in Dart:

class Patient {
  // Properties (instance variables)
  String name;
  int age;
  String id;
  
  // Constructor
  Patient(this.name, this.age, this.id);
  
  // Method
  void displayInfo() {
    print('Patient: $name, Age: $age, ID: $id');
  }
}

void main() {
  // Creating an object
  var patient1 = Patient('John Doe', 45, 'P12345');
  
  // Accessing properties
  print(patient1.name);  // John Doe
  
  // Calling methods
  patient1.displayInfo();  // Patient: John Doe, Age: 45, ID: P12345
}

Notice how the constructor uses this.name, this.age, this.id - this is a shorthand syntax in Dart that both defines the constructor parameters and assigns them to the corresponding instance variables.

7.3 Four Core OOP Principles in Dart

Let’s explore how Dart implements the four fundamental principles of OOP:

7.3.1 1. Encapsulation

Encapsulation means hiding internal state and requiring all interaction to be performed through an object’s methods. In Dart, you can create private variables by prefixing them with an underscore (_).

class MedicalRecord {
  // Private properties (note the underscore)
  String _patientId;
  List<String> _diagnoses = [];
  
  // Constructor
  MedicalRecord(this._patientId);
  
  // Getter
  String get patientId => _patientId;
  
  // Methods to interact with private data
  void addDiagnosis(String diagnosis) {
    _diagnoses.add(diagnosis);
  }
  
  List<String> getDiagnoses() {
    // Return a copy to prevent direct modification
    return List.from(_diagnoses);
  }
}

void main() {
  var record = MedicalRecord('P12345');
  
  // This works
  record.addDiagnosis('Hypertension');
  
  // This would cause an error because _diagnoses is private
  // record._diagnoses.add('Diabetes');  // Error!
  
  // Instead, we use the provided method
  print(record.getDiagnoses());  // [Hypertension]
}

Unlike Python where encapsulation is more of a convention (with _ variables), Dart enforces privacy at the library level. Private members are truly inaccessible outside their library.

7.3.2 2. Inheritance

Inheritance allows you to create a new class that reuses, extends, or modifies the behavior of another class. Dart uses the extends keyword for inheritance:

// Parent class
class Person {
  String name;
  int age;
  
  Person(this.name, this.age);
  
  void introduce() {
    print('Hi, I am $name and I am $age years old.');
  }
}

// Child class
class Doctor extends Person {
  String specialty;
  
  // Call the parent constructor using super
  Doctor(String name, int age, this.specialty) : super(name, age);
  
  // Override parent method
  @override
  void introduce() {
    super.introduce();  // Call the parent method
    print('I am a $specialty specialist.');
  }
  
  // Add new method
  void diagnose() {
    print('Dr. $name is examining the patient...');
  }
}

void main() {
  var doctor = Doctor('Sarah Chen', 35, 'Radiology');
  doctor.introduce();
  // Output:
  // Hi, I am Sarah Chen and I am 35 years old.
  // I am a Radiology specialist.
  
  doctor.diagnose();  // Dr. Sarah Chen is examining the patient...
}

Notice how we use super to call the parent constructor and method. The @override annotation is not strictly required but is good practice as it helps catch errors.

7.3.3 3. Polymorphism

Polymorphism allows objects of different classes to be treated as objects of a common superclass. The most common use is when a parent class reference is used to refer to a child class object.

abstract class MedicalProfessional {
  void performDuty();
}

class Doctor implements MedicalProfessional {
  String specialty;
  
  Doctor(this.specialty);
  
  @override
  void performDuty() {
    print('Doctor is diagnosing patients');
  }
}

class Nurse implements MedicalProfessional {
  @override
  void performDuty() {
    print('Nurse is administering medication');
  }
}

void staffDuty(MedicalProfessional staff) {
  staff.performDuty();
}

void main() {
  staffDuty(Doctor('Radiology'));  // Doctor is diagnosing patients
  staffDuty(Nurse());  // Nurse is administering medication
}

In this example, both Doctor and Nurse implement the MedicalProfessional interface. The staffDuty function accepts any object that implements this interface, demonstrating polymorphism.

7.3.4 4. Abstraction

Abstraction means hiding complex implementation details and showing only the necessary features of an object. In Dart, you can use abstract classes and interfaces to achieve abstraction.

// Abstract class
abstract class Scan {
  // Abstract method (no implementation)
  void perform();
  
  // Concrete method with implementation
  void prepare() {
    print('Preparing the patient for scan');
  }
}

// Concrete implementation
class MRIScan extends Scan {
  @override
  void perform() {
    print('Performing MRI scan');
  }
}

class CTScan extends Scan {
  @override
  void perform() {
    print('Performing CT scan');
  }
}

void main() {
  // Can't instantiate an abstract class
  // var scan = Scan();  // Error!
  
  var mri = MRIScan();
  mri.prepare();  // Inherited from abstract class
  mri.perform();  // Implementation specific to MRIScan
}

7.4 Dart-Specific OOP Features

Now that we’ve covered the basics, let’s look at some Dart-specific OOP features that might be different from what you’re used to:

7.4.1 Named Constructors

Dart allows multiple constructors through named constructors:

class Patient {
  String name;
  int age;
  String id;
  List<String> allergies;
  
  // Primary constructor
  Patient(this.name, this.age, this.id) : allergies = [];
  
  // Named constructor
  Patient.emergency(this.name) : 
    age = 0,
    id = 'EMERGENCY',
    allergies = [];
  
  // Another named constructor with initializer list
  Patient.withAllergies(this.name, this.age, this.id, List<String> allergyList) 
    : allergies = List.from(allergyList);
}

void main() {
  var patient1 = Patient('John', 45, 'P12345');
  var patient2 = Patient.emergency('Unknown');
  var patient3 = Patient.withAllergies('Alice', 30, 'P54321', ['Penicillin', 'Latex']);
}

7.4.2 Factory Constructors

Factory constructors can return instances from cache or instances of subclasses:

class Singleton {
  static final Singleton _instance = Singleton._internal();
  
  // Private constructor
  Singleton._internal();
  
  // Factory constructor
  factory Singleton() {
    return _instance;
  }
}

void main() {
  var s1 = Singleton();
  var s2 = Singleton();
  print(identical(s1, s2));  // true - they are the same instance
}

7.4.3 Mixins

Mixins allow you to reuse code in multiple class hierarchies:

mixin Logger {
  void log(String message) {
    print('LOG: $message');
  }
}

mixin TimeStamper {
  String getTimestamp() {
    return DateTime.now().toString();
  }
}

// Use mixins with the 'with' keyword
class DiagnosticReport with Logger, TimeStamper {
  String patientId;
  String findings;
  
  DiagnosticReport(this.patientId, this.findings);
  
  void save() {
    log('Saving report for patient $patientId at ${getTimestamp()}');
    // Save to database logic would go here
  }
}

void main() {
  var report = DiagnosticReport('P12345', 'Normal lung examination');
  report.save();
  // Output: LOG: Saving report for patient P12345 at 2023-08-01 15:30:45.123456
}

7.4.4 Extension Methods

Extension methods allow you to add functionality to existing classes without modifying them:

// Extending the built-in String class
extension DiagnosticCodeExtension on String {
  bool isValidDiagnosticCode() {
    // Check if the string matches the pattern: Letter followed by 3 digits
    return RegExp(r'^[A-Z][0-9]{3}$').hasMatch(this);
  }
}

void main() {
  var code1 = 'A123';
  var code2 = 'AB12';
  
  print(code1.isValidDiagnosticCode());  // true
  print(code2.isValidDiagnosticCode());  // false
}

7.5 Comparing with Your Existing Knowledge

Since you already know Python and JavaScript, here are some key differences in Dart’s OOP approach:

  1. Strong Typing: Unlike JavaScript and Python, Dart is strongly typed (though type annotations can be omitted with var).

  2. True Privacy: Unlike Python’s convention-based privacy, Dart enforces privacy at the library level with the _ prefix.

  3. Interface Implementation: Dart uses implements for interfaces rather than Python’s duck typing.

  4. No Multiple Inheritance: Like JavaScript, Dart doesn’t support multiple inheritance, but uses mixins instead.

7.6 Getting Started with OOP in Flutter

In Flutter, nearly everything is an object. Widgets, the building blocks of Flutter apps, are instances of classes. Here’s how OOP applies in a simple Flutter widget:

import 'package:flutter/material.dart';

// StatelessWidget is a class we're extending
class GreetingWidget extends StatelessWidget {
  // Properties
  final String patientName;
  
  // Constructor
  const GreetingWidget({Key? key, required this.patientName}) : super(key: key);
  
  // Override build method
  @override
  Widget build(BuildContext context) {
    return Text('Welcome, $patientName!');
  }
}

7.7 super keyword

Understanding the super Keyword in Dart:

In Dart, the super keyword is a powerful feature of object-oriented programming that allows a child class to interact with its parent class. Let me explain how it works and provide some context based on your programming background.

Basic Concept

The super keyword in Dart serves as a reference to the parent class. It allows a subclass to access the members (methods and properties) of its superclass, similar to how you might use inheritance in Python, JavaScript, or R.

7.7.1 When to Use super

There are several key scenarios where you’ll use the super keyword:

  1. Calling the parent constructor
  2. Accessing overridden methods
  3. Accessing parent class properties
  4. Using named constructors from the parent

7.7.2 Comparing to Languages You Know

Since you have experience with Python and JavaScript, here’s how super in Dart compares:

  • In Python: Similar to super() calls, but with different syntax
  • In JavaScript: Similar to super in ES6 classes

7.7.3 Examples and Usage Patterns

7.7.3.1 Calling the Parent Constructor

class Parent {
  String name;
  
  Parent(this.name);
}

class Child extends Parent {
  int age;
  
  // Using super to call parent constructor
  Child(String name, this.age) : super(name);
}

7.7.3.2 Accessing Overridden Methods

class Animal {
  void makeSound() {
    print('Some generic sound');
  }
}

class Dog extends Animal {
  @override
  void makeSound() {
    // Call the parent implementation first
    super.makeSound();
    // Then do Dog-specific behavior
    print('Woof!');
  }
}

7.7.3.3 Accessing Parent Class Properties

class Vehicle {
  int speed = 0;
  
  void accelerate() {
    speed += 10;
  }
}

class SportsCar extends Vehicle {
  @override
  void accelerate() {
    // Access and modify parent property
    super.speed += 5; 
    // Call parent method
    super.accelerate();
  }
}

7.7.4 Advanced Usage

Dart allows for named constructors, and you can use super with them too:

class Parent {
  Parent.named(String value) {
    print('Parent named constructor with $value');
  }
}

class Child extends Parent {
  Child.named(String value) : super.named(value) {
    print('Child named constructor');
  }
}

7.8 Practice Exercise

To solidify your understanding, try creating a simple medical records system in Dart with the following classes:

  • A Person base class
  • A Patient class that extends Person
  • A MedicalRecord class that uses encapsulation
  • A Scan abstract class with concrete implementations

Use the OOP principles we’ve discussed to structure your code. This exercise will help you practice Dart’s syntax while applying OOP concepts.