Object-Oriented Programming Principles in Dart

Subscribe to my newsletter and never miss my upcoming articles

In this article, I want to describe the Object-Oriented Programming and its four principles using examples written in Dart. The concepts are valid in any object-oriented programming language but here we will focus on their implementation in Dart. You can run all the code that I wrote here in DartPad that's really useful when you want to have a ready environment to test some Dart code. The docs for the Dart language is available on its website: Dart documentation.

Introduction

Object-Oriented Programming is a programming paradigm based on the concept of objects. An object is a particular structure that contains two important pieces of code:

  • properties: variables inside an object which define its characteristics, or in other word its state or data;
  • methods: functions with which it can interact with other parts of the code, define its behavior and things that the object can actually do.

To be more precise an object is an instance of a class because the class defines the blueprint or behavior scheme and the object is its implementation within the code. This derives the fact that often object-oriented programming languages are also referred to as class programming languages and the two terms, class and object, are often considered synonyms but in reality, they are not.

In Dart we define a class in this way:

class Developer {
  //properties definition
  int age;
  String name;

  //constructor
  Developer({this.name,this.age});

  //method
  void sayMyName(){
    print("My name is $name");
  }
}

We create a Developer class with these 3 important pieces of code:

  • two properties, age and name, that define the data of our class;
  • a constructor that is used to create an instance of the Developer class, in other words, a Developer object. Dart provides a default constructor, but if we want to assign variables during creation we will have to define a constructor ourselves;
  • a sayMyName() method that we can use to assign functionality to a Developer.

To create an instance of the Developer class and let the Developer say its name we could simply use:

Developer me = Developer(age: 33, name: "Alberto");
me.sayMyName();

Note here, used as a language convention, I started the class name with an uppercase letter and I named properties and methods in camelCase format with lowercase as initial.

Now that we have conceptually introduced what an object is, we can move on to the 4 fundamental principles of object-oriented programming: Abstraction, Encapsulation, Inheritance, Polymorphism, by first bringing a theoretical description and then writing some example code.

Abstraction

Abstraction is the first principle of OOP and like many other programming languages, it's a way to reduce code complexity by dividing functionality into different "chunks" and exposing only essential functionality to the outside world.

Abstraction allows you to focus on what a class does instead of how it does it

In other words, when you instantiate a class you only have to worry about the methods it provides, parameters these methods receive, and the outputs it returns. All the actual implementation parts must not be known externally so if you want to change them, all the code dependent on that class would not be affected by the change. Let's make an example with a simple Square class:

class Square {
  int side;

  Square({this.side});

  int getPerimeter(){
    return side+side+side+side;
  }
}

I defined a Square class and I said that it has a property named side and a method that returns its perimeter. So if I create a Square object with a side of 10, by calling getPerimeter() I can get its perimeter:

Square mySquare = Square(side:10);
print(mySquare.getPerimeter());

Obviously, the code prints 40. Let's suppose that now I found a super optimized way to calculate the perimeter with this new function:

int getPerimeter(){
    return side*4;
}

Now if I want to instantiate another Square object and prints its perimeter I don't have to change anything because I have "abstracted" the perimeter calculation functionality, I hid the details on how I calculated perimeter focusing only on giving the perimeter calculation functionality.

Encapsulation

This is the second principle of OOP and it refers to the ability of an object to hide its data or its state and allow access to its properties only through particular methods. In other languages, this is done by defining the property private and the methods public. So if someone instantiates a class of a certain type he cannot access directly to properties but he must use methods to change property values. A simple example to show this concept is:

class Human {
  //With the underscore, I can define a variable private 
  // so from the outside this variable cannot be changed directly
  int _superSecretVariableForTheAge;

  Human();

  //I create two methods called getter and setter to modify 
  // properties inside my class
  void setAge(int newAge){
     this._superSecretVariableForTheAge = newAge;
  }
  int getAge(){
    return _superSecretVariableForTheAge;
  }
}

