Home Flutter Books Dart Apprentice

9
Advanced Classes

Chapter 6 covered a lot of the foundational elements of classes, but now it’s time to extend that knowledge. Object-oriented programming has captured developers’ imaginations for decades. Just as the name class is inspired by biological notation, the true beauty of object-oriented programming is how you’re able to elegantly build connective tissue between your classes. In this chapter you’ll learn to use tools such as inheritance, interfaces and mixins to move beyond simple coding and enter a world of software design.

Extending classes

In many situations, you’ll need to create a hierarchy of classes that share some base functionality. You can create your own hierarchies by extending classes. This is also called inheritance, because the classes form a tree in which child classes inherit from parent classes. The parent and child classes are also called super classes and subclasses respectively. Since all Dart types derive from the Object type, Object forms the root of all class hierarchies.

Creating your first subclass

To see how inheritance works, you’ll create your own hierarchy of classes. In a little while, you’ll make a Student class which needs grades, so first make a Grade enum:

enum Grade { A, B, C, D, F }

Creating similar classes

Next create two classes named Person and Student like so:

class Person {
  Person(this.givenName, this.surname);
  String givenName;
  String surname;
  String get fullName => '$givenName $surname';
  String toString() => fullName;
}

class Student {
  Student(this.givenName, this.surname);
  String givenName;
  String surname;
  var grades = <Grade>[];
  String get fullName => '$givenName $surname';
  String toString() => fullName;
}

Subclassing to remove code duplication

You can remove the duplication between Student and Person by making Student extend Person. You do so by adding extends Person after the class name, and removing everything but the Student constructor and the grades list.

class Student extends Person {
  Student(String givenName, String surname)
    : super(givenName, surname);
  var grades = <Grade>[];
}

Calling super last in an initializer list

As a quick side note, if you use an initializer list, the call to super always goes last, that is, after assert statements and initializers. You can see the order in the following example:

class SomeChild extends SomeParent {

  SomeChild(double height)
      : assert(height != 0),  // assert
        _height = height,     // initializer
        super();              // super

  final double _height;
}

Using the classes

OK, back to the primary example. Create Person and Student objects like so:

final jon = Person('Jon', 'Snow');
final jane = Student('Jane', 'Snow');
print(jon.fullName);
print(jane.fullName);
Jon Snow
Jane Snow
final historyGrade = Grade.B;
jane.grades.add(historyGrade);

Overriding parent methods

Suppose you want the student’s full name to print out differently than the default way it’s printed in Person. You can do so by overriding the fullName getter. Add the following two lines to the bottom of the Student class:

@override
String get fullName => '$surname, $givenName';
Jon Snow
Snow, Jane

Calling super from an overridden method

As another aside, sometimes you override methods of the parent class because you want to add functionality, rather than replace it, as you did above. In that case, you usually make a call to super either at the beginning or end of the overridden method.

class SomeParent {
  void doSomeWork() {
    print('parent working');
  }
}

class SomeChild extends SomeParent {
  @override
  void doSomeWork() {
    super.doSomeWork();
    print('child doing some other work');
  }
}
final child = SomeChild();
child.doSomeWork();
parent working
child doing some other work
@override
void doSomeWork() {
  print('child doing some other work');
  super.doSomeWork();
}

Multi-level hierarchy

Back to the primary example again. Add more than one level to your class hierarchy by defining a class that extends from Student.

class SchoolBandMember extends Student {
  SchoolBandMember(String givenName, String surname)
    : super(givenName, surname);
  static const minimumPracticeTime = 2;
}

Sibling classes

Create a sibling class to SchoolBandMember named StudentAthlete that also derives from Student.

class StudentAthlete extends Student {
  StudentAthlete(String givenName, String surname)
    : super(givenName, surname);
  bool get isEligible =>
    grades.every((grade) => grade != Grade.F);
}
final jessie = SchoolBandMember('Jessie', 'Jones');
final marty = StudentAthlete('Marty', 'McFly');

Visualizing the hierarchy

Here’s what your class hierarchy looks like now:

Type inference in a mixed list

Since Jane, Jessie and Marty are all students, you can put them into a list.

final students = [jane, jessie, marty];

