Chapter 4. Utilizing object orientation

 

In this chapter

  • Object initialization
  • Abstract methods
  • Composition and Inheritance
  • Abstract interfaces
  • Type inference and public APIs

 

Scala is a rich object-oriented language. In Scala, every value is an object. Even operators are method calls against the class of an object. Scala offers mixin inheritance through the use of traits. Objects are core to everything in Scala, and understanding the details of how they work is important for using Scala.

Object, class, and traits are used to define public APIs for libraries. The initialization, comparison, and composition of objects are the bread and butter of Scala development. Initialization is important because of mixin inheritance and the way objects get instantiated in various locations. Comparing two objects for equality is critical and can be made trickier when inheritance gets in the mix. Finally, composition of functionality is how code reuse is accomplished, and Scala provides a few new ways to compose objects.

4.1. Limit code inside an object or trait’s body to initialization logic

In Scala, the code inside an object, trait or class’s body is the constructor.

A common starting point for most developers learning Scala is the standard “Hello, World” program. You’ll see many examples on the internet with the following code:

Listing 4.1. Poor Hello World! example in Scala
object Test extends Application {
  println("Hello, World!")
}

Although elegant, this code sample is misleading in its simplicity. The Application trait uses a nifty trick to simplify creating a new application but comes with a price. Let’s look at a simplified version of the Application trait in the following listing.

Listing 4.2. Application trait
trait Application {
  def main(args: Array[String]): Unit = {}
}

That’s it. That one empty method is all that’s needed for the Application trait. Why does this work? Let’s dig into the bytecode.

When compiling traits, Scala creates an interface/implementation pair of classes. The interface is for JVM interoperability and the implementation is a set of static methods that can be used by classes implementing the trait. When compiling the Test object, a main method is created that forwards to the Application implementation class. Although this method is empty, the logic inside the Test object is placed in the Test object’s constructor. Next, Scala creates “static forwarders” for the object. One of these static forwarder methods will be the main method, in the signature the JVM expects. The static forwarder will call the method on the singleton instance of the Test object. This instance is constructed in a static initialization block. And finally, we get to the issue. Code inside a static initialization block isn’t eligible for HotSpot optimization. In fact, in older versions of the JVM, methods called from a static initialization block wouldn’t be optimized either. In the most recent benchmarks, this has been corrected, such that only the static block itself isn’t optimized.

Scala 2.9 provides a better solution: the DelayedInit trait.

4.1.1. Delayed construction

Scala 2.9 provides a new mechanism for dealing with constructors. DelayedInit is a marker trait for the compiler. When implementing a class that extends DelayedInit, the entire constructor is wrapped into a function and passed to the delayedInit method. Let’s a look at the DelayedInit trait.

trait DelayedInit {
  def delayedInit(x: => Unit): Unit
}

The trait has one method, delayedInit. As stated before, this method has a function object passed to it. This function contains all the regular constructor logic, which provides a clean solution to the Application trait. Let’s implement your own to demonstrate the DelayedInit behavior.

trait App extends DelayedInit {
  var x: Option[Function0[Unit]] = None
  override def delayedInit(cons: => Unit) {
    x = Some(() => cons)
  }
  def main(args: Array[String]): Unit =
    x.foreach(_())
}

The new App trait extends DelayedInit. It defines an Option x containing the constructor behavior. The delayedInit method is overridden to store the constructor logic in the x variable. The main method is defined so that it will execute the constructor logic stored in the x variable. Now that the trait is created, let’s try it in the REPL.

scala> val x = new App { println("Now I'm initialized") }
x: java.lang.Object with App = $anon$1@2013b9fb

scala> x.main(Array())
Now I'm initialized

The first line creates a new anonymous subclass of the App trait. This subclass prints the string "Now I'm initialized" in its constructor. The string isn’t printed to the console during construction. The next line calls the main method on the App trait. This calls the delayed constructor, and the string "Now I'm initialized" is printed.

The DelayedInit trait can be dangerous because it delays the construction of the object until a later time; methods that expect a fully initialized object may fail subtly at runtime. The DelayedInit trait is ideal for situations where object construction and initialization are delayed. For example, in the Spring bean container, objects are constructed and then properties are injected before the object is considered complete. The DelayedInit trait could be used to delay the full construction of an object until after all properties have been injected. A similar mechanism could be used for objects created in Android.

