|
| 1 | +#Traits in Pharo |
| 2 | + |
| 3 | +## Trait in a Nutshell |
| 4 | + |
| 5 | +Traits are kind of class fragments: they define methods and state. |
| 6 | +Classes or traits can use traits. When they do, they acquire all the trait methods and behavior. |
| 7 | +Now the composer (a class or a trait) can define to change some part of the trait it uses. |
| 8 | +With trait, definitions in the composer always take precedence over the definition defined in the used traits. |
| 9 | + |
| 10 | +### Simple Example |
| 11 | + |
| 12 | +#### Definition |
| 13 | + |
| 14 | +Here we define a trait named `TFun`. It defines a simple method named what which returns a string. |
| 15 | + |
| 16 | +```Smalltalk |
| 17 | +Trait << #TFun |
| 18 | + package: 'MyPackage' |
| 19 | +
|
| 20 | +TFun >> what |
| 21 | + ^ 'It is fun' |
| 22 | +``` |
| 23 | + |
| 24 | +#### Use |
| 25 | + |
| 26 | +Now we define a class named `WithFun` that uses the traits `TFun` (the message `traits:` is the message to add traits to a class). |
| 27 | + |
| 28 | +``` |
| 29 | +Object << #WithFun |
| 30 | + traits: { TFun }; |
| 31 | + package: 'MyPackage' |
| 32 | +``` |
| 33 | + |
| 34 | +Then we instantiate the class and send the message `what`. The message is resolved and the expected result is return. |
| 35 | +The class WithFun has all the methods defined in the trait `TFun`. |
| 36 | + |
| 37 | +``` |
| 38 | +WithFun new what |
| 39 | +> 'It is fun' |
| 40 | +``` |
| 41 | + |
| 42 | +### Method Definition |
| 43 | + |
| 44 | +You can then add a new method to the trait. As any other methods it can send message to existing methods. |
| 45 | +Here we add the method `superWhat`. |
| 46 | + |
| 47 | +``` |
| 48 | +TFun >> superWhat |
| 49 | +
|
| 50 | + ^ self what , '. Really fun' |
| 51 | +``` |
| 52 | + |
| 53 | +We can now send this message to instances of the class `WithFun`. |
| 54 | + |
| 55 | +``` |
| 56 | +WithFun new superWhat |
| 57 | +> 'It is fun. Really fun' |
| 58 | +``` |
| 59 | + |
| 60 | +### Other users |
| 61 | + |
| 62 | +The same trait can be used by multiple classes or traits. |
| 63 | +Here we defined a new class named `WithFun2` that uses the trait `TFun`. |
| 64 | + |
| 65 | +``` |
| 66 | +Object << #WithFun2 |
| 67 | + traits: { TFun }; |
| 68 | + package: 'MyPackage' |
| 69 | +``` |
| 70 | + |
| 71 | +And we can see that instances of this new class can execute the methods |
| 72 | +of the trait. |
| 73 | + |
| 74 | +``` |
| 75 | +WithFun2 new superWhat |
| 76 | +> 'It is fun. Really fun' |
| 77 | +``` |
| 78 | + |
| 79 | +### Overriding trait methods |
| 80 | + |
| 81 | +Now image that the class `WithFun` needs to have another behavior. |
| 82 | +We can redefine locally a method. This is this locally define method that will be executed |
| 83 | +instead of the method from the trait. This only impacts the class defining the specific method. |
| 84 | + |
| 85 | +The class `WithFun redefines the method `superWhat` as follows: |
| 86 | +``` |
| 87 | +WithFun >> superWhat |
| 88 | +
|
| 89 | + ^ self what , '. Really fun and powerful' |
| 90 | +``` |
| 91 | + |
| 92 | +Now the following expression shows that only the implementation of the class `WithFun` gets modified |
| 93 | +by the new definition. |
| 94 | + |
| 95 | +``` |
| 96 | +{ WithFun new . WithFun2 new} collect: #superWhat |
| 97 | +>> #('It is fun. Really fun and powerful' 'It is fun. Really fun') |
| 98 | +``` |
| 99 | + |
| 100 | + |
| 101 | + |
| 102 | + |
| 103 | +### Using multiple traits |
| 104 | + |
| 105 | + |
| 106 | + |
| 107 | +You can also use multiple Traits with your class with the `#+` message. |
| 108 | + |
| 109 | +```Smalltalk |
| 110 | +MySuperClass << #MyClass |
| 111 | + traits: { TNameOfMyTrait + TNameOfMySecondTrait }; |
| 112 | + package: 'MyPackage' |
| 113 | +``` |
| 114 | + |
| 115 | +## Abstract methods |
| 116 | + |
| 117 | +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. |
| 118 | + |
| 119 | +```Smalltalk |
| 120 | +TMyTrait >> addButton: aButton |
| 121 | + self buttons add: aButton |
| 122 | +``` |
| 123 | + |
| 124 | +```Smalltalk |
| 125 | +TMyTrait >> buttons |
| 126 | + ^ self explicitRequirement |
| 127 | +``` |
| 128 | + |
| 129 | +> 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. |
| 130 | +
|
| 131 | +## Stateful traits |
| 132 | + |
| 133 | +Since Pharo 7, it is possible to add slots to Traits. This will make you trait a stateful trait. |
| 134 | + |
| 135 | +Examples: |
| 136 | + |
| 137 | +```Smalltalk |
| 138 | +Trait << #MDLWithConfigurableRightPanel |
| 139 | + slots: { #panelComponent . #toolbar }; |
| 140 | + package: 'MaterialDesignLite-Extensions' |
| 141 | +``` |
| 142 | + |
| 143 | +```Smalltalk |
| 144 | +Trait << #FamixTWithEnumValues |
| 145 | + slots: { #enumValues => FMMany type: #FamixTEnumValue opposite: #parentEnum }; |
| 146 | + package: 'Famix-Traits-EnumValue' |
| 147 | +``` |
| 148 | + |
| 149 | +## Traits initialization |
| 150 | + |
| 151 | +Traits do not include a way to initialize classes using them, it relies on conventions. |
| 152 | + |
| 153 | +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. |
| 154 | + |
| 155 | +In case of trait composition (See [Trait composition](#trait-composition)), a trait composed of other traits can also implement a initialize method calling the one of the Traits it includes. |
| 156 | + |
| 157 | +## Customize method received from a Trait |
| 158 | +When a class uses a trait, it is possible for it to reject or alias some methods. |
| 159 | + |
| 160 | +### Reject some methods received from the trait |
| 161 | +In some case it is needed to reject a method of a Trait. It can be achieved using `#-` message. |
| 162 | + |
| 163 | +```Smalltalk |
| 164 | +TestCase << #StackTest |
| 165 | + traits: { TEmptyTest - {#testIfNotEmptyifEmpty. #testIfEmpty. #testNotEmpty} + (TCloneTest - {#testCopyNonEmpty}) }; |
| 166 | + slots: { #empty. #nonEmpty } ; |
| 167 | + package: 'Collections-Tests-Stack' |
| 168 | +``` |
| 169 | + |
| 170 | +### Alias some methods received from the trait |
| 171 | +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`. |
| 172 | + |
| 173 | +``` |
| 174 | +Object << #MyObjectUsingTraitByAliasingMethod |
| 175 | + traits: { TTraitToBeUsed @ { #methodAlias -> #aliasedMethod } }; |
| 176 | + package: 'TestTraitAliasing' |
| 177 | +``` |
| 178 | + |
| 179 | +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: |
| 180 | + |
| 181 | +```st |
| 182 | +TLocated >> moveTo: aCell |
| 183 | + "Define the movement" |
| 184 | +
|
| 185 | +Object << #Antelope |
| 186 | + traits: { TLocated @ { #basicMoveTo: -> #moveTo: } }; |
| 187 | + ... |
| 188 | +
|
| 189 | +Antelope >> moveTo: aCell |
| 190 | + self basicMoveTo: aCell. |
| 191 | + "Do some post-movement actions" |
| 192 | +``` |
| 193 | + |
| 194 | +## Customize instance variables received from a (stateful) Trait |
| 195 | +When a class uses a trait, it is possible for it to reject or alias some instance variables. |
| 196 | + |
| 197 | +### Reject some instance variables received from the trait |
| 198 | +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. |
| 199 | + |
| 200 | +``` |
| 201 | +Object << #MyObjectUsingTraitByRejectingInstVar |
| 202 | + traits: {} TTraitToBeUsed asTraitComposition -- #instVarNameToRemove }; |
| 203 | + package: 'TestTraitAliasing' |
| 204 | +``` |
| 205 | + |
| 206 | +> `#asTraitComposition` needs to sent to the trait because `#--` message is not understood by trait but by trait composition. |
| 207 | +
|
| 208 | +### Alias some instance variables received from the trait |
| 209 | +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`. |
| 210 | + |
| 211 | +``` |
| 212 | +Object << #MyObjectUsingTraitByAliasingInstVar |
| 213 | + traits: { (TTraitToBeUsed >> { #instVarAlias -> #aliasedInstVar }) } ; |
| 214 | + package: 'TestTraitAliasing' |
| 215 | +``` |
| 216 | + |
| 217 | +## Trait composition |
| 218 | + |
| 219 | +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: |
| 220 | + |
| 221 | +```Smalltalk |
| 222 | +Trait << TMyComposedTrait |
| 223 | + traits: { TMyFirstTrait + TMySecondTrait} ; |
| 224 | + package: 'MyPackage' |
| 225 | +``` |
| 226 | + |
| 227 | +Example: |
| 228 | + |
| 229 | +```Smalltalk |
| 230 | +Trait << #EpTEventVisitor |
| 231 | + traits: { EpTCodeChangeVisitor }; |
| 232 | + package: 'Epicea-Visitors' |
| 233 | +``` |
| 234 | +## Conflicts |
| 235 | + |
| 236 | +Two kinds of *conflicts* can happen with methods implemented on Traits. |
| 237 | + |
| 238 | +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. |
| 239 | + |
| 240 | +2. Two traits implementing the same method are used. In that case, if the method is called it will raise an error `traitConflict`. |
| 241 | + |
| 242 | +A way to solve both cases is to use method aliasing and to remove the conflicting method: |
| 243 | +```Smalltalk |
| 244 | +Object << #MyObjectUsingTraitByAliasingMethod |
| 245 | + traits: { TTraitToBeUsed @ { #methodAlias -> #conflictingMethod } - { #conflictingMethod } }; |
| 246 | + package: 'TestTraitAliasing' |
| 247 | +``` |
| 248 | + |
| 249 | +Another way to solve case 2. is to implement a method on the class using the trait in order to chose the behavior wanted. |
0 commit comments