Skip to content

Latest commit

 

History

History
206 lines (154 loc) · 7.61 KB

Traits.md

File metadata and controls

206 lines (154 loc) · 7.61 KB

Traits

Description

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.

Create and use a new Trait

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'

Concrete example:

Trait named: #FamixTWithEnumValues
	uses: {}
	slots: {}
	category: 'Famix-Traits-EnumValue'

This will create a new Trait called TNameOfMyTrait stored in MyPackage.

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".

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.

Tu 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'

Abstract methods

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.

Stateful traits

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 }
	category: 'MaterialDesignLite-Extensions'
Trait named: #FamixTWithEnumValues
	uses: {}
	slots: { #enumValues => FMMany type: #FamixTEnumValue opposite: #parentEnum }
	category: 'Famix-Traits-EnumValue'

Traits initialization

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.

Customize method received from a Trait

When a class uses a trait, it is possible for it to reject or alias some methods.

Reject some methods received from the trait

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'

Alias some methods received from the trait

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'

Customize instance variables received from a (stateful) Trait

When a class uses a trait, it is possible for it to reject or alias some instance variables.

Reject some instance variables received from the trait

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 -- #instVarNameToRemove
	slots: {  }
	classVariables: {  }
	package: 'TestTraitAliasing'

Alias some instance variables received from the trait

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 aliasSelector: { #instVarAlias -> #aliasedInstVar })
	slots: {  }
	classVariables: {  }
	package: 'TestTraitAliasing'

Trait composition

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
	category: 'MyPackage'

Example:

Trait named: #EpTEventVisitor
	uses: EpTCodeChangeVisitor
	category: 'Epicea-Visitors'

Conflicts

Two kinds of conflicts can happen with methods implemented on Traits.

  1. 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.

  2. 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.