The DelayedInit trait solves the problem where construction and initialization of objects are required, due to external constraints, to happen at different times. This separation isn’t recommended in practice but sometimes is necessary. Another initialization problem exists in Scala, and this occurs with multiple inheritance.

4.1.2. And then there’s multiple inheritance

Scala traits provide the means to declare abstract values and define concrete values that rely on the abstract. For example, let’s create a trait that stores property values from a config file.

trait Property {
  val name: String
  override val toString = "Property(" + name + ")"
}

The Property trait defines an abstract member name which stores the current name of the Property. The toString method is overridden to create a string using the name member. Let’s instantiate an instance of this trait.

scala> val x = new Property { override val name = "HI" }
x: java.lang.Object with Property = Property(null)

 

Rule 9: Avoid abstract val in traits

Using abstract values in traits requires special care with object initialization. While early initializer blocks can solve this, lazy val can be a simpler solution. Even better is to avoid these dependencies by using constructor parameters and abstract classes.

 

The val x is defined as an anonymous subclass of the Property trait. The name is overridden to be the string "HI". But when the REPL prints the value of toString, it shows the value null for name. This is due to order of initialization. The base trait, Property, is initialized first during construction. When the toString method looks for the value of name, it hasn’t been initialized yet, so it finds the value null. After this, the anonymous subclass is constructed and the name property is initialized.

Two ways to solve the problem exist. The first is to define the toString method as lazy. Although this delays when the toString method will look for the value of the name property, it doesn’t guarantee that the initialization order is correct. The better solution is to use early member definitions.

Scala 2.8.0 reworked the initialization order of traits. Part of this was the creation of early member definitions. This is done by creating what looks like an anonymous class definition before mixing in a trait. Here’s an example:

scala> class X extends { val name = "HI" } with Property
defined class X

scala> new X
res2: X = Property(HI)

The class X is defined such that it extends the Property trait. But before the Property trait is an anonymous block containing the early member definition. This is a block containing a definition of the val name. When constructing the class X, the toString method correctly displays the name HI. A second way to declare early initializers exists.

scala> new { val name = "HI2" } with Property
res3: java.lang.Object with Property{
  val name: java.lang.String("HI2")} = Property(HI2)

The next line constructs a new anonymous Property. The anonymous block after the new keyword is the early member definition. This defines the members that should be initialized before the Property trait’s constructor is initialized. The REPL prints the correct toString from the Property value.

Early member definitions solve issues that occur when a trait defines an abstract value and relies on it in other concrete values. We usually avoid this situation due to issues from previous versions of Scala. For any complicated trait hierarchies, early member initializations provide a more elegant solution to the problem. Because the members that need early initialization can be buried behind several layers of inheritance, it’s important to document these throughout a type hierarchy.

Scala makes multiple inheritance simpler and provides mechanisms for dealing with the complicated situation. Early member definitions are one such way. Some things can be done to help prevent issues in the future such as providing empty implementations for abstract methods.

4.2. Provide empty implementations for abstract methods on traits

One of the first things I tried to do when first toying with the Scala language was use traits for a kind of “mixin inheritance.” The problem I was trying to solve involved modeling a real-world situation. I needed to be able to create managed objects, including physical servers, network switches, and so on. The system needed to emulate the real world and create realistic-looking data that would be fed through our application’s processing stream. We used this simulation shown in the following figure 4.1 to test “maximum throughput” of the software.

Figure 4.1. Simulation class hierarchy

We want this system to model real-world entities as best as possible. We also want the ability to mix in different behaviors to our entities, where certain base traits could provide default behavior. Starting out, we want to model network switches and network servers, including Windows and Linux servers, along with some form of agent that runs on these services and provides additional functionality. Let’s create a simple base class for a SimulationEntity.

Listing 4.3. SimulationEntity class
trait SimulationEntity {
  def handleMessage(msg: SimulationMessage,
                   ctx: SimulationContext): Unit
}

This is a simple trait that contains a handleMessage method. This method takes in a message and a context and performs some behavior. The design of the simulation is such that each entity will communicate through a simulation context via messages. When an entity receives a message, it updates its current state and sends messages appropriate to that state. The context can also be used to schedule behavior for later in the simulation. We’re off to a great start. Let’s define a simple NetworkEntity trait with simple NetworkEntity behavior. Remember that in a chain-of-command pattern, we want to define a base set of functionality and defer the rest to a parent class.

