Book Image

Mastering Dart

By : Sergey Akopkokhyants
Book Image

Mastering Dart

By: Sergey Akopkokhyants

Overview of this book

Table of Contents (19 chapters)
Mastering Dart
Credits
About the Author
About the Reviewers
www.PacktPub.com
Preface
Index

Classes and mixins


We all know its wasteful trying to reinvent the wheel. It's even more wasteful trying to do it each time we want to build a car. So how can a program code be written more efficiently and made reusable to help us develop more powerful applications? In most cases, we turn to the OOP paradigm when trying to answer this question. OOP represents the concept of objects with data fields and methods that act on that data. Programs are designed to use objects as instances of classes that interact with each other to organize functionality.

Types

The Dart language is dynamically typed, so we can write programs with or without the type annotations in our code. It's better to use the type annotations for the following reasons:

  • The type annotations enable early error detection. The static analyzer can warn us about the potential problems at the points where you've made the mistakes.

  • Dart automatically converts the type annotations into runtime assertion checks. In the checked mode, the dynamic type assertions are enabled and it can catch some errors when types do not match.

  • The type annotations can improve the performance of the code compiled in JavaScript.

  • They can improve the documentation making it much easier to read the code.

  • They can be useful in special tools and IDE such as the name completion.

The fact that the type annotations were not included in our code does not prevent our program from running. The variables without the type annotations have a dynamic type and are marked with var or dynamic. Here are several recommendations where the type annotations are appropriate:

  • You should add types to public and private variables

  • You can add types to parameters of methods and functions

  • You should avoid adding types to the bodies of methods or functions

Classes

In the real world, we find many individual objects, all of the same kind. There are many cars with the same make and model. Each car was built from the same set of blueprints. All of them contain the same components and each one is an instance of the class of objects known as Car, as shown in the following code:

library car;

// Abstract class [Car] can't be instantiated.
abstract class Car {
  // Color of the car.
String color;
  // Speed of the car.
  double speed;
  // Carrying capacity
  double carrying;

  // Create new [Car] with [color] and [carrying] info.
  Car(this.color, this.carrying);

  // Move car with [speed]
  void move(double speed) {
    this.speed = speed;
  }

  // Stop car.
  void stop() {
    speed = 0.0;
  }
}

Objects have methods and instance variables. The color, speed, and carrying are instance variables. All of them have the value null as they were not initialized. The instance methods move and stop provide the behavior for an object and have access to instance variables and the this keyword. An object may have getters and setters—special methods with the get and set keywords that provide read and write access to the instance variables. The Car class is marked with the abstract modifier, so we can't create an instance of this class, but we can use it to define common characteristics and behaviors for all the subclasses.

Inheritance

Different kinds of objects can have different characteristics that are common with others. Passenger cars, trucks, and buses share the characteristics and behaviors of a car. This means that different kinds of cars inherit the commonly used characteristics and behaviors from the Car class. So, the Car class becomes the superclass for all the different kinds of cars. We allow passenger cars, trucks, and buses to have only one direct superclass. A Car class can have unlimited number of subclasses. In Dart, it is possible to extend from only one class. Every object extends by default from an Object class:

library passenger_car;

import 'car.dart';

// Passenger car with trailer.
class PassengerCar extends Car {
  // Max number of passengers.
  int maxPassengers;

  // Create [PassengerCar] with [color], [carrying] and [maxPassengers].
  PassengerCar(String color, double carrying, this.maxPassengers) :
    super(color, carrying);
}

The PassengerCar class is not an abstract and can be instantiated. It extends the characteristics of the abstract Car class and adds the maxPassengers variable.

Interface

Each Car class defines a set of characteristics and behaviors. All the characteristics and behaviors of a car define its interface—the way it interacts with the outside world. Acceleration pedal, steering wheel, and other things help us interact with the car through its interface. From our perspective, we don't know what really happens when we push the accelerator pedal, we only see the results of our interaction. Classes in Dart implicitly define an interface with the same name as the class. Therefore, you don't need interfaces in Dart as the abstract class serves the same purpose. The Car class implicitly defines an interface as a set of characteristics and behaviors.

