DEV Community

Nikita Katchik
Nikita Katchik

Posted on

The simplest traits showcase

Intro

It is handy to be able to separate the interface from the implementation; however, it often is not enough. In this piece, I'd like to talk about probably the simplest example of an interface-class paradigm limitation.

What if we had to design a Size class in Java or other classic OOP language? There are many variations, especially in construction (factories) and setters (if any), but one thing would have remained unchanged in every single implementation: the two numbers.

// Java

class Size {
    private int mWidth;
    private int mHeight;

    public Size(int width, int height) {
        if (width < 0 || height < 0) {
            throw new IllegalArgumentException();
        }
        mWidth = width;
        mHeight = height;
    }

    public int width() {
        return mWidth;
    }

    public int height() {
        return mHeight;
    }

    public int area() {
        return width() * height();
    }
}
Enter fullscreen mode Exit fullscreen mode

Every engineer had to write something like this a couple of times, but has this never bothered you from the data perspective? It strikes me odd every time a pair of numbers is suddenly a Size. A pair of numbers can be anything, but since the data is not detachable from implementation, we create classes like Size, Point, and ComplexNumber – all containing precisely two numbers: from a data perspective, they are not different types.

Of course, we do this because, in Java, we have to think from the interface's perspective; implementation simply contains all the necessary member variables to fulfil the interface contract. The price we pay here is having to create a Size even if we have a pair of perfectly fine numbers on the stack.
In the case of Size, it does not seem like a big sacrifice; however, once the structure of the types, the transformations performed on them, and the wiring become more complex, not being able to use perfectly fine data can become rather limiting, especially if it's be something big.

The solution

What if there was an idiomatic way to treat data in different ways on different occasions? This is precisely what a trait is: a predefined way to interpret certain data type. Let us have a look at how similar thing could be done with traits in Rust.

First, we don't even have to declare a data type here: we can simply use a tuple instead. Here is how we'd declare a variable suitable to be treated as a size.

// Rust

let s = (2, 3);
Enter fullscreen mode Exit fullscreen mode

Then we declare a trait. Notice that we define area() function right away. That's because the area calculation does not depend on the details of width() and height() functions implementation details.

// Rust

trait Size {
    fn width(&self) -> u32;

    fn height(&self) -> u32;

    fn area(&self) -> u32 {
        return self.width() * self.height();
    }
}
Enter fullscreen mode Exit fullscreen mode

All that is left to do is to define to interpret a tuple of two integers (u32, u32) as a Size.

// Rust

impl Size for (u32, u32) {
    fn width(&self) -> u32 {
        return self.0;
    }

    fn height(&self) -> u32 {
        return self.1;
    }
}
Enter fullscreen mode Exit fullscreen mode

It is just as simple to implement the Size trait for a slice of two u32's.

// Rust

impl Size for [u32; 2] {
    fn width(&self) -> u32 {
        return self[0];
    }

    fn height(&self) -> u32 {
        return self[1];
    }
}
Enter fullscreen mode Exit fullscreen mode

We have just implemented Size trait for two basic data types which preexisted in the language. Now let us try invoking these implementations.

// Rust

fn foo() {
    let tuple = (2, 3);

    // Automatic trait resolution
    println!("Area: {}", tuple.area());

    // Explicitly choose trait implementation
    println!("Area: {}", Size::area(&tuple));


    let array = [4, 5];

    // Automatic trait resolution
    println!("Area: {}", array.area());

    // Explicitly choose trait implementation
    println!("Area: {}", Size::area(&array));
}
Enter fullscreen mode Exit fullscreen mode

Thoughts

Data doesn't have to be glued together with the implementation even when the interface is declared separately, and creating useful code without defining redundant data types is possible. With traits, we can interpret the same piece of data differently on different occasions, which changes everything in the pursuit of a good design. Traits are sometimes referred to as a concept of object-oriented programming, but in my opinion it is misleading: the complete traits can only be found in languages leaning towards data-oriented design, like Rust.

Top comments (0)