Listing 4.4. NetworkEntity trait

Scala traits have a handy property of not defining their super class until after they have been mixed in and initialized. This means that an implementer of a trait doesn’t necessarily know which type super will be until a process called linearization occurs.

 

Class linearization

Linearization is the process of specifying a linear ordering to the superclasses of a given class. In Scala, this ordering changes for each subclass and is reconstructed for classes in the hierarchy. This means that two subclasses of some common parent could have different linearizations and therefore different behaviors.

 

Because of linearization, the NetworkEntity trait could be using super correctly, or it might not, as the compilation output implies:

simulation.scala:21: error: method handleMessage in trait
SimulationEntity is accessed from super.
It may not be abstract unless it's
overridden by a member declared `abstract' and `override'
    case _ => super.handleMessage(msg, ctx)
                   ^
one error found

To make this work properly, the Scala compiler must know that no matter what, we can safely call super.handleMessage. This means we have to do one of two things: define a self-type or make the abstract method have a default “do nothing” implementation that would get called. The self-type approach could work, but it limits how your trait could be mixed in. We would be defining an alternative “base” that the trait had to be mixed into. This base would then need to have some kind of implementation for handleMessage. This provides too much restriction for the aims of the application.

The right way to approach this is to implement the method in the Simulation-Entity trait. This gives all our mixed-in traits the ability to delegate to super, which is a common theme when using traits as mixins. You must select some point in an object hierarchy where traits may start being mixed in. In our simulation, we desire to start directly at the top with SimulationEntity. But if you’re attempting to use traits with a Java hierarchy, this might not be the case. You may desire to start mixing into some lower-level abstraction. In the case of Java Swing, you could start your trait mixin classes with a javax.swing.JComponent rather than something lower, like a java.awt.Component. The point is that you need to select the right location to ensure that your mixin-delegate behavior will work correctly.

Sometimes with real-life libraries you can’t find default behaviors to delegate into. In this case, you might think that you could provide your own “empty implementation” trait. Let’s see if we can do that on your network simulation example. Let’s define your classes like so:

Listing 4.5. Empty implementation trait attempt

This code looks like a perfectly reasonable class hierarchy and compiles correctly, but it doesn’t work in practice. The reason is that the linearization of a concrete entity (Router in this case) doesn’t work with the MixableParent trait; things aren’t ordered as we’d like. The issue arises when we try to create a Router with NetworkEntity class. This class compiles fine but fails to handle the Test message at runtime, because this is how the linearization works. The following figure 4.2 shows the class hierarchy for a Router with NetworkEntity class and numbering classes/traits in their linearization order. This order determines what super means for each trait in the hierarchy.

Figure 4.2. Linearization of Router with NetworkEntity

As you can see, the MixableParent class is being called directly after NetworkEntity but before Router. This means that the behavior in NetworkEntity is never called because the MixableParent doesn’t call its super! Therefore, we have to find a way of getting MixableParent earlier in the linearization. Because things linearize right to left in Scala, we want to try creating a MixableParent with Router with NetworkEntity. That first requires turning the Router class into a trait. This might not be feasible in real life, but let’s continue the exercise. We’ll see what this looks like in a Scala REPL session:

 

Rule 10: Provide empty implementations for abstract methods on composable traits

In Scala, trait linearization means that super calls within a trait may be different depending on how an object is linearized. To provide full flexibility, each composable trait should be able to call a super method, even in that super method doesn’t do anything.

 

Listing 4.6. REPL session with simulation classes

As you can see, the behavior is now correct, but it isn’t quite intuitive that you have to use the MixableParent first in every entity creation. Also, the Router trait suffers from the same issues as MixableParent. It doesn’t delegate to its parent class! This is okay because Router is an entity that other behavior is mixed into, but in some cases this would be unacceptable. Also, there are cases where you can’t convert your classes into traits.

When creating a hierarchy of mixable behaviors via trait, you need to ensure the following:

  • You have a mixin point that traits can assume as a parent.
  • Your mixable traits delegate to their parent in meaningful ways
  • You provide default implementations for chain-of-command style methods at your mixin point.

4.3. Composition can include inheritance