Checking an object’s type at runtime

You can use the is and is! keywords to check whether a given object is or is not within the direct hierarchy of a class. Write the following code:

print(jessie is Object);
print(jessie is Person);
print(jessie is Student);
print(jessie is SchoolBandMember);
print(jessie is! StudentAthlete);

Prefer composition over inheritance

Now that you know about inheritance, you may feel ready to conquer the world. You can model anything as a hierarchy. Experience, though, will teach you that deep hierarchies are not always the best choice.

class Student {
  List<Role> roles;
}

Mini-exercises

  1. Create a class named Fruit with a String field named color and a method named describeColor, which uses color to print a message.
  2. Create a subclass of Fruit named Melon and then create two Melon subclasses named Watermelon and Cantaloupe.
  3. Override describeColor in the Watermelon class to vary the output.

Abstract classes

The classes and subclasses you created in the last section were concrete classes. It’s not that they’re made of cement; it just means that you can make actual objects out of them. That’s in contrast to abstract classes, from which you can’t make objects.

Creating your own abstract classes

Have a go at working this out in Dart now. Without venturing too far into the fringes of how taxonomists make their decisions, create the following Animal class:

abstract class Animal {
  bool isAlive = true;
  void eat();
  void move();

  @override
  String toString() {
    return "I'm a $runtimeType";
  }
}

Can’t instantiate abstract classes

As mentioned, you can’t create an instance of an abstract class. See for yourself by writing the following line:

final animal = Animal();
Abstract classes can’t be instantiated.
Try creating an instance of a concrete subtype.

Concrete subclass

Create a concrete Platypus now. Stop thinking about cement. Just add the following empty class to your IDE below your Animal class:

class Platypus extends Animal {}

Adding the missing methods

You could write the methods yourself, but VS Code gives you a shortcut. Put your cursor on Platypus and press Command+. on a Mac or Control+. on a PC. You’ll see the following pop-up:

class Platypus extends Animal {
  @override
  void eat() {
    // TODO: implement eat
  }

  @override
  void move() {
    // TODO: implement move
  }
}

Filling in the TODOs

Since this is a concrete class, it needs to provide the actual implementation of the eat and move methods. In the eat method body, add the following line:

print('Munch munch');
print('Glide glide');
void layEggs() {
  print('Plop plop');
}

Testing the results

Test your code out now in main:

final platypus = Platypus();
print(platypus.isAlive);
platypus.eat();
platypus.move();
platypus.layEggs();
print(platypus);
true
Munch munch
Glide glide
Plop plop
I'm a Platypus

Treating concrete classes as abstract

There is one more interesting thing to do before moving on. In the line where you declared platypus, hover your cursor over the variable name:

Animal platypus = Platypus();

// platypus.layEggs();
I'm a Platypus

Interfaces

Interfaces are similar to abstract classes in that they let you define the behavior you expect for all classes that implement the interface. They’re a means of hiding the implementation details of the concrete classes from the rest of your code. Why is that important? To answer that question it’s helpful to understand a little about architecture. Not the Taj Mahal kind of architecture, software architecture.

Software architecture

When you’re building an app, your goal should be to keep core business logic separate from infrastructure like the UI, database, network and third-party packages. Why? The core business logic doesn’t change frequently, while the infrastructure often does. Mixing unstable code with stable would cause the stable code to become unstable.

Communication rules

Here is where interfaces come in. An interface is a description of how communication will be managed between two parties. A phone number is a type of interface. If you want to call your friend, you have to dial your friend’s phone number. Dialing a different number won’t work. Another word for interface is protocol, as in Internet Protocol or Hypertext Transfer Protocol. Those protocols are the rules for how communication happens among the users of the protocol.

Separating business logic from infrastructure

In the image below, you can see the interface is between the business logic and the code for accessing the database.

Creating an interface

There’s no interface keyword in Dart. Instead, you can use any class as an interface. Since only the field and method names are important, most interfaces are made from abstract classes that contain no logic.

abstract class DataRepository {
  double fetchTemperature(String city);
}

Implementing the interface

The Dart class above was just a normal abstract class, like the one you made earlier. However, when creating a concrete class to implement the interface, you must use the implements keyword instead of the extends keyword.

