Chapter 7. Extending classes and interfaces

This chapter covers

  • Building class and interface inheritance hierarchies
  • Introducing the Object base class
  • Understanding the dynamic type annotation

Object-oriented programming allows for more than just encapsulating data and behavior in a class. It allows you to extend existing behavior defined elsewhere, which promotes great code reuse. Reusing existing code lets you write less new code in your apps and has the added benefit that existing code should also be tested code.

In this chapter, we’ll examine class and interface inheritance in Dart, including how to provide new functionality by overriding existing methods and properties and how to write abstract classes that you can use only as parents in an inheritance hierarchy. If you have written in Java or C#, this will be familiar. But those from a JavaScript background should note the absence of JavaScript’s prototypical inheritance mechanism.

We’ll explore the base Object class from which all other classes and types inherit, including strings, numbers, Boolean values, null, and your own custom classes. Object provides some standard methods and properties that you can override in your own classes, such as the toString() method. We’ll also look at the dynamic type, which Dart uses to enable optional typing.

In chapter 6, you declared a number of classes based around an imaginary AuthService example, which was used to provide authentication and authorization functionality to your enterprise app. When Alice provides her username and password to your app, the auth() function authorizes her details and returns a User type that the application uses. You’ll continue to use that example in this chapter by building on the User type to create a hierarchy of classes and interfaces for use with a simple AuthService. To recap, the abstract AuthService and the EnterpriseAuthService classes are defined in a library called logon_lib, as shown in figure 7.1.

Figure 7.1. The AuthService definition from the logon_lib example

You’ll use inheritance to extend the User type so the enterprise service can return an enterprise user with the functionality defined elsewhere.

 

Note

If you’re already familiar with Java or C#, you might want to skim this chapter, because Dart’s class inheritance mechanism is the same. Dart allows single-class inheritance with multiple interfaces. Check the “Remember” sections in each summary for the important concepts, and keep an eye out for noSuchMethod.

 

7.1. Extending classes with inheritance

Let’s start by defining a base class for the User type, which follows the same model as the one in the AuthService that we discussed in chapter 6. The following listing shows the User type.

Listing 7.1. A new User class

AuthService uses this class by returning an instance of the User class. For example, you can write User aUser = new User("Alice");, which creates an instance of the User class, initialized with the username "Alice". Your app can now use this variable to check new password validity and update the email address.

But your enterprise service needs more functionality in a User: for example, marking an account as expired and providing more robust password checking, such as confirming that the same password hasn’t been reused for the last five password changes. You can do this by using inheritance. Inheritance allows you to take an existing type, such as a User, and add more functionality to it by subclassing using the extends keyword. When inheriting a class, you can also specify new behavior for existing methods and properties by overriding them.

7.1.1. Class inheritance

When classes inherit each other, the subclass gains the functionality of the methods and properties of the parent class, known as the superclass. The subclass also shares an “is-an” relationship with all the parent classes going up the hierarchy.

Dart allows single inheritance, which means a class can extend from a single parent. But a single parent can have multiple children, and each child can have its own children, building up a hierarchy many classes deep.

In the example app, the user Alice will log on to the system and be represented by an instance of the EnterpriseUser class, returned from the enterprise authentication service’s auth() function. This class, which inherits from User, gains all the members of User by using the extends keyword. The representation of Alice as either a User or an EnterpriseUser will have the same functionality at the moment, provided by inheritance. This functionality works in an EnterpriseUser “is-a” User relationship. You can test the “is-an” relationship in Dart by using the is keyword. The is keyword allows you to examine the type of an object and get a Boolean value back in return, as shown in figure 7.2.

Figure 7.2. EnterpriseUser shares an “is-an” relationship with all the classes going up the hierarchy.

This feature provides a lot of flexibility for properties, types, and method parameters. If you know that your method will use only the features of the User base type, then you can specify the parameter type User but still pass in an EnterpriseUser. This approach allows others to reuse your code by passing in their own inherited version of User, safe in the knowledge that the method will still function correctly.