An aphorism among the Java community is “favor composition over inheritance.” This simple advice means that it’s usually best in object-oriented Java to create new classes that “contain” other classes, rather than inherit from them. This allows the new class to use features/functionality of several other classes, whereas inheritance is limited to one class. This advice also has other benefits, including creating more self-contained “do one thing well” classes. Interestingly, Scala blurs this aphorism with its addition of traits.

Scala traits are composable in flexible ways. You can decide the ordering of polymorphic behavior by adjusting the order trait inheritance is declared. Multiple traits can also be inherited. These features combine to make traits a viable mechanism of composing functionality. Trait composability isn’t all roses; there are still some issues that aren’t addressed. Let’s look at the issues associated with composing behavior via inheritance in Java in table 4.1. Let’s look at the complaints associated with composing behavior via inheritance in Java and see how they stack up against Scala. I refer to composing behavior via inheritance as “inheritance-composition” and classes/traits that can do this as “inheritance-composable.” Composition that’s done via members of an object I’ll refer to as “member-composition” and classes/traits that can do this as “member-composable.”

Table 4.1. Issues with inheritance versus object composition

Issue

Java interfaces

Java abstract classes

Scala traits

Reimplement behavior in subclasses X    
Can only compose with parent behavior   X  
Breaks encapsulation X X X
Need to call a constructor to compose X X X

Scala traits immediately solve the problem of having to reimplement behavior in subclasses. They also use a clever trick to support multiple-inheritance on the JVM, making them “inheritance-composable” with more than one parent’s behavior. Scala traits still suffer from two major issues, breaking encapsulation and needing access to a constructor. Let’s look at how critical breaking encapsulation is.

Scala traits break encapsulation when used for composable behaviors. Let’s see what the impact of this would be. Suppose we have a class that represents a data access service in the system. This class has a set of query-like methods that look for data and return it. Suppose also that we want to provide a logging ability so we can do postmortem analysis on a system if it runs into an issue. Let’s see how this would look with classic composition techniques:

Listing 4.7. Composition of Logger and DataAccess classes

Notice how the DataAccess class uses the Logger class. The current method of composition means that the DataAccess class must be able to instantiate the Logger class. An alternative would be to pass a logger into the constructor of the DataAccess class. In either case, the DataAccess trait contains all logging behavior. One point about the preceding implementation is that the logging behavior is nested in your DataAccess class. If we instead wanted to also have the ability to use DataAccess with no Logger, then we need to create a third entity that composes behavior from the first two. It would look something like this:

Listing 4.8. Composition of Logger and DataAccess into third class
trait Logger {
  def log(category: String, msg: String) : Unit = {
       println(msg)
  }
}

trait DataAccess {
  def query[A](in: String) : A = {
     ...
  }
}

trait LoggedDataAccess {
  val logger = new Logger
  val dao = new DataAccess

  def query[A](in: String) : A = {
     logger.log("QUERY", in)

     dao.query(in)
  }
}

Now we have standalone classes Logger and DataAccess that are minimal in implementation. We’ve composed their behavior into the third LoggedDataAccess class. This implementation has all the benefits of DataAccess and Logger being encapsulated and doing only one thing. The LoggedDataAccess class aggregates the two, providing mixed behavior. The issue here is that LoggedDataAccess doesn’t implement the DataAccess interface. These two types can’t be used interchangeably in client code via polymorphism. Let’s see what this would look like with pure inheritance:

Listing 4.9. Inheritance-based composition of Logger and DataAccess
trait Logger {
  def log(category: String, msg: String) : Unit = {
       println(msg)
  }
}

trait DataAccess {
   def query[A](in: String): A = {
     ...
   }
}

trait LoggedDataAccess extends DataAccess with Logger {
   def query[A](in: String): A = {
      log("QUERY", in)
      super.query(in)
   }
}

Notice how the LoggedDataAccess class is now polymorphic on DataAccess and Logger. This means you could use the new class where you would expect to find a DataAccess or Logger class, so this class is better for later composition. Something is still strange here: LoggedDataAccess is also a Logger. This seems an odd dichotomy to have for a DataAccess class. In this simple example, it seems Logger would be an ideal candidate for member-composition into the LoggedDataAccess class.

4.3.1. Member composition by inheritance

Another way to design these two classes (outlined in “Scalable Component Abstractions” by Oderksy and colleagues) involves inheritance-composition and member-composition. To start, let’s create a Logger trait hierarchy. The hierarchy will have three logger types, one for local logging, one for remote logging, and one that performs no logging.