class FakeWebServer implements DataRepository {
  @override
  double fetchTemperature(String city) {
    return 42.0;
  }
}

Using the interface

How do you use the interface on the business logic side? Remember that you can’t instantiate an abstract class, so this won’t work:

final repository = DataRepository();
final DataRepository repository = FakeWebServer();
final temperature = repository.fetchTemperature('Berlin');

Adding a factory constructor

Do you remember factory constructors from Chapter 6? If you do, you’ll recall that factory constructors can return subclasses. Add the following line to your interface class:

factory DataRepository() => FakeWebServer();
abstract class DataRepository {
  factory DataRepository() => FakeWebServer();
  double fetchTemperature(String city);
}
final repository = DataRepository();
final temperature = repository.fetchTemperature('Manila');

Interfaces and the Dart SDK

If you browse the Dart source code, which you can do by Command or Control-clicking Dart class names like int or List or String, you’ll see that Dart makes heavy use of interfaces to define its API. That allows the Dart team to change the implementation details without affecting developers. The only time developers are affected is when the interface changes.

Extending vs implementing

There are a couple of differences between extends and implements. Dart only allows you to extend a single superclass. This is known as single inheritance, in contrast with other languages that allow multiple inheritance.

class MySubclass extends OneClass, AnotherClass {} // Not OK
class MyClass implements OneClass, AnotherClass {} // OK
class MySubclass extends OneClass implements AnotherClass {}
class SomeClass extends AnotherClass {}
class SomeClass implements AnotherClass {}

Example of extending

Assume AnotherClass looks like the following:

class AnotherClass {
  int myField = 42;
  void myMethod() => print(myField);
}
class SomeClass extends AnotherClass {}
final someClass = SomeClass();
print(someClass.myField);      // 42
someClass.myMethod();          // 42

Example of implementing

Using implements in the same way doesn’t work:

class SomeClass implements AnotherClass {} // Not OK
class SomeClass implements AnotherClass {
  @override
  int myField = 0;

  @override
  void myMethod() => print('Hello');
}
final someClass = SomeClass();
print(someClass.myField);      // 0
someClass.myMethod();          // Hello

Mini-exercises

  1. Create an interface called Bottle and add a method to it called open.
  2. Create a concrete class called SodaBottle that implements Bottle and prints “Fizz fizz” when open is called.
  3. Add a factory constructor to Bottle that returns a SodaBottle instance.
  4. Instantiate SodaBottle by using the Bottle factory constructor and call open on the object.

Mixins

Mixins are an interesting feature of Dart that you might not be familiar with, even if you know other programming languages. They’re a way to reuse methods or variables among otherwise unrelated classes.

Problems with extending and implementing

Think back to the Animal examples again. Say you’ve got a bunch of birds, so you’re carefully planning an abstract class to represent them. Here’s what you come up with:

abstract class Bird {
  void fly();
  void layEggs();
}
class Robin extends Bird {
  @override
  void fly() {
    print('Swoosh swoosh');
  }

  @override
  void layEggs() {
    print('Plop plop');
  }
}

Mixing in code

To make a mixin, you take whatever concrete code you want to share with different classes, and package it in its own special mixin class.

mixin EggLayer {
  void layEggs() {
    print('Plop plop');
  }
}

mixin Flyer {
  void fly() {
    print('Swoosh swoosh');
  }
}
class Robin extends Bird with EggLayer, Flyer {}
class Platypus extends Animal with EggLayer {
  @override
  void eat() {
    print('Munch munch');
  }

  @override
  void move() {
    print('Glide glide');
  }
}
final platypus = Platypus();
final robin = Robin();
platypus.layEggs();
robin.layEggs();

Mini-exercises

  1. Create a class called Calculator with a method called sum that prints the sum of any two integers you give it.
  2. Extract the logic in sum to a mixin called Adder.
  3. Use the mixin in Calculator.

Extension methods

Up to this point in the chapter, you’ve been writing your own classes and methods. Often, though, you use other people’s classes when you’re programming. Those classes may be part of a core Dart library, or they may be from packages that you got off Pub. In either case, you don’t have the ability to modify them at will.

Extension syntax