Now if I want to define a new Human and set a new age I can only use the setAge method because I cannot access directly the _superSecretVariableForTheAge:

void main() {

  Human mySelf = Human();
  mySelf.setAge(33);
  print(mySelf.getAge());

  //I cannot acces directly the property with this line of code
  mySelf._superSecretVariableForTheAge = 44;

}

If managed correctly, it allows you to see the object as a black-box, with which the interaction takes place only and only through the methods defined by the class. Encapsulation and Abstraction are very connected to each other because they allow you to expose functionality outside the class hiding the implementation details.

Inheritance

This principle leads us to an important concept in object-oriented programming that allows a class to inherit properties and methods from another class and to extend them. So let's define:

  • sub-class: the class that inherits properties and methods from another class, to fix ideas this is often called a child class;
  • super-class: a class that is extended and that provides the basis for other classes to which it provides basic properties and methods, is often also called the parent class.

Based on these concepts we define the concept of an animal with classes and then we create a dog:

class Animal {
  double _weight;

  Animal();

  void breathe(){
    print('I am breathing fresh air');
  }
  void setWeight(double newWeight){
    this._weight = newWeight;
  }
  double getweight(){
    return _weight;
  }
}

So I create a Dog class that inherits from the Animal class by using the extends keyword:

class Dog extends Animal {
  int _legs;

  Dog();

  void run(){
    print('I am running on my legs');
  }
  void setLegs(int newLegs){
    this._legs = newLegs;
  }
  int getLegs(){
    return _legs;
  }
}

Now we can create a simple program in Dart to create a Dog and access both the properties and methods from the Dog class and Animal class.

void main() {

  Dog myPuppy = new Dog();
  myPuppy.setLegs(4);
  myPuppy.setWeight(4.5);
  myPuppy.breathe();
  myPuppy.run();
  print('My puppy weight is ${myPuppy.getweight()} kg and it has got ${myPuppy.getLegs()} legs.');
}

In the example, you can see that I create myPuppy with a class type Dog that inherits from the Animal class. By doing that I can make him breathe and give him a weight using the method defined in its parent class because all animals have the ability to breathe and have weight.

Inheritance can be of the following three types: Single, Multiple and Multi-level. Dart supports Single Inheritance and Multi-level, so a class must extend from only one parent class and you can create multi-level relationships like this: grandparent class -> parent class -> child class.

Polymorphism

Polymorphism refers to the language's ability to use the correct method of a class based on the type of the variable at runtime and not at compile time. This way if you have a method defined in a super class and two child classes that override it, the language at runtime will execute the correct function code based on the type of the variable. Let's take an example to better understand:

class Animal {
  void move(){
    print('I am moving.');
  }
}

class Dog extends Animal {
  void move(){
    print('I am running.');
  }
}

class Shark extends Animal {
  void move(){
    print('I am swimming.');
  }
}
void main() {

  Animal myAnimal = new Animal();
  myAnimal.move();

  Animal myDog = new Dog();
  myDog.move();

  Animal myShark = new Shark();
  myShark.move();
}

If we execute the code it will print:

I am moving.
I am running.
I am swimming.

Dart based on the type of the variables myDog and myShark correctly run functions defined relatively in the Dog and Shark classes.

In this article, we have learned the 4 principles of OOP and wrote its implementations in Dart, in all object-oriented programming languages principles are the same, the only difference is how you code it.

Bye, Alberto

Comments (2)

Diego Carvalho's photo

Good article. I have a question. Why have you used 'this.' in front of properties on Animal class and not on Human class? Thanks

Alberto Bonacina's photo

Because I forgot it. To be precise, the 'this' keyword would only be needed in cases where the input variable and the local variable have the same name, and by using the 'this' keyword one can distinguish the local variable from the one passed as a parameter.

void setWeight(double age){ this.age = age; }

In which the assignment of the value of age is given to the local variable this.age. In the examples, I have reported there is no such problem because the local variable and the one passed as a parameter do not have the same name, so you can remove 'this' in all the code, and everything would still work correctly.