- Description
- Create and use a new Trait
- Abstract methods
- Stateful traits
- Traits initialization
- Customize method received from a Trait
- Customize instance variables received from a (stateful) Trait
- Trait composition
- Conflicts
Traits are pure units of behavior that can be composed to form classes or other traits. The trait composition mechanism is an alternative to multiple or mixin inheritance in which the composer has full control over the trait composition. It enables more reuse than single inheritance without introducing the drawbacks of multiple or mixin inheritance.
Pharo 7's Traits are modular and not tied to the Kernel. So it would be possible to have multiple implementations.
Creation of a new Trait is close to the creation of a new class. It is done in a programatic way:
Trait named: #TNameOfMyTrait
uses: {}
package: 'MyPackage'
This will create a new Trait called TNameOfMyTrait
stored in MyPackage
.
Concrete example:
Trait named: #FamixTWithEnumValues
uses: {}
slots: {}
package: 'Famix-Traits-EnumValue'
Calypso provides a menu entry to create traits. To access it, right-click on the classes list (with no class or trait selected) and select "New trait".
Warning: In Pharo < 8, the message
#named:uses:slots:package:
should be replaced by#named:uses:slots:category:
.
Then you add a new method to the Trait, just as you would implement a method in a class. All classes using this trait will be able to use methods created in the Traits except if for methods overriden by the class.
To use your Trait you just need to declare it in the class declaration as parameter of the #uses:
keyword.
MySuperClass subclass: #MyClass
uses: TNameOfMyTrait
slots: { }
classVariables: { }
package: 'MyPackage'
Concrete example:
FAMIXType subclass: #FAMIXEnum
uses: FamixTWithEnumValues
slots: { }
classVariables: { }
package: 'Famix-Compatibility-Entities'
You can also use multiple Traits with your class with the #+
message.
MySuperClass subclass: #MyClass
uses: TNameOfMyTrait + TNameOfMySecondTrait
slots: { }
classVariables: { }
package: 'MyPackage'
We might need to call a method for which the implementation will be specific to the class using the trait. To manage this case, a Trait can hold methods that explicitely declare that user should define it. These methods contain a call to #explicitRequirement
message.
TMyTrait>>addButton: aButton
self buttons add: aButton
TMyTrait>>buttons
^ self explicitRequirement
Some Pharo developers create Traits with all their methods calling
#explicitRequirement
message. Doing this kind of simulate an interface (as Java's interfaces). Users of one of these traits thus declare that they support the interface it defines and override all methods defined by the trait.
Since Pharo 7, it is possible to add instance variables or a slot to Traits. This will make you trait a stateful trait.
Examples:
Trait named: #MDLWithConfigurableRightPanel
uses: {}
slots: { #panelComponent. #toolbar }
package: 'MaterialDesignLite-Extensions'
Trait named: #FamixTWithEnumValues
uses: {}
slots: { #enumValues => FMMany type: #FamixTEnumValue opposite: #parentEnum }
package: 'Famix-Traits-EnumValue'
Traits do not include a way to initialize classes using them, it relies on conventions.
One way to manage this might be to implement a method named initializeTMyTraitName
on each traits needing an initialization and to call all those methods on the class using them.
In case of trait composition (See Trait composition), a trait composed of other traits can also implement a initialize method calling the one of the Traits it includes.
When a class uses a trait, it is possible for it to reject or alias some methods.
In some case it is needed to reject a method of a Trait. It can be achieved using #-
message.
TestCase subclass: #StackTest
uses: TEmptyTest - {#testIfNotEmptyifEmpty. #testIfEmpty. #testNotEmpty} + (TCloneTest - {#testCopyNonEmpty})
slots: { #empty. #nonEmpty }
classVariables: { }
package: 'Collections-Tests-Stack'
It is possible to alias some methods received from a trait. If, for example you alias #aliasedMethod
with #methodAlias
as shown below, your class will hold both #methodAlias
and #aliasedMethod
.
Object subclass: #MyObjectUsingTraitByAliasingMethod
uses: TTraitToBeUsed @ { #methodAlias -> #aliasedMethod }
slots: { }
classVariables: { }
package: 'TestTraitAliasing'
Here is a simple example. Consider a situation when a trait TLocated
implements a method moveTo:
that defines the movement of an object to a given cell. The user of this trait needs to implement the post-movement operation. Usually, this would be done by overriding the moveTo:
method and calling super moveTo: aCell
in the first line of the new implementation. However, the super calls can not be used with traits as they install methods directly into the code of their users. The simple workaround would be to create an allias basicMoveTo:
for the trait method and then call it from the new moveTo:
method implemented by the user class:
TLocated >> moveTo: aCell
"Define the movement"
Object subclass: #Antelope
uses: TLocated @ { #basicMoveTo: -> #moveTo: }
...
Antelope >> moveTo: aCell
self basicMoveTo: aCell.
"Do some post-movement actions"
When a class uses a trait, it is possible for it to reject or alias some instance variables.
In some case it is needed to reject an instance variable of a Trait. It can be achieved using #--
message. It works similarly to methods rejecting explaining in previous section.
Object subclass: #MyObjectUsingTraitByRejectingInstVar
uses: TTraitToBeUsed asTraitComposition -- #instVarNameToRemove
slots: { }
classVariables: { }
package: 'TestTraitAliasing'
#asTraitComposition
needs to sent to the trait because#--
message is not understood by trait but by trait composition.
It is possible to alias some instance variables received from a trait. If, for example you alias #aliasedInstVar
with #instVarAlias
as shown below, your class will hold both #instVarAlias
and #aliasedInstVar
.
Object subclass: #MyObjectUsingTraitByAliasingInstVar
uses: (TTraitToBeUsed >> { #instVarAlias -> #aliasedInstVar })
slots: { }
classVariables: { }
package: 'TestTraitAliasing'
Traits are composable, this mean that you can have Traits using other traits. It is done in the same way than class using a Trait:
Trait named: TMyComposedTrait
uses: TMyFirstTrait + TMySecondTrait
package: 'MyPackage'
Example:
Trait named: #EpTEventVisitor
uses: EpTCodeChangeVisitor
package: 'Epicea-Visitors'
Two kinds of conflicts can happen with methods implemented on Traits.
-
A method is present on a used Trait, but the class using this Trait also implements this method. In that case, the method lookup will select the method from the class. It is an equivalent of an override of method.
-
Two traits implementing the same method are used. In that case, if the method is called it will raise an error
traitConflict
.
A way to solve both cases is to use method aliasing and to remove the conflicting method:
Object subclass: #MyObjectUsingTraitByAliasingMethod
uses: TTraitToBeUsed @ { #methodAlias -> #conflictingMethod } - { #conflictingMethod }
slots: { }
classVariables: { }
package: 'TestTraitAliasing'
Another way to solve case 2. is to implement a method on the class using the trait in order to chose the behavior wanted.