Chapters

Hide chapters

Dart Apprentice: Beyond the Basics

First Edition · Flutter · Dart 2.18 · VS Code 1.71

Dart Apprentice: Beyond the Basics

Section 1: 15 chapters
Show chapters Hide chapters

5. Interfaces
Written by Jonathan Sande

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.

Note: Business logic, which is sometimes called business rules or domain logic, refers to the essence of what your app does. The business logic of a calculator app would be the mathematical calculations themselves. Those calculations don’t depend on what your UI looks like or how you store the answers.

The following image shows an idealized app with the stable business logic in the middle and the more volatile infrastructure parts surrounding it:

Business Logic Web Frameworks File I/O 3rd Party Packages Database Shared Preferences UI

The UI shouldn’t communicate directly with the web. You also shouldn’t scatter direct calls to the database across your app. Everything goes through the central business logic. In addition to that, the business logic shouldn’t know any implementation details about the infrastructure.

This gives you a plug-in-style architecture, where you can swap one database framework for another and the rest of the app won’t even know anything changed. You could replace your mobile UI with a desktop UI, and the rest of the app wouldn’t care. This is useful for building scalable, maintainable and testable apps.

Communication Rules

Here’s 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 (IP) or Hypertext Transfer Protocol (HTTP). Those protocols are the rules for how communication happens among the users of the protocol.

When you create an interface in Dart, you define the rules for how one part of your codebase will communicate with another part. As long as both parts follow the interface rules, each part can change independently of the other. This makes your app much more manageable. In team settings, interfaces also allow different people to work on different parts of the codebase without worrying that they’re going to mess up someone else’s code.

Another related term you’ve probably heard before is API, or Application Programming Interface. An API is the public-facing set of methods that allow one program or code base to talk to another. Up to now, you’ve only been a consumer of other developers’ APIs. For example, you’ve been using the API that came with the Dart SDK every time you write Dart code. Or if you’ve experimented with Flutter, you might have used the Firebase API or some other third-party API that you got from a Pub package. You’ve come to the point now, though, where you’re ready to begin developing your own APIs.

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.

Business Logic Database Interface

The business logic doesn’t know anything about the database. It’s just talking to the interface. That means you could even swap out the database for a completely different form of storage, like cloud storage or file storage. The business logic doesn’t care.

There’s a famous adage related to this that goes, code against interfaces, not implementations. You define an interface, and then you code your app to use that interface only. While you must implement the interface with concrete classes, the rest of your app shouldn’t know anything about those concrete classes, only the interface.

Coding an Interface in Dart

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.

Creating an Abstract Interface Class

Say you want to make a weather app, and your business logic needs to get the current temperature in some city. Since those are the requirements, your Dart interface class would look like this:

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

Note that repository is a common term to call an interface that hides the details of how data is stored and retrieved. Also, the reason the result of fetchTemperature is nullable is that someone might ask for the temperature in a city that doesn’t exist.

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 use the implements keyword instead of the extends keyword.

Add the following concrete class:

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

Here are a couple of points to note:

  • Besides the benefits mentioned previously, another great advantage of using an interface is that you can create mock implementations to temporarily replace real implementations. In the FakeWebServer class, you’re simply returning a random number instead of going to all the work of contacting a real server. This allows you to have a “working” app until you get around to writing the code to contact the web server. This is also useful when you’re testing your code and you don’t want to wait for a real connection to the server.
  • Speaking of waiting for a web server, a real interface would return a type of Future<double?> instead of returning double? directly. However, you haven’t read Chapter 12, “Futures”, yet, so this example omits the Future part.

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();

You could potentially use the FakeWebServer implementation directly like so:

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

But this defeats the whole point of trying to keep the implementation details separate from the business logic. When you get around to swapping out the FakeWebServer with another class, you’ll have to go back and make updates at every place in your business logic that mentions it.

Adding a Factory Constructor