If we define a racing car, then we must implement all the characteristics and behaviors of the Car class, but with substantial changes to the engine, suspension, breaks, and so on:

import 'car.dart';
import 'passenger_car.dart';

void main() {
  // Create an instance of passenger car of white color,
  // carrying 750 kg and max passengers 5.
  Car car = new PassengerCar('white', 750.0, 5);
  // Move it
  car.move(100.0);
}

Here, we just created an instance of PassengerCar and assigned it to the car variable without defining any special interfaces.

Mixins

Dart has a mixin-based inheritance, so the class body can be reused in multiple class hierarchies, as shown in the following code:

library trailer;

// The trailer
class Trailer {
  // Access to car's [carrying] info
  double carrying = 0.0;

  // Trailer can carry [weight]
  void carry(double weight) {
    // Car's carrying increases on extra weight.
    carrying += weight;
  }
}

The Trailer class is independent of the Car class, but can increase the carrying weight capacity of the car. We use the with keyword followed by the Trailer class to add mixin to the PassengerCar class in the following code:

library passenger_car;

import 'car.dart';
import 'trailer.dart';

// Passenger car with trailer.
class PassengerCar extends Car with Trailer {
  // Max number of passengers.
  int maxPassengers = 4;

  /**
   * Create [PassengerCar] with [color], [carrying] and [maxPassengers].
   * We can use [Trailer] to carry [extraWeight].
   */
  PassengerCar(String color, double carrying, this.maxPassengers,
      {double extraWeight:0.0}) : super(color, carrying) {
    // We can carry extra weight with [Trailer]
    carry(extraWeight);
  }
}

We added Trailer as a mixin to PassengerCar and, as a result, PassengerCar can now carry more weight. Note that we haven't changed PassengerCar itself, we've only extended its functionality. At the same time, Trailer can be used in conjunction with the Truck or Bus classes. A mixin looks like an interface and is implicitly defined via a class declaration, but has the following restrictions:

  • It has no declared constructor

  • The superclass of a mixin can only be an Object

  • They do not contain calls to super

Well-designed classes

What is the difference between well-designed and poorly-designed classes? Here are the features of a well-designed class:

  • It hides all its implementation details

  • It separates its interface from its implementation through the use of abstract classes

  • It communicates with other classes only through their interfaces

All the preceding properties lead to encapsulation. It plays a significant role in OOP. Encapsulation has the following benefits:

  • Classes can be developed, tested, modified, and used independently

  • Programs can be quickly developed because classes can be developed in parallel

  • Class optimization can be done without affecting other classes

  • Classes can be reused more often because they aren't tightly coupled

  • Success in the development of each class leads to the success of the application

All our preceding examples include public members. Is that right? So what is the rule that we must follow to create well-designed classes?

To be private or not

Let's follow the simple principles to create a well-designed class:

  • Define a minimal public API for the class. Private members of a class are always accessible inside the library scope so don't hesitate to use them.

  • It is not acceptable to change the level of privacy of the member variables from private to public to facilitate testing.

  • Nonfinal instance variables should never be public; otherwise, we give up the ability to limit the values that can be stored in the variable and enforce invariants involving the variable.

  • The final instance variable or static constant should never be public when referring to a mutable object; otherwise, we restrict the ability to take any action when the final variable is modified.

  • It is not acceptable to have the public, static final instance of a collection or else, the getter method returns it; otherwise, we restrict the ability to modify the content of the collection.

The last two principles can be seen in the following example. Let's assume we have a Car class with defined final static list of parts. We can initialize them with Pedal and Wheel, as shown in the following code:

