In [1] Schärli et al. provide a simple and understandable point of view: "It should be possible to view the class either as a flat collection of methods or as a composite entity built from traits. The flattened view promotes understanding; the hierarchic view promotes reuse." The authors establish the (original) definition of traits with the list of points below.
First of all, we can view trait composition as a way to complement single inheritance. Each trait is an independent collection of methods, that implements a certain behavior. These methods can depend on other methods / objects (following the general principle of reusability), thus traits can be parametric, by declaring what they require. Let's view a class with traits. What is class? Class = State + Traits + Glue as the authors say. This means that a class can declare its state variables, can be extended with traits and these traits on the one hand can operate on class' variables via getters / setters and on the other can also call methods from other traits. Traits' composition is not an (total) ordered relation, thus order is irrelevant. The only thing that matters structurally, is that traits as a set, may or may not have conflicting features (methods). If traits had state then the diamond problem could arise very easily, something that is avoided with traits. Conflicting features can arise when various traits define two or more features with the same signature. Then the resolution must be made explicitly and the authors introduce the notions of aliases and exclusion. If conflicting methods arise between class (this class or a superclass) and some trait, class methods take precedence over trait methods and trait methods take precedence over superclass methods. What is of great importance is that if you take a method in a trait, and the same method in a class is composed with the same trait, then the method has the same semantics (flattening property).
The authors present a use case of traits by refactoring the Smalltalk-80 collection hierarchy. They argue that collections have various characteristics; namely explicit ordering, implicit ordering, unordered, extensible, immutable, keyed etc. By single inheritance a programmer can provide a solution with code duplication or just by lifting everything up to the hierarchy and then throwing unsupported exceptions (effectively disabling the methods that are not needed). The collection was refactored to use 20 traits each one providing different behaviors and depending on others.
- A trait provides a set of methods that implement behavior.
- A trait requires a set of methods that parameterize the provided behavior.
- Traits do not specify any state variables, and the methods provided by traits never directly access state variables.
- Traits can be composed: trait composition is symmetric and conflicting methods are excluded from the composition.
- Traits can be nested, but the nesting has no semantics for classes—nested traits are equivalent to flattened traits.
First of all, we can view trait composition as a way to complement single inheritance. Each trait is an independent collection of methods, that implements a certain behavior. These methods can depend on other methods / objects (following the general principle of reusability), thus traits can be parametric, by declaring what they require. Let's view a class with traits. What is class? Class = State + Traits + Glue as the authors say. This means that a class can declare its state variables, can be extended with traits and these traits on the one hand can operate on class' variables via getters / setters and on the other can also call methods from other traits. Traits' composition is not an (total) ordered relation, thus order is irrelevant. The only thing that matters structurally, is that traits as a set, may or may not have conflicting features (methods). If traits had state then the diamond problem could arise very easily, something that is avoided with traits. Conflicting features can arise when various traits define two or more features with the same signature. Then the resolution must be made explicitly and the authors introduce the notions of aliases and exclusion. If conflicting methods arise between class (this class or a superclass) and some trait, class methods take precedence over trait methods and trait methods take precedence over superclass methods. What is of great importance is that if you take a method in a trait, and the same method in a class is composed with the same trait, then the method has the same semantics (flattening property).
The authors present a use case of traits by refactoring the Smalltalk-80 collection hierarchy. They argue that collections have various characteristics; namely explicit ordering, implicit ordering, unordered, extensible, immutable, keyed etc. By single inheritance a programmer can provide a solution with code duplication or just by lifting everything up to the hierarchy and then throwing unsupported exceptions (effectively disabling the methods that are not needed). The collection was refactored to use 20 traits each one providing different behaviors and depending on others.
- Schärli, Nathanael, et al. "Traits: Composable units of behaviour." ECOOP 2003–Object-Oriented Programming (2003): 327-339.
So, essentially, traits are like interfaces on steroids. That is, you are allowed to provide an implementation as long as it doesn't access class state (fields). This seems to be an intriguing alternative for mixins. However, it promotes "self-use", which can backfire in some situations. Consider a container class that includes an add(E e) method. A reasonable trait would be one that defined:
ReplyDeleteaddAll(Collection col) {
for (E e : col)
add(e);
}
The idiom of self-use (in this case, addAll() calling add() internally) is generally considered an implementation detail that the user shouldn't rely on. As a class evolves over time and new subclasses arise, it becomes increasingly difficult to keep track of all the method caller-callee relations and to maintain consistency by adding new decorators and such (e.g. one that counts the size of the collection). This makes it hard to believe that heavy use of traits will not hinder code extensibility in the long run.
Hi George!
DeleteWhat makes you say that traits are like interfaces? Do traits only provide the promise of structural conformity to classes they are applied to? Traits are only similar (or may seem similar) to interfaces, but only syntactically. I would prefer to think of traits as shared less/abstract class definitions (semantically) and not more/concrete interfaces.
On to your concrete case. With every pl feature comes responsibility (:P) and also guidelines for idiomatic use. Consider the case in which, not only do you want to add the addAll method, but provide all the *All variants at once (just say). That would include containsAll, removeAll, retainAll, addAll and similar optional operations. Traits give you the opportunity to pack all this into a "package" and attach this behavior to classes implementic collections with singular operations (just saying an example, not praising this design :P). My point is, traits aren't mean of encapsulation, but a behavior attaching feature. Speaking of Java, Scala collections are basicaly based on that design. Specifically for Scala, consider that traits can not only be used statically but dynamically too (with dynamic dispatch on the super). val customerDTO = new Customer with Save with Delete
Interesting article for Scala Collections http://docs.scala-lang.org/overviews/core/architecture-of-scala-collections.html
If the add method is declared public in the container, whouldn't it be considered to be part of the container's API? So addAll just relies on the contract that the container's API provides.
ReplyDeleteBreaking the contract while evolving the container over time has consequences not only for the trait in question but for every code that used the container's API.