Do you remember learning about factory constructors Dart Apprentice: Fundamentals? If you do, you’ll recall that factory constructors can return subclasses. Add the following line to your interface class:

factory DataRepository() => FakeWebServer();

Your interface should look like this now:

abstract class DataRepository {
  factory DataRepository() => FakeWebServer();
  double? fetchTemperature(String city);
}

Since FakeWebServer is a subclass of DataRepository, the factory constructor is allowed to return it. The neat trick is that by using an unnamed constructor for the factory, you can make it look like it’s possible to instantiate the class now.

Write the following in main:

final repository = DataRepository();
final temperature = repository.fetchTemperature('Manila');

Ah, now your code on this side has no idea that that repository is actually FakeWebServer. When it comes time to swap in the real implementation, you only need to update the subclass returned by the factory constructor in the DataRepository interface.

Note: In the code above, you used a factory to return the concrete implementation of the interface. There are other options, though. Do a search for service locators (of which the get_it package is a good example) and dependency injection to learn more about this topic.

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.

This concept is key to the flexibility that Dart has as a language. The Dart VM implements the interface one way and gives you the ability to hot-reload your Flutter apps. The dart compile js tool implements the interface using JavaScript and gives you the ability to run your code on the web. The dart compile exe tool implements the interface on Windows or Linux or Mac to let you run your code on those platforms.

The implementation details are different for every platform, but you don’t have to worry about that because your code will only talk to the interface, not to the platform. Are you starting to see how powerful interfaces can be?

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, which is in contrast with other languages that allow multiple inheritance.

So the following is not allowed in Dart:

class MySubclass extends OneClass, AnotherClass {} // Not OK

However, you can implement more than one interface:

class MyClass implements OneClass, AnotherClass {} // OK

You can also combine extends and implements:

class MySubclass extends OneClass implements AnotherClass {}

But what’s the difference between just extending or implementing? That is, how are these two lines different:

class SomeClass extends AnotherClass {}
class SomeClass implements AnotherClass {}

When you extend AnotherClass, SomeClass has access to any logic or variables in AnotherClass. However, if SomeClass implements AnotherClass, SomeClass must provide its own version of all methods and variables in AnotherClass.

Example of Extending

Assume AnotherClass looks like the following:

class AnotherClass {
  int myField = 42;
  void myMethod() => print(myField);
}

You can extend it like this with no issue:

class SomeClass extends AnotherClass {}

Check that SomeClass objects have access to AnotherClass data and methods:

final someClass = SomeClass();
print(someClass.myField);      // 42
someClass.myMethod();          // 42

Run that and you’ll see 42 printed twice.

Example of Implementing

Using implements in the same way doesn’t work:

class SomeClass implements AnotherClass {} // Not OK

The implements keyword tells Dart that you only want the field types and method signatures. You’ll provide the concrete implementation details for everything yourself. How you implement it is up to you, as demonstrated in the following example:

class SomeClass implements AnotherClass {
  @override
  int myField = 0;

  @override
  void myMethod() => print('Hello');
}

Test that code again as before:

final someClass = SomeClass();
print(someClass.myField);      // 0
someClass.myMethod();          // Hello

This time you see your custom implementation results in 0 and Hello.

Challenges

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

Challenge 1: Fizzy Bottles

  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.

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.

Key Points

  • 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.
  • Dart only allows single inheritance on its classes.

Where to Go From Here?

Once you learn how to use a hammer, everything will look like a nail. Now that you know about abstract classes and interfaces, you might be tempted to use them all the time. Don’t over-engineer your apps, though. Start simple, and add abstraction as you need it.

Throughout the Dart Apprentice books, you’ve gotten a few ideas for writing clean code. However, the principles of building clean architecture take clean coding to a whole new level. You won’t master the skill all at once, but reading books and articles and watching videos on the subject will help you grow as a software engineer.

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.
© 2024 Kodeco Inc.