The following snippet shows that you can use an EnterpriseUser in the same way as a User object:

 

Note

EnterpriseUser’s implicit interface is also inherited from the superclass. The public interface of any object is the aggregate of all the public interfaces of inherited classes.

 

You can now add functionality that’s specific to EnterpriseUser, as shown in the next listing. This lets EnterpriseUser use all the existing functionality from User but also provide new functionality.

Listing 7.2. EnterpriseUser with additional functionality

7.1.2. Inheriting constructors

Although inheritance allows a child class to gain the existing functionality from the superclass’s methods and properties, the child class doesn’t inherit any constructors from the superclass. Figure 7.3 demonstrates some of the logical members of EnterpriseUser, which inherits the methods and properties from the User class.

Figure 7.3. A child class inherits all the existing functionality from the superclass except constructors.

This means if you wanted to use the constructor in the superclass, you’d need to write your own constructor in EnterpriseUser and use the super keyword to refer to the superclass’s constructor. You can do this in a special section of the constructor called the constructor initializer block, which appears between the constructor parameter list and the constructor body:

The constructor initializer is a block of code that Dart executes before the instance of the class has been created. This is the only place you can call super constructors; calling them in the constructor body isn’t allowed. You can also call any super constructor, including named constructors—the parameter list in your class doesn’t need to match the parameter list of the superclass. If the base class provides named super constructors, you can call any of them by using the superclass as the constructor name prefix. Likewise, a named constructor in the child class can call a superclass’s constructors at will:

The constructor initialization block is a comma-separated list of commands that can also be used to initialize final properties (as discussed in chapter 6), but any call to the super constructor must always come first.

7.1.3. Overriding methods and properties

When you inherit a parent class, you gain all the functionality of the members of the parent class. But sometimes you need to provide your own version of that functionality, such as when you want to be able to provide password validation in the EnterpriseUser class. The superclass User already has an isPasswordValid() method, and you get the functionality of that method when you inherit the class. But when Alice wants to change her password in your enterprise system, the requirements for changing a password are stricter than just comparing the new password with the old password. Alice can’t reuse the same password for five password changes, which means that in the EnterpriseUser class, you need to override the functionality provided by the parent User class: for example, to remember the last five password hashes. Fortunately, this is easy to do by providing a new method implementation with the same name and parameters, as shown in the following listing. It’s still possible to reuse the inherited functionality of the underlying isPasswordValid() method provided in the parent User class by using the super keyword again to refer to the same function in the base class, also in this listing.

Listing 7.3. EnterpriseUser overriding functionality from parent User class

You can override properties in a similar manner. A property is just shorthand for a getter or setter method, and the same principle applies. The User class provides a username property, but when Alice logs on to your enterprise system you want to validate the username to ensure that it’s longer than four characters. You can override the setter and getter by providing a new implementation of the getter and setter to perform this validation, as shown in the following listing.

Listing 7.4. logon.dart: overriding properties inherited from the parent class

So far, you’ve seen how Dart allows classes to inherit from a parent class, optionally reusing existing functionality from that parent class. But sometimes you’ll want to design a class and interface inheritance hierarchy in such a way that a user of your code must provide their own functionality, by using abstract classes.

7.1.4. Including abstract classes in a class hierarchy

You saw the abstract keyword in chapter 6, where you used it to define a class containing only method bodies, which you used as interface definitions. You can also use abstract classes to force users of your class to provide their own implementation methods, while still providing implementation methods of your own.

In the example system, you currently have two classes that form a hierarchy. When Alice logs on to the system on the developer’s system, she’s represented by the User class. When she logs on to the production system with the enterprise servers, she’s represented by the EnterpriseUser class, which shares an “is-an” relationship with its parent. This hierarchy is captured in figure 7.4.

Figure 7.4. The current inheritance hierarchy between your interfaces and classes