Listing 4.10. Logger hierarchy
trait Logger {
  def log(category: String, msg: String): Unit = {
       println(msg)
  }
}

trait RemoteLogger extends Logger {
  val socket = ...
  def log(category: String, msg: String): Unit = {
    //Send over socket
  }
}

trait NullLogger extends Logger {
   def log(category: String, msg: String): Unit = {}
}

The next thing we do is create what I’ll call an abstract member-composition class. This abstract class defines an overridable member. We can then create subclasses matching all the existing Logger subclasses.

Listing 4.11. Abstract member-composition trait HasLogger
trait HasLogger {
  val logger: Logger = new Logger
}

trait HasRemoteLogger extends HasLogger {
  override val logger: Logger = new RemoteLogger {}
}

trait HasNullLogger extends HasLogger {
  override val logger: Logger = new NullLogger {}
}

The HasLogger trait does one thing: contains a logger member. This class can be subclassed by other classes who want to use a Logger. It gives a real “is-a” relationship to make inheritance worthwhile to Logger users. “Why the indirection?” you may be asking yourself. The answer comes with the ability to override members as you would methods in Scala. This allows you to create classes that extend HasLogger and then mixin the other HasLogger traits later for different behavior. In the following listing, let’s look at using the HasLogger trait to implement our DataAccess class.

Listing 4.12. DataAccess class with HasLogger trait
trait DataAccess extends HasLogger {

   def query[A](in: String) : A = {
     logger.log("QUERY", in)
     ...
   }
}

Now for the real fun. Let’s write a unit test for the DataAccess class. In the unit test, we don’t want to be logging output; we want to test the behavior of the function. To do so, we want to use the NullLogger implementation. Let’s look at a specification test for DataAccess:

Listing 4.13. Specification test for DataAccess

We now have the ability to change the composition of the DataAccess class when we instantiate it. As you can see, we gain the benefits of member-composition and inheritance composition at the cost of more legwork. Let’s see if Scala has something that could reduce this legwork.

 

Note

A trait containing multiple abstract members is sometimes called an environment. This is because the trait contains the environment needed for another class to function.

 

4.3.2. Classic constructors with a twist

In the case of classic Java-like inheritance, we can try to compose using constructor arguments. This reduces the number of parent classes to one, as only abstract/concrete classes can have arguments, and they can only be singly inherited. But Scala has two features that will help you out:

  • Named and default parameters
  • Promote constructor arguments to members

In the following listing, let’s recreate the DataAccess class, but this time as a full up class where the logger is a constructor argument. Let’s also define a default argument for logger. We’ll promote this argument to be an immutable member on the Data-Access class.

Listing 4.14. DataAccess as a class with default arguments
class DataAccess(val logger: Logger = new Logger {}) {

   def query[A](in: String) : A = {
     logger.log("QUERY", in)
     ...
   }
}

This class is simple. It defaults to a particular logger at instantiation time and lets you supply your own (via constructor) if desired. The real fun comes when we want to extend this class, provide users with a mechanism to supply a logger to the subclass and use the same default as the DataAccess class. To do so, we’ll have to understand how the compiler collects default arguments.

When a method has default arguments, the compiler generates a static method for obtaining the default. Then when user code calls a method, if it doesn’t supply an argument, the compiler calls the static method for the default and supplies the argument. In the case of a constructor, these arguments get placed on the companion object for the class. If there’s no companion object, one will be generated. The companion object will have methods for generating each argument. These argument-generating methods use a form of name mangling so the compiler can deterministically call the correct one. The mangling format is method name followed by argument number, all separated with $. Let’s look at what a subclass of DataAccess would have to look like for our requirements:

Listing 4.15. Inheritance with default arguments
class DoubleDataAccess(
    logger: Logger = DataAccess.`init$default$1`
  ) extends DataAccess(logger) {
  ...
}

