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
this.name, this.age, this.gpa) {
Student(// Increment the student count when a new instance is created
.studentCount++;
Student}
// 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)
= "Medical University"
school = 0
student_count
# 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
+= 1
Student.student_count
# 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
"School: ${Student.school}");
print("Motto: ${Student.getSchoolMotto()}");
print(
// Creating instances
var student1 = Student("John", 22, 3.7);
var student2 = Student("Sarah", 24, 3.9);
// Accessing instance properties and methods
.getDetails());
print(student1"Is honor student: ${student1.isHonorStudent()}");
print(
.getDetails());
print(student2"Is honor student: ${student2.isHonorStudent()}");
print(
// Accessing class method that uses class property
"Total students: ${Student.getTotalStudents()}");
print(}
# 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
= Student("John", 22, 3.7)
student1 = Student("Sarah", 24, 3.9)
student2
# 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
this.name, this.age, this.id);
Patient(
// Method
void displayInfo() {
'Patient: $name, Age: $age, ID: $id');
print(}
}
void main() {
// Creating an object
var patient1 = Patient('John Doe', 45, 'P12345');
// Accessing properties
.name); // John Doe
print(patient1
// Calling methods
.displayInfo(); // Patient: John Doe, Age: 45, ID: P12345
patient1}
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
this._patientId);
MedicalRecord(
// Getter
String get patientId => _patientId;
// Methods to interact with private data
void addDiagnosis(String diagnosis) {
.add(diagnosis);
_diagnoses}
List<String> getDiagnoses() {
// Return a copy to prevent direct modification
return List.from(_diagnoses);
}
}
void main() {
var record = MedicalRecord('P12345');
// This works
.addDiagnosis('Hypertension');
record
// This would cause an error because _diagnoses is private
// record._diagnoses.add('Diabetes'); // Error!
// Instead, we use the provided method
.getDiagnoses()); // [Hypertension]
print(record}
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;
this.name, this.age);
Person(
void introduce() {
'Hi, I am $name and I am $age years old.');
print(}
}
// Child class
class Doctor extends Person {
String specialty;
// Call the parent constructor using super
String name, int age, this.specialty) : super(name, age);
Doctor(
// Override parent method
@override
void introduce() {
super.introduce(); // Call the parent method
'I am a $specialty specialist.');
print(}
// Add new method
void diagnose() {
'Dr. $name is examining the patient...');
print(}
}
void main() {
var doctor = Doctor('Sarah Chen', 35, 'Radiology');
.introduce();
doctor// Output:
// Hi, I am Sarah Chen and I am 35 years old.
// I am a Radiology specialist.
.diagnose(); // Dr. Sarah Chen is examining the patient...
doctor}
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;
this.specialty);
Doctor(
@override
void performDuty() {
'Doctor is diagnosing patients');
print(}
}
class Nurse implements MedicalProfessional {
@override
void performDuty() {
'Nurse is administering medication');
print(}
}
void staffDuty(MedicalProfessional staff) {
.performDuty();
staff}
void main() {
'Radiology')); // Doctor is diagnosing patients
staffDuty(Doctor(// Nurse is administering medication
staffDuty(Nurse()); }
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() {
'Preparing the patient for scan');
print(}
}
// Concrete implementation
class MRIScan extends Scan {
@override
void perform() {
'Performing MRI scan');
print(}
}
class CTScan extends Scan {
@override
void perform() {
'Performing CT scan');
print(}
}
void main() {
// Can't instantiate an abstract class
// var scan = Scan(); // Error!
var mri = MRIScan();
.prepare(); // Inherited from abstract class
mri.perform(); // Implementation specific to MRIScan
mri}
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
this.name, this.age, this.id) : allergies = [];
Patient(
// Named constructor
.emergency(this.name) :
Patient= 0,
age = 'EMERGENCY',
id = [];
allergies
// Another named constructor with initializer list
.withAllergies(this.name, this.age, this.id, List<String> allergyList)
Patient: 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
._internal();
Singleton
// Factory constructor
factory Singleton() {
return _instance;
}
}
void main() {
var s1 = Singleton();
var s2 = Singleton();
, s2)); // true - they are the same instance
print(identical(s1}
7.4.3 Mixins
Mixins allow you to reuse code in multiple class hierarchies:
mixin Logger {
void log(String message) {
'LOG: $message');
print(}
}
mixin TimeStamper {
String getTimestamp() {
return DateTime.now().toString();
}
}
// Use mixins with the 'with' keyword
class DiagnosticReport with Logger, TimeStamper {
String patientId;
String findings;
this.patientId, this.findings);
DiagnosticReport(
void save() {
'Saving report for patient $patientId at ${getTimestamp()}');
log(// Save to database logic would go here
}
}
void main() {
var report = DiagnosticReport('P12345', 'Normal lung examination');
.save();
report// 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';
.isValidDiagnosticCode()); // true
print(code1.isValidDiagnosticCode()); // false
print(code2}
7.5 Comparing with Your Existing Knowledge
Since you already know Python and JavaScript, here are some key differences in Dart’s OOP approach:
Strong Typing: Unlike JavaScript and Python, Dart is strongly typed (though type annotations can be omitted with
var
).True Privacy: Unlike Python’s convention-based privacy, Dart enforces privacy at the library level with the
_
prefix.Interface Implementation: Dart uses
implements
for interfaces rather than Python’s duck typing.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:
- Calling the parent constructor
- Accessing overridden methods
- Accessing parent class properties
- 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;
this.name);
Parent(}
class Child extends Parent {
int age;
// Using super to call parent constructor
String name, this.age) : super(name);
Child(}
7.7.3.2 Accessing Overridden Methods
class Animal {
void makeSound() {
'Some generic sound');
print(}
}
class Dog extends Animal {
@override
void makeSound() {
// Call the parent implementation first
super.makeSound();
// Then do Dog-specific behavior
'Woof!');
print(}
}
7.7.3.3 Accessing Parent Class Properties
class Vehicle {
int speed = 0;
void accelerate() {
+= 10;
speed }
}
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 {
.named(String value) {
Parent'Parent named constructor with $value');
print(}
}
class Child extends Parent {
.named(String value) : super.named(value) {
Child'Child named constructor');
print(}
}
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 extendsPerson
- 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.