class Car {
  // Be careful with that code !!!
static final List PARTS = ['Pedal', 'Wheel'];
}
void main() {
  print('${Car.PARTS}'); // Print: [Pedal, Wheel]

  // Change part
  Car.PARTS.remove('Wheel');
  print('${Car.PARTS}'); // Print: [Pedal]
}

However, there's a problem here. While we can't change the actual collection variable because it's marked as final, we can still change its contents. To prevent anyone from changing the contents of the collection, we change it from final to constant, as shown in the following code:

class Car {
  // This code is safe
  static const List PARTS = const ['Pedal', 'Wheel'];
}

void main() {
  print('${Car.PARTS}'); // Print: [Pedal, Wheel]

  // Change part
  Car.PARTS.remove('Wheel');
  print('${Car.PARTS}');
}

This code will generate the following exception if we try to change the contents of PARTS:

Unhandled exception:
Unsupported operation: Cannot modify an immutable array
#0 List.remove (dart:core-patch/array.dart:327)
…

Variables versus the accessor methods

In the previous section, we mentioned that nonfinal instance variables should never be public, but is this always right? Here's a situation where a class in our package has a public variable. In our Car class, we have a color field and it is deliberately kept as public, as shown in the following code:

// Is that class correct?
class Car {
  // Color of the car.
  String color;
}

If the Car class is accessible only inside the library, then there is nothing wrong with it having public fields, because they don't break the encapsulation concept of the library.

Inheritance versus composition

We defined the main rules to follow and create a well-designed class. Everything is perfect and we didn't break any rules. Now, it's time to use a well-designed class in our project. First, we will create a new class that extends the current one. However, that could be a problem as inheritance can break encapsulation.

It is always best to use inheritance in the following cases:

  • Inside the library, because we control the implementation and relationship between classes

  • If the class was specifically designed and documented to be extended

It's better not to use inheritance from ordinary classes because it's dangerous. Let's discuss why. For instance, someone developed the following Engine class to start and stop the general purpose engine:

// General purpose Engine
class Engine {
  // Start engine
  void start() {
     // ...
  }

  // Stop engine
  void stop() {
    // ...
  }
}

We inherited the DieselEngine class from the Engine class and defined when to start the engine that we need to initialize inside the init method, as shown in the following code:

import 'engine.dart';

// Diesel Engine
class DieselEngine extends Engine {
  DieselEngine();

  // Initialize engine before start
  void init() {
    // ...
  }
  void start() {
   // Engine must be initialized before use
   init();
   // Start engine
   super.start();
  }
}

Then, suppose someone changed their mind and decided that the implementation Engine must be initialized and added the init method to the Engine class, as follows:

// General purpose Engine
class Engine {
  // Initialize engine before start
  void init() {
    // ...
  }

  // Start engine
  void start() {
    init();
  }

  // Stop engine
  void stop() {
    // ...
  }
}

As a result, the init method in DieselEngine overrides the same method from the Engine superclass. The init method in the superclass is an implementation detail. The implementation details can be changed many times in future from release to release. The DieselEngine class is tightly-coupled with and depends on the implementation details of the Engine superclass. To fix this problem, we can use a different approach, as follows:

import 'engine.dart';

// Diesel Engine
class DieselEngine implements Engine {
  Engine _engine;

  DieselEngine() {
    _engine = new Engine();
  }

  // Initialize engine before start
  void init() {
    // ...
  }

  void start() {
    // Engine must be initialized before use
    init();
    // Start engine
    _engine.start();
  }

  void stop() {
    _engine.stop();
  }
}

We created the private engine variable in our DieselEngine class that references an instance of the Engine class. Engine now becomes a component of DieselEngine. This is called a composition. Each method in DieselEngine calls the corresponding method in the Engine instance. This technique is called forwarding, because we forward the method's call to the instance of the Engine class. As a result, our solution is safe and solid. If a new method is added to Engine, it doesn't break our implementation.

The disadvantages of this approach are associated performance issues and increased memory usage.