The system can create instances of the User class and the EnterpriseUser class because they’re both complete classes that fully define the functionality required by the interfaces they implement. Developers can use the User class independently of any of its child classes. But often when designing a library, you’ll want to force a developer using one of your classes to provide some of their own functionality, because you don’t know at design time what the functionality needs to be.

In the example User interface, you could add a checkPasswordHistory() function. This function allows implementing classes to check the password history as part of the isPasswordValid() function. Unfortunately, when designing the User class, you wouldn’t know how to check the password history. You can force users of the User class to provide that functionality by ensuring that they inherit the User class and override the checkPasswordHistory() function to provide their own functionality. To achieve this, use the abstract keyword when defining the User class, as shown in the next listing. The abstract keyword indicates that you can’t create a new instance of this class; instead, it allows child classes that inherit it to fully meet those requirements.

Listing 7.5. Making User an abstract class

One of the benefits of using an abstract class is that you can call methods that have yet to have their functionality defined, which passes the responsibility down to the user of your class. Now that you’ve made the User class abstract, EnterpriseUser needs to provide the functionality for checkPasswordHistory(). EnterpriseUser, which has an “is-a” relationship with the User class, also needs to either declare itself as abstract or fulfill the requirements defined by the User class. Figure 7.5 shows how the implementation is used when you call the isPasswordValid() method on an instance of the EnterpriseUser class.

Figure 7.5. Methods declared in interfaces can be implemented by child classes if the parent class is declared as abstract.

Abstract classes are great when you want to leave the implementation decisions to a user of your library but you also want to dictate the order in which something happens. In the isPasswordValid() example, you first validate the password hash and then call checkPasswordHistory(), because the call to the enterprise system might involve an external call and you can save resources by performing that step only if you’ve first validated the password.

There was a lot in this section, and you’ve learned how Dart provides inheritance with classes and interfaces. Object-oriented programming—and inheritance in particular—is a complex topic, and the best way to understand it is to experiment with the code and try to use some of the features in your own libraries. The built-in libraries from the Dart SDK are full of interfaces and inheritance; they’re open source and readily available in the Dart Editor. You can find a tour of some of the libraries in the appendix.

Now that we’ve covered inheritance, it’s time to examine the Object class, which is the parent for all classes in Dart regardless of whether your class explicitly extends it.

 

Remember

  • The extends keyword denotes that a class is inheriting (subclassing) another class.
  • The abstract keyword indicates that a class isn’t providing its own implementation of a method. Classes that inherit an abstract class should provide that functionality.
  • Subclasses don’t inherit a superclass’s constructor. You can call constructors in the parent class by using the super prefix to refer to them in the constructor initializer block.
  • You can also call specific methods and properties of the parent class by using the super prefix anywhere in normal code.

 

7.2. Everything is an object

Everything in Dart is an object, which differs from Java and C#, where you have primitive types such as int and boolean (and have object equivalents, such as java.lang.Integer). The Object class is built into the Dart language and is the ultimate parent class of every class other than itself.

When you create an instance of a variable in your app, whether it’s a String, an int, or your own class such as EnterpriseAuthService, you’re creating an instance of an Object. You can look at this in two ways. First, in object-oriented programming, you create instances of objects, and from a computer science point of view, objects are the areas of computer memory that are allocated to store actual data in a running app. This isn’t the same as in a class, which is the representation in a source code file that Dart uses to construct objects.

Second, Dart has an Object class from which every other class inherits automatically. This happens regardless of whether you use the extends keyword in your class definition. All the built-in classes and types such as String, int, null, and functions share an “is-an” relationship with Object.

7.2.1. Testing the “is-an” relationship with Object

Object inheritance creates an “is-an” relationship going up the hierarchy, and this “is-an” relationship can be tested in code, using the is keyword, which returns a Boolean true or false value. The following statements all return true:

You can use the “is-an” relationship with your own classes. When you create a new class, such as EnterpriseAuthService, you’re automatically inheriting from the base Object class. To create an “is-an” relationship with inheritance, use the extends keyword to define the parent of the class you’re inheriting. But this isn’t required with Object; you get the inheritance automatically without needing to use the extends keyword, as shown in figure 7.6.

Figure 7.6. Classes automatically extend the Object base class.

This inheritance works all the way down the inheritance hierarchy. When your EnterpriseUser inherits from User, it too inherits from the base Object class but via the User class. This inheritance happens because inheritance provides an “is-an” relationship up the class hierarchy, meaning that each child has an “is-an” relationship with every parent going up the hierarchy, as shown in figure 7.7.

Figure 7.7. All classes have an “is-an” relationship with the Object class.

The “is-an” relationship can be also be tested with your own classes by using the is keyword to return a true or false value. The following statements always return true:

 

Note

The “is-an” relationship works only one way (up the hierarchy, from the child to the parent), so calling print(Object is User); returns false because an Object isn’t a User.

 

You can observe two points when you have a single base class:

  • Every class has an “is-an” relationship with Object, meaning you can refer to everything as an object in variable and parameter declarations.
  • Every class inherits some basic functionality provided by the base Object class.

We’ll look at each of these in turn.

7.2.2. Using the “is-an” Object relationship

By having every type inherit from Object automatically, you can pass any type where an object is expected, including null, functions, and Boolean values; for example, the function doLogon(AuthService authService, String username, String password); from chapter 6 could be rewritten to explicitly check the type of the input parameters as follows:

Writing code like this removes the ability for the type checker to validate that the variables you’re passing in to your function are correct. The as keyword lets you tell the type checker that you’re expecting to treat a variable as a specific type. For example, you could rewrite the return line in the doLogon() function from the previous snippet as follows:

But it can be useful when you have a function that wants to take different types that don’t share another common interface or other common parent class. In this instance, it’s valid to declare the parameter as an Object and check the type in the function:

 

Warning

You should try to avoid using this pattern in your external-facing code, because it provides no useful documentation to users or tools. If you find yourself using this pattern, be sure to provide appropriate comments in the code to explain why.

 

Later in this chapter, we’ll look at the dynamic type annotation, which is the type that all variables and parameters have when you supply no type information—and which is subtly different from using Object. When you use Object, you explicitly state that you’re expecting that callers of your function can use any type. Specifying no type information (using dynamic typing), on the other hand, means you haven’t explicitly indicated which type can be used. By using Object in a parameter list, you document that you’re expecting users to be able to provide any type to the function or method.

7.2.3. Using toString() functionality inherited from the base Object class

The Object type provides a small number of methods and properties that all classes can use. The most commonly used one is the toString() method, which provides a string representation of the instance.

When you try to use an instance of any class as a string—for example, by passing it into Dart’s top-level print() function—the toString() method of Object gets called. The example in the following listing explicitly calls the toString() function, but if you don’t explicitly call toString(), Dart will call it implicitly.

Listing 7.6. Calling toString() outputs a textual representation of the object

Calling toString() on an Object is interesting. It outputs the text Instance of 'Object', which is as descriptive as it can be at that point. If you were to call toString() on an instance of User, you’d get the message Instance of 'User' because your class is using the functionality built into the Object class. If you called toString() on a different class, such as a list of numbers, the list of numbers would be printed rather than the text Instance of 'List' because the List class provides its own toString() method, which overrides the functionality provided by Object.

You can override this functionality in your own classes, too, by adding a toString() function to your class definition. For example, you can make your User class output the name of the user rather than use the default functionality provided by Object.toString(). Doing so provides a benefit if you add logging functionality, because you can pass the user instance into your logging function or the top-level print() function and get back a descriptive message. The next listing shows an example implementation that also uses the original functionality found in Object by explicitly calling super.toString().

Listing 7.7. Overriding the toString() functionality from Object

