Standardizing behavior
Let's imagine you want to be able to calculate property prices for real estate. We can start with the type Rectangle to represent rectangular pieces of land. The function property_price accepts a Rectangle and a price per square meter as its parameters and returns the price of the land.
Not all pieces of land are rectangular, however, which is why we should try to move beyond this limitation. We can implement further types, such as Triangle to represent differently shaped pieces of land.
Now, we are facing a problem where we need to define two functions (property_price_rectangle and property_price_triangle) to calculate the property price for each shape, even though both functions essentially have the same implementation. We could turn the functions into methods on each type, which allows us to call both methods property_price again. However, that does not solve the problem of duplicate implementations. A real solution to the problem would be to have a singular function that can work with multiple shapes, as long as they all implement area.
In the following example, another type called Shape is introduced. This type defines the behavior that is supposed to be shared between Rectangle and Triangle, which, in our case, is the method called area. To associate Rectangle and Triangle with Shape, the keyword called implements is used, as can be seen in the following example. When one type implements another type, it means that the first type has to implement all fields that the second type includes. In this example, Rectangle and Triangle have to implement the method called area using the exact way that Shape dictates.
In terms of terminology, Shape is referred to as a trait of Rectangle and Triangle. Also, Shape is called a supertype of Rectangle and Triangle, while the latter two are referred to as subtypes of Shape.
It should also be mentioned that, just because Shape is used as a trait here, it can still be used as a regular type, for instance, by being instantiated. Further, a type that implements a trait may implement additional behavior as long as the behavior that is dictated by the trait is implemented correctly. The benefit of using traits is that it is now possible to pass an instance of Rectangle or Triangle anywhere a Shape is expected.
As you can see, we are back to only one function for calculating the property price. The function now accepts any shape that implements the Shape trait.
A type can implement more than one trait at a time. For instance, the following example is an extension of the previous example, with another shape called Cube being added. A cube's surface area can be calculated like a shape, therefore, it can implement the trait Shape. However, a cube is also a three-dimensional body, meaning that its volume can also be calculated. Therefore, Cube also implements the trait Body. In the definition of Cube, both traits can simply be placed after the implements keyword, separated by a comma. Depending on which selection of behavior you wish to access, you need to refer to instances of the subtypes by the correct name of one of the traits. In case you want to access the entire behavior of a type, you have to refer to it using its own name.
One more important detail to take note of is the type of the this parameter. In the definition of the trait, this has the type of the trait. However, in the subtypes that implement the trait, this has the type of the implementing subtype. This needs to be done in order for you to be able to access all the properties and methods of the subtype inside its method.