To make an extension, you use the following syntax:

extension on SomeClass {
  // your custom code
}
extension YourExtensionName on ClassName {
  // your custom code
}

String extension example

Did you ever make secret codes when you were a kid, like a=1, b=2, c=3, and so on? For this example, you’re going to make an extension that will convert a string into a secret coded message. Then you’ll add another extension method to decode it.

Solving in the normal way

First, solve the problem as you would with a normal function.

String encode(String input) {
  final output = StringBuffer();
  for (int codePoint in input.runes) {
    output.writeCharCode(codePoint + 1);
  }
  return output.toString();
}
final original = 'abc';
final secret = encode(original);
print(secret);

Converting to an extension

The next step is to convert the encode function above to an extension so that you can use it like so:

final secret = 'abc'.encoded;
extension on String {
  String get encoded {
    final output = StringBuffer();
    for (int codePoint in runes) {
      output.writeCharCode(codePoint + 1);
    }
    return output.toString();
  }
}
final secret = 'abc'.encoded;
print(secret);

Adding a decode extension

Add the decoded method inside the body of the String extension as well:

String get decoded {
  final output = StringBuffer();
  for (int codePoint in runes) {
    output.writeCharCode(codePoint - 1);
  }
  return output.toString();
}

Refactoring to remove code duplication

Refactor your String extension by replacing the entire extension with the following:

extension on String {
  String get encoded {
    return _code(1);
  }
  String get decoded {
    return _code(-1);
  }
  String _code(int step) {
    final output = StringBuffer();
    for (int codePoint in runes) {
      output.writeCharCode(codePoint + step);
    }
    return output.toString();
  }
}

Testing the results

To make sure that everything works, test both methods like so:

final original = 'I like extensions!';
final secret = original.encoded;
final revealed = secret.decoded;
print(secret);
print(revealed);
J!mjlf!fyufotjpot"
I like extensions!

int extension example

Here’s an example for an extension on int.

extension on int {
  int get cubed {
    return this * this * this;
  }
}
print(5.cubed);

Enum extension example

Dart enums, which you learned about in Chapter 4, are pretty basic in themselves. However, with the power of extensions, you can do much more with them.

enum ProgrammingLanguage { dart, swift, javaScript }
extension on ProgrammingLanguage {
  bool get isStronglyTyped {
    switch (this) {
      case ProgrammingLanguage.dart:
      case ProgrammingLanguage.swift:
        return true;
      case ProgrammingLanguage.javaScript:
        return false;
      default:
        throw Exception('Unknown Programming Language $this');
    }
  }
}
final language = ProgrammingLanguage.dart;
print(language.isStronglyTyped);

Challenges

Before moving on, here are some challenges to test your knowledge of advanced classes. It’s best if you try to solve them yourself, but solutions are available if you get stuck. These are available with the supplementary materials for this book.

Challenge 1: Heavy monotremes

Dart has a class named Comparable, which is used by the the sort method of List to sort its elements. Add a weight field to the Platypus class you made in this lesson. Then make Platypus implement Comparable so that when you have a list of Platypus objects, calling sort on the list will sort them by weight.

Challenge 2: Fake notes

Design an interface to sit between the business logic of your note-taking app and a SQL database. After that, implement a fake database class that will return mock data.

Challenge 3: Time to code

Dart has a Duration class for expressing lengths of time. Make an extension on int so that you can express a duration like so:

final timeRemaining = 3.minutes;

Key points

  • A subclass has access to the data and methods of its parent class.
  • You can create a subclass of another class by using the extends keyword.
  • A subclass can override its parent’s methods or fields to provide custom behavior.
  • Dart only allows single inheritance on its classes.
  • Abstract classes define class members and may or may not contain concrete logic.
  • Abstract classes can’t be instantiated.
  • One rule of clean architecture is to separate business logic from infrastructure logic like the UI, storage, third-party packages and the network.
  • Interfaces define a protocol for code communication.
  • Use the implements keyword to create an interface.
  • Mixins allow you to share code between classes.
  • Extension methods allow you to give additional functionality to classes that are not your own.

Where to go from here?

To see how to use interfaces in the context of building a real app, check out the raywenderlich.com article State Management with Provider.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.