Subclasses of User, such as EnterpriseUser, also inherit this functionality if they don’t override it themselves. Calling toString() on an instance of the EnterpriseUser class will result in the output Instance of 'EnterpriseUser': Alice, which is far more informative than the default. Of course, you could always override toString() again in the EnterpriseUser class definition.

7.2.4. Intercepting noSuchMethod() calls

The Object class also exposes another useful method: noSuchMethod(). Unlike in Java and C# (but as in many other dynamic languages such as Ruby and Python), you aren’t restricted to using properties and methods that have been explicitly declared on a class you’re using.

When you call a method or access a property that doesn’t exist on a class definition, Dart first checks to see whether the method or property exists on any parent classes going up the hierarchy. If it doesn’t, Dart then tries to find a method called noSuchMethod() declared in the hierarchy. If that method hasn’t been explicitly defined, Dart calls the base Object.noSuchMethod() function, which throws a NoSuchMethodError.

You can use this feature with the User class hierarchy. When Alice logs on, you can’t always trust the data that comes back from EnterpriseService because of data inconsistencies in the source system. The agreement with the EnterpriseService team is that they will return as much data as they can, and you’ll validate that it meets your requirements. For this reason, no validate() method is exposed on any of the User or EnterpriseUser implementation classes, but you might sometimes want to call it nonetheless. Figure 7.8 shows how Dart handles this call.

Figure 7.8. Dart checks up the hierarchy for the method, which doesn’t exist. Then it checks up the hierarchy for a noSuchMethod() implementation until it finds the one declared in the base Object class.

When you call validate(), Dart—being optimistic and expecting that you, as a developer, know what you’re doing—will try to run it. In the end, Dart will find the default noSuchMethod() implementation in the base Object class and will throw an error, which can be caught with a try/catch handler, as shown in the following snippet:

try {
  user.validate("Alice");
on NoSuchMethodError catch (err) {
  // handle or ignore the error
}

You can prevent the NoSuchMethodError being thrown by implementing your own version of noSuchMethod(). This approach allows you to intercept the missing method call and execute some arbitrary code, such as validating the data.

noSuchMethod() takes two parameters: a String representing the name of the method you tried to access and a List containing a list of the parameters you passed into the method. We’ll discuss lists in more depth in the next chapter, but for now you need to know that lists are zero-based and that you can access a list’s elements by using the square bracket syntax familiar from many other languages. The following listing shows an example implementation of noSuchMethod(String name, List args). This example code prints out the method name and the number of arguments passed to the method.

Listing 7.8. Implementing noSuchMethod()

When you call user.validate("Alice");, it results in the string "validate, 1" being output to the console. Figure 7.9 demonstrates the new flow.

Figure 7.9. Now that noSuchMethod() has been defined, it’s found and executed.

It’s possible to check explicitly for method names and continue to throw the noSuchMethodError from the base class, if that’s required, by checking the value of the name parameter and calling super.noSuchMethod(name,args) to pass the call up the class hierarchy. This approach allows you to capture specific missing methods while ignoring others.

noSuchMethod() can also intercept a property access. Suppose you tried to access on the User a password field that didn’t exist. You might want to ignore the set and return a string of asterisks for the get. When noSuchMethod() receives a call for a property access, it prefixes the name field with either get: or set:, which lets you determine whether the getter or setter is being accessed. Thus calling print(user.password); can be handled by the following noSuchMethod() implementation:

7.2.5. Other default functionality of the Object class

The Object class also provides some behavior that other classes get by default, most notably the equals operator ==. In the next chapter we’ll discuss how to use this and other operators by overloading them in your own classes, but the default functionality is unsurprising.

The equals operator defined in the Object class returns a true/false value when you compare an instance of an object to another instance of an object. That is, when you compare two variables for equality, they return true if they’re the same instance, as shown in figure 7.10.

Figure 7.10. The equals operator from the Object class allows comparison of two instances of an object.

In the next chapter, we’ll look at how you override the default functionality of the equals and other operators, but first we need to examine the dynamic type. dynamic is the type Dart uses when you don’t specify any type information. Every instance of a variable has one, and it’s accessible explicitly from the Object class’s dynamic property.

 

Remember

  • Everything “is-an” object.
  • Object defines the toString() and noSuchMethod(name, args) methods, which you can override in your own classes.
  • noSuchMethod() can capture unknown method calls and property access.

 

7.3. Introducing the dynamic type

The final type we’ll look at in this section is dynamic. When you build your libraries, such as the logon_lib library that provides authentication services, it’s good practice to provide type information to other developers and to the tools. Doing so allows the developers and the tools to infer meaning from the type information you choose. In your library, or when prototyping, it’s perfectly valid to not specify any type information. When you don’t explicitly give any type information, such as when you use the var keyword to declare a variable, Dart uses a special type annotation called dynamic.

The dynamic type is similar in concept to the Object type, in that every type “is-a” dynamic type. As such, you can use the dynamic type annotation in variable and parameter declarations and as function return types. In terms of the language, specifying the dynamic type is the same as providing no type information; but when you do specify the dynamic type, you let other developers know that you made a decision for the dynamic type to be used rather than just not specifying it. We’ll look at this in more detail later in the chapter. First, figure 7.11 shows how dynamic is used automatically when you don’t provide other type information: the dynamic type is used on the left, and an explicit type, such as String, is used on the right.

Figure 7.11. Illustration of where Dart implies the dynamic type, compared with the equivalent strong typing

 

How does the “is-an” relationship with dynamic work?

Every type in Dart is an object, but every class also has an “is-an” relationship with dynamic, including Object. This is because dynamic is the base interface that every other class, including the Object class, implements. The dynamic interface provides no methods and properties, is hardcoded into the virtual machine, and, unlike Object, can’t be extended or inherited from.

 

7.3.1. Using the dynamic type annotation

You can use the dynamic keyword, which identifies the dynamic type, explicitly in place of other type information; it’s typically used where you want to indicate that you have explicitly decided to not provide any type information. This usage is subtly different from that of Object, which is used when you’ve explicitly decided to allow any object. When reading other people’s code, you can make the interpretations shown in table 7.1 based on their choice of Object or dynamic.

Table 7.1. How to interpret different uses of the Object and dynamic types

Example function declaration

What you can infer

doLogon(Object user); Developer made an active decision to allow any instance of an object to be passed into this function
doLogon(dynamic user); Developer made an active decision that they don’t yet know what type of object should be passed into this function
doLogon(username); Developer hasn’t yet declared what type of object should be passed into this function (implicitly dynamic)
doLogon(User user); Developer actively declared that a User should be passed into this function

In practice, though, avoid using the dynamic keyword explicitly, unless you provide adequate code comments explaining your decision to use dynamic. As with all rules, there’s an exception, which we’ll look at in more depth when we start to use generics in chapter 8.

7.4. Summary

In this chapter, we looked at Dart’s inheritance, which allows you to build inheritance hierarchies of classes. Object is the parent class of every other class and type, including the built-in ones such as String, int, and null. The dynamic type, on the other hand, is the type Dart uses at runtime to allow code to run without type annotations affecting the executing code.

 

Remember

  • Use the extends keyword to declare that a class inherits a parent class.
  • The super keyword lets you call members (methods and properties) on a parent class.
  • You can override specific members by providing your own implementations.
  • Object provides the toString() method, which you can use to provide extra information when outputting log messages.
  • noSuchMethod() from the base Object class can be used to intercept missing methods and properties.
  • The dynamic type annotation represents the untyped version of variables and parameters in Dart.

 

We aren’t quite finished with classes! The next chapter introduces generics with a discussion of Dart’s collection classes such as Collection, List, and Map. You’ll also learn how to create flexible, general-purpose classes by implementing generics yourself. You’ll also discover operator overloading, which helps you create truly self-documenting code by customizing the meaning built into the standard operators.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.11.227