Classes and Objects in Programming in Dart language

Introduction to Classes and Objects in in Dart language

Dart is a powerful, multiparadigm language that was developed with significant input fr

om Google, fit for building mobile, web, and server applications efficiently. Class and object-oriented design, as in most modern object-oriented languages, is one of the core pillars for Dart. These building blocks create a means to structure complex maintainable code. In this article, we are going to explain classes and objects in Dart.

What are Classes in Dart?

A class in Dart is a blueprint or template used to create objects. It encapsulates data (fields or properties) and behavior (methods or functions) that define how objects created from the class should function. The idea behind classes is to group related properties and functionalities together in a single, reusable entity.

Declaring a Class in Dart

To declare a class in Dart, you use the class keyword followed by the name of the class. Here’s an example:

class Car {
  String brand;
  int year;
  
  // Constructor
  Car(this.brand, this.year);
  
  // Method to display car details
  void displayDetails() {
    print('Car Brand: $brand, Year: $year');
  }
}

Here, we created a class ‘Car‘ comprising two properties – brand and year. It also comprises a constructor that initializes the object and one method displayDetails(), which shows the details of the car.

What are Objects in Dart?

An object, in simple terms, is an instance of a class. The instantiation of an object from a class allocates memory to store the properties and methods defined in that class. Objects allow you to interact with the functionalities and data encapsulated by the class.

Creating an Object in Dart

To create an object in Dart, you use the new keyword or simply call the class constructor directly:

void main() {
  // Creating an object of the Car class
  Car myCar = Car('Tesla', 2023);
  
  // Calling the method of the Car object
  myCar.displayDetails();
}

Here, the object myCar is created from the Car class, initializing it with specific values. The object then accesses the displayDetails() method, which prints the car’s details.

Constructors in Dart

Constructors are special methods used to create and initialize objects. Dart allows different types of constructors, such as default constructors, named constructors, and factory constructors.

  • Default Constructor: This is a simple constructor that directly initializes properties.
Car(String brand, int year) {
  this.brand = brand;
  this.year = year;
}
  • Named Constructors: Dart allows multiple constructors in a class, which can be helpful in creating objects in different ways.
Car.fromOldModel(String brand) {
  this.brand = brand;
  this.year = 1990; // Default old model year
}
  • Factory Constructors: These constructors are used when you need more control over object creation. They can return new instances or even cached objects.
class Car {
  String brand;
  int year;
  
  // Factory constructor
  factory Car.specialEdition(String brand) {
    return Car(brand, 2023); // Special edition model
  }
}

Understanding Object-Oriented Principles in Dart

Dart supports key object-oriented principles that govern how classes and objects interact. Let’s break down the fundamental principles:

  • Encapsulation: It refers to bundling the data (properties) and methods that operate on the data within a single unit (class). This concept protects data from being accessed directly from outside the class, promoting data integrity.

Dart provides encapsulation using getters and setters, along with access modifiers like private, achieved by prefixing an underscore (_) before a variable or method.

class Car {
  String _brand;  // Private field
  
  // Getter for _brand
  String get brand => _brand;
  
  // Setter for _brand
  set brand(String newBrand) => _brand = newBrand;
}

In this example, the ElectricCar class extends the Car class, inheriting its properties and methods while adding new functionality like batteryCapacity.

  • Polymorphism: Dart enables polymorphism, which means that an object can take many forms. You can achieve this by overriding methods in a subclass.
@override
void displayDetails() {
  print('Electric Car: $brand, Year: $year, Battery Capacity: $batteryCapacity');
}
  • Abstraction: Abstraction allows you to define classes that represent abstract concepts, meaning the classes cannot be instantiated directly. Dart provides abstract classes to achieve this.
abstract class Vehicle {
  void drive(); // Abstract method
}

class Car extends Vehicle {
  @override
  void drive() {
    print('Car is driving');
  }
}

Why we need Classes and Objects in Programming in Dart language?

In Dart, as in most of the modern programming languages, classes and objects are the basis for object-oriented programming. Classes and objects are necessary in their own terms because they can organize or manage a difficult system with ease. Here’s why, in Dart programming, classes and objects are important features of the language:

1. Encapsulation

Encapsulation refers to the practice of bundling data and the methods that operate on that data into a single unit, known as a class. This concept is vital for:

  • Data Protection: Encapsulation helps protect an object’s internal state by restricting direct access and allowing modifications only through well-defined methods. This approach prevents unintended interference and promotes data integrity.
  • Maintainability: Encapsulation indeed enables changes to the internal implementation of a class that can be made without influencing other parts of the codebase that depend upon, or interact with, the class. It provides a demarcation that enhances maintainability and modifiability in one’s codebase.
2. Reusability and Modularity

One can achieve reusability and modularity through the use of classes and objects:

  • Reusability of Code: Once a class has been defined, it can be instantiated a number of times, and the same class can be used in different contexts. Such reusability does not result in any redundancy, thus increasing efficient coding practices.
  • Modularity of Design: Classes enable developers to break down complex systems into small, easily maintainable pieces. Such modularity allows for an organized structure in clarity, hence making the code more comprehensible and easy to deal with.
3. Inheritance

Inheritance is the mechanism in which a new class could inherit properties and methods from an already existing class. This feature does the following:

  • Promotes Code Reuse: Common functionality can be defined in a base class and inherited by multiple subclasses. This reuse reduces code duplication and encourages a cleaner codebase.
  • Models Hierarchical Relationships: Inheritance helps model real-world relationships and hierarchies in code, thus making the code more intuitive and closer to domain concepts.
4. Polymorphism

Polymorphism allows objects of different classes to be treated as objects of a common superclass. It provides:

  • Flexibility and extensibility: A subclass can provide an implementation of methods that override the methods in its superclass. So, the same name can be given to methods belonging to different classes, each having a distinct implementation. This results in more general and flexible code.
  • Dynamic behavior: Through a common interface, Polymorphism supports operations on objects of diverse types. It is bound to increase adaptability and extensibility.
5. Abstraction

Abstraction defines classes and methods abstractly, defining what to do, but not how to do it. This principle does the following:

  • defines interfaces: Classes can define methods that subclasses must implement, thus forcing the implementation of a contract or an interface.
  • Simplifies Complexity:It helps the developer focus on the higher-level design and the important features of a system while suppressing the irrelevant and intricate implementation details. This helps to address some of the complexities but also provides greater focus on core functionalities.
6. Design Patterns

With classes and objects, design patterns such as:

  • Singleton: Ensures a class has only one instance, providing a global point of access to that instance.
  • Factory Method: Defines an interface for creating objects, allowing subclasses to alter the type of objects created.
  • Observer: Establishes a dependency between objects so that changes in one object trigger updates in its dependents.

These patterns apply classes and objects to solve some common design problems in an effective way.

Example of Classes and Objects in in Dart language

Modeling a Simple Banking System

Let’s model a basic banking system with classes and objects. We’ll create a BankAccount class to represent a bank account and demonstrate how to use this class to create and interact with objects.

Step 1: Define the BankAccount Class

The BankAccount class will have properties for storing the account holder’s name and balance, and methods to deposit and withdraw money.

class BankAccount {
  String accountHolder;
  double balance;

  // Constructor to initialize the account
  BankAccount(this.accountHolder, this.balance);

  // Method to deposit money into the account
  void deposit(double amount) {
    if (amount > 0) {
      balance += amount;
      print('Deposited \$${amount}. New balance: \$${balance}');
    } else {
      print('Deposit amount must be positive.');
    }
  }

  // Method to withdraw money from the account
  void withdraw(double amount) {
    if (amount > 0 && amount <= balance) {
      balance -= amount;
      print('Withdrew \$${amount}. New balance: \$${balance}');
    } else if (amount > balance) {
      print('Insufficient funds.');
    } else {
      print('Withdrawal amount must be positive.');
    }
  }

  // Method to display account details
  void displayAccountDetails() {
    print('Account Holder: $accountHolder');
    print('Balance: \$${balance}');
  }
}
Step 2: Create Objects and Use the BankAccount Class

Now, let’s create objects from the BankAccount class and use them to perform various operations.