You’ll notice two things in this code. First, the constructor is pickled with a method name of init. This is because in the JVM bytecode, constructors are called <init>. The second is the use of the backtick (`) operator. In Scala, this method is used to denote “I’m going to use an identifier here with potentially nonstandard characters that could cause parsing issues.” This is handy when calling methods defined in other languages that have different reserved words and identifiers.

We’ve finally created a method of simplifying composition using constructor arguments. The method certainly suffers from ugliness when trying to also include inheritance in your classes. Let’s look at the pros and cons of each compositional method in the following table:

Table 4.2. Pros/Cons of compositional methods

Method

Pros

Cons

Member composition
  • Standard Java practice
  • No polymorphism
   
  • Inflexible
Inheritance composition
  • Polymorphism
  • Violation of encapsulation
Abstract member composition
  • Most flexible
  • Code bloat—especially setting up parallel class hierarchies
Composition using constructor with default arguments
  • Reduction in code size
  • Doesn’t work well with inheritance

Many “new” methods of doing object composition are possible within Scala. I recommend picking something you’re comfortable with. When it comes to inheritance, I prefer “is-a” or “acts-as-a” relationships for parents. If there’s no “is-a” or “acts-as-a” relationship and you still need to use inheritance-composition, use the abstract member composition pattern. If you have single-class hierarchies and no “is-a” relationships, your best option is composition using constructors with default arguments. Scala provides the tools you need to solve the problem you have at hand. Make sure you understand it fully before deciding on an object-composition strategy.

In section 11.3.2, we show an alternative means of composing objects using a functional approach. Although the concepts behind this approach are advanced, the approach offers a good middle ground between using constructors with default arguments and abstract member composition.

4.4. Promote abstract interface into its own trait

 

Rule 11: Put the abstract interface into its own trait

It’s possible to mix implementation and interface with traits, but it is still a good idea to provide a pure abstract interface. This can be used by either Scala or Java libraries. It can then be extended by a trait which fills in the implementation details.

 

Modern object-oriented design promotes the use of abstract types to declare interfaces. In Java, these use the interface keywords and can’t include implementation. In C++ the same could be accomplished by using all pure virtual functions. A common pitfall among new Scala developers was also an issue with C++: With the new power of traits, it can be tempting to put method implementations into traits. Be careful when doing so! Scala’s traits do the most to impact binary compatibility of libraries. In the following listing, let’s look at a simple Scala trait and a class that uses this trait to see how it compiles:

Listing 4.16. Simple Scala trait and implementation class
trait Foo {
  def someMethod(): Int = 5
}
class Main() extends Foo{
}

The following listing shows the javap output for the Main class:

Listing 4.17. javap disassembly of Main class

As you can see, with some adjustment to reading JVM bytecode, the Main class is given a delegate class from the compiler. One obvious issue with binary compatibility is that if the Foo trait is given another method, the Main class won’t be given a delegate method without recompiling it. The JVM does something funny, though. It will allow you to link (think binary compatibility) even if a class doesn’t fully implement an interface. It errors out only when someone tries to use a method on the interface that’s unimplemented. Let’s take it for a test toast in the following listing. We’ll change the Foo trait without modifying the Main class.

Listing 4.18. Modified Foo trait
trait Foo {
  def someMethod(): Int = 5
  def newMethod() = "HAI"
}

As you can see, we’ve added the newMethod method. We should still be able to use the compiled Main to instantiate a Foo at runtime. Here’s what it looks like:

Listing 4.19. ScalaMain testing class
object ScalaMain {
   def main(args : Array[String]) {
      val foo: Foo = new Main();
      println(foo.someMethod());
      println(foo.newMethod());
   }
}

You’ll notice we’re making a new Main object and coercing its type to be a Foo. The most interesting piece of this class is that it compiles and runs. Let’s look at its output.

java -cp /usr/share/java/scala-library.jar:. ScalaMain
5
Exception in thread "main" java.lang.AbstractMethodError:
Main.newMethod()Ljava/lang/String;
at ScalaMain$.main(ScalaMain.scala:7)
at ScalaMain.main(ScalaMain.scala)

Notice that the classes link fine; it even runs the first method call! The issue comes when calling the new method from the Foo trait. This finally causes an Abstract-MethodError to be thrown, the closest we get to a linking error. The confusing part to a Scala newcomer is that the trait provides a default implementation! Well, if we want to call the default implementation, we can do so at runtime. Let’s look at the modified ScalaMain in the following listing:

Listing 4.20. Modified ScalaMain testing class
object ScalaMain {
   def main(args: Array[String]) {
      val foo: Foo = new Main()
      println(foo.someMethod())

      val clazz = java.lang.Class.forName("Foo$class")
      val method = clazz.getMethod("newMethod", Array(classOf[Foo]): _*)
      println(method.invoke(null, foo));
   }
}

You’ll see we’re looking up and using the new method via reflection. Here’s the runtime output:

java -cp /usr/share/java/scala-library.jar:. ScalaMain
5
HAI

This points out an interesting side of the JVM/Scala’s design; methods added to traits can cause unexpected runtime behavior. Therefore it’s usually safe to recompile all downstream users, to be on the safe side. The implementation details of traits can throw off new users, who expect new methods with implementations to automatically link with precompiled classes. Not only that, but adding new methods to traits will also not break binary compatibility unless someone calls the new method!

4.4.1. Interfaces you can talk to

When creating two different “parts” of a software program, it’s helpful to create a completely abstract interface between them that they can use to talk to each other. See figure 4.3. This middle piece should be relatively stable, compared to the others, and have as few dependencies as possible. One thing you may have noticed from earlier is that Scala’s traits compile with a dependency on the ScalaObject trait. It’s possible to remove this dependency, something that’s handy if the two pieces of your software wanted to use differing Scala-library versions.

Figure 4.3. Abstract interface between software modules.

The key to this interaction is that each module depends on the common interface code and no artifacts from each other. This strategy is most effective when there are different developers on module A and module B, such that they evolve at different rates. Preventing any kind of dependencies between the modules allows the new module systems, such as OSGi, to dynamically reload module B without reloading module A so long as the appropriate framework hooks are in place and all communications between the modules A and B happen via the core-api module.

To create a trait that compiles to a pure abstract interface, similar to a Java interface don’t define any methods. Look at the PureAbstract trait in the following listing:

Listing 4.21. PureAbstract trait
trait PureAbstract {
   def myAbstractMethod(): Unit
}

Now let’s look at the javap disassembled code:

javap -c PureAbstract
Compiled from "PureAbstract.scala"
public interface PureAbstract{
  public abstract void myAbstractMethod();
}

You’ll notice the PureAbstract trait doesn’t have a dependency on ScalaObject. This is a handy method of creating abstract interfaces when needed; it becomes important when used with module systems like OSGi. In fact, this situation is similar to the one faced when interfacing two C++ libraries using C interfaces.

4.4.2. Learning from the past

Although this rule may seem contradictory to the “Provide empty implementations for abstract methods,” the two are used to solve differing problems. Use this rule when trying to create separation between modules, and provide implementations for abstract methods when creating a library of traits you intend users to extend via mixins. Pure abstract traits also help explicitly identify a minimum interface. A dichotomy of thought exists here. Some designers prefer “rich” APIs, and others prefer “thin,” where a thin API would be the minimum necessary to implement a piece of functionality, and a rich API would contain a lot of extra helper methods to ease usage.

Scala traits bring the power to add lots of helper methods, something lacking in Java’s interfaces. This kind of power was common in C++, which also suffered many more issues with binary compatibility. In C++, binary compatibility issues forced the creation of a pure “C” integration layer for libraries. This layer wrapped a rich C++ hierarchy inside the library. Clients of the library would then implement wrappers around the C layer, converting back from classless world to OO and providing the “rich” API. In my experience, these classes usually were thin wrappers around the C layer and mostly lived in header files, such that users of the library could gain binary compatibility without having to write their own wrapper.

In Scala, we can provide our rich interface via a simple delegate trait and some mixins. The “thin” interface should be something that we can reasonably expect someone to implement completely. This way the users of the “abstract interface” can grow their rich interface as needed for their project, assuming the “thin” interface is complete.

When you have two pieces of software that will be interacting but were developed by diverse or disparate teams, you should promote abstract interfaces into their own traits and lock those traits down as best as possible for the life of that project. When the abstract interface needs to be modified, all dependent modules should be upgraded against the changed traits to ensure proper runtime linkage.

4.5. Provide return types in your public APIs

 

Rule 12: Provide return types for public APIs

Scala can infer return types to methods. However, for a human reading a nontrivial method implementation, infering the return type can be troubling. In addition, letting Scala infer the return type can allow implementation details to slip past an interface. It’s best to explicitly document and enforce return types in public APIs.

 

Imagine you’re developing a messaging library. This library contains a Message-Dispatcher interface that users of your library can use to send messages. A Factory method also takes various configuration parameters and returns a MessageDispatcher. As a library designer, we decide that we want to rework existing implementation to create different MessageDispatcher implementations based on the parameters to the Factory method. Let’s start with a MessageDispatcher trait in the following listing:

Listing 4.22. MessageDispatcher trait
trait MessageDispatcher[-T] {
  def sendMessage(msg: T) : Unit
}

The trait is rather simple; it provides a mechanism to send messages. Now let’s create the factory and an implementation class:

Listing 4.23. MessageDispatcher factory and implementation class
class ActorDispatcher[-T, U <: OutputChannel[T]](receiver: U)
 extends MessageDispatcher[T] {
  override def sendMessage(msg: T) {
     receiver ! msg
  }
}

object MyFactory {
  def createDispatcher(a: OutputChannel[Any]) =
    new ActorDispatcher(actor)
}

The code is pretty standard. The actor dispatcher will transmit messages to an actor in the Scala actors library. We’ll discuss that library in depth later. For now, we’ll focus on the createDispatcher factory method. This method looks standard but has one issue: The return type isn’t a MessageDispatcher but an ActorDispatcher. This means we’ve leaked our abstraction. See the javap output for proof:

public final class MyFactory$ extends java.lang.Object
  implements scala.ScalaObject{
    public static final MyFactory$ MODULE$;
    public static {};
    public ActorDispatcher createDispatcher(java.lang.Object);
}

We’ve leaked the ActorDispatcher class in the public API. This may be okay in a small project, but it lends itself to issues if others rely on receiving ActorDispatcher instances from this method instead of a MessageDispatcher. We can easily change this by refactoring your API slightly to return more than one type. Let’s create a Null-Dispatcher that doesn’t send messages. We also change the createDispatcher method to take in any type of object and return appropriate dispatchers for each. If we don’t have a useful dispatcher, we’ll use the NullDispatcher.

Listing 4.24. MessageDispatcher factory with two implementation classes
object NullDispatcher
    extends MessageDispatcher[Any] {
  override def sendMessage(msg: Any) : Unit = {}
}

object MyFactory {
      def createDispatcher(a: Any) = {
        a match {
          case actor: OutputChannel[Any] => new ActorDispatcher(actor)
          case _ => NullDispatcher
        }
      }
}

This slight change has made the compiler reinfer a different return type. We can see proof of this in the new javap output:

public final class MyFactory$ extends java.lang.Object
      implements scala.ScalaObject{
    public static final MyFactory$ MODULE$;
    public static {};
    public MessageDispatcher createDispatcher(java.lang.Object);
}

The resulting API has inferred MessageDispatcher as the return type. This could silently break code that was relying on receiving an ActorDispatcher. It’s easy enough to annotate the return type for a public API. Modify the createDispatcher method as follows:

object MyFactory {
      def createDispatcher(a: Any): MessageDispatcher[Any] = {
        a match {
          case actor: OutputChannel[Any] => new ActorDispatcher(actor)
          case _ => NullDispatcher
        }
      }
}

Now the return type is locked to MessageDispatcher[Any] and anything that violates this will cause a compiler warning, rather than breaking client code.

To help avoid confusion or leaking implementation details, it’s best to provide explicit return types on public methods in your API. This can also help speed compilation slightly, as the type inferences don’t need to figure out a return type, and it gives a chance for your implicit conversions to kick in, coercing things to the desired type. The only time it would be okay to not specify return types is in the case of sealed single-class hierarchy, a private method, or when overriding a method in a parent that explicitly declares a return type. Ironically, when coding in a functional style, you find that you tend not to use inheritance as much as you would think. I find this rule generally applies to my domain model and perhaps my UI library, but not the more functional aspects of my code.

4.6. Summary

Scala’s object system is powerful and elegant. The body of code in a class definition defines the constructor of a class. For top-level objects, this means that code in the body should avoid expensive operations and other non-construction behavior. Scala also allows mixin inheritance. But when defining methods, only methods marked with override can override an existing implementation in the hierarchy. Adding override can help ease mixin inheritance and avoid method typos. Mixin inheritance also provides a new way to compose software. Mixins can mark objects as having a value and allow new values to be mixed in via inheritance. This technique provides the most flexibility when pure abstract interfaces are used for the API systems. Finally, type inference can change an API as the object model expands. For public methods, it’s best to explicitly annotate return types on critical interfaces. This leads to the best use of objects in Scala.

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

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