void main() {
  // Create a new bank account object
  BankAccount myAccount = BankAccount('Alice Smith', 1000.0);

  // Display account details
  myAccount.displayAccountDetails();

  // Deposit money into the account
  myAccount.deposit(500.0);

  // Withdraw money from the account
  myAccount.withdraw(200.0);

  // Attempt to withdraw more than the balance
  myAccount.withdraw(1500.0);

  // Display account details again to see the final balance
  myAccount.displayAccountDetails();
}

Class Definition: The BankAccount class encapsulates the properties (accountHolder and balance) and methods (deposit, withdraw, displayAccountDetails) that operate on the properties.

Constructor: The BankAccount constructor initializes the accountHolder and balance when a new object is created.

Methods:

  • deposit(double amount): Adds money to the account if the amount is positive.
  • withdraw(double amount): Removes money from the account if there are sufficient funds and the amount is positive.
  • displayAccountDetails(): Prints the account holder’s name and current balance.

Object Creation and Usage: In the main function, an instance of BankAccount is created with initial values. Methods are then called on this object to perform deposits and withdrawals, and to display account details.

Disadvantages of Classes and Objects in in Dart language

While classes and objects in Dart (and object-oriented programming in general) offer many advantages, there are also some potential disadvantages and challenges associated with their use. Here are some of the key drawbacks:

1. Complexity

  • Increased Learning Curve: Understanding object-oriented concepts such as inheritance, polymorphism, and encapsulation can be challenging for beginners. The added complexity can make it harder for new developers to grasp the fundamentals.
  • Overhead: Object-oriented designs can sometimes introduce unnecessary complexity, especially in simple applications where procedural programming might be more straightforward.

2. Performance Overhead

  • Memory Usage: Creating multiple objects and managing their lifecycle can lead to increased memory usage. Each object may consume additional memory for storing state and methods, which can impact performance, especially in resource-constrained environments.
  • Object Creation Cost: Instantiating objects can involve overhead, including allocation and initialization of memory. In performance-critical applications, this overhead might become a concern.

3. Overengineering

  • Premature Optimization: Developers might over-engineer solutions by applying complex design patterns and architectures where simpler approaches would suffice. This can lead to unnecessary complexity and hinder maintainability.
  • Excessive Abstraction: Overusing abstraction can make the codebase harder to understand. If not carefully managed, excessive abstraction can lead to a disconnect between the code and the real-world problem it aims to solve.

4. Inheritance Pitfalls

  • Tight Coupling: Inheritance can create tight coupling between base and derived classes, making changes in the base class potentially disruptive to all derived classes. This can lead to issues with maintaining and evolving the code.
  • Fragile Base Class Problem: Modifying a base class can inadvertently affect subclasses in unforeseen ways, leading to bugs and maintenance challenges.

5. Difficulty in Debugging

  • Complex Interactions: The interactions between various classes and objects can become intricate, making it challenging to trace bugs and understand the flow of the application. This complexity can complicate debugging efforts.
  • Inheritance Chains: Deep inheritance hierarchies can obscure the source of issues, as it may not be immediately clear which class or method is responsible for a particular behavior.

6. Performance Issues with Inheritance

  • Method Lookup Time: When a method is called on an object, the system needs to determine the correct implementation based on the object’s class. This method lookup can add overhead, especially in complex class hierarchies.

7. Misuse of Inheritance

  • Inheritance vs. Composition: Using inheritance where composition would be more appropriate can lead to inflexible designs. Composition, which involves including objects as components within other objects, can often provide better flexibility and modularity.

8. Increased Complexity in Large Projects

  • Class Explosion: In large projects, the number of classes can become overwhelming. Managing a vast number of classes and their interactions can lead to increased complexity and make the codebase harder to navigate.
  • Dependency Management: Complex class relationships and dependencies can make it challenging to manage changes and ensure that modifications do not introduce unintended

Discover more from PiEmbSysTech

Subscribe to get the latest posts sent to your email.

Leave a Reply

Scroll to Top

Discover more from PiEmbSysTech

Subscribe now to keep reading and get access to the full archive.

Continue reading