Building a Log Store in Rust – Part 2

16 Jan

Rust Doesn’t Have Inheritance, Or Does It?

Coming from Java/C++/Python, I’m used to using inheritance when I want to take a piece of functionality and extend it. Rust doesn’t have inheritance, so I was thrown initially on how I’d go about extending functionality in Rust.

Rust does however have traits. Traits are basically interfaces. Traits define a contract that a type can chose to implement or not. For example, the Read and Write traits are implemented by a number of types, including all the usual suspects: File, TcpStream, UnixStream, Cursor, etc. Therefore, you can easily swap in a File for a TcpStream, so long as you’re only using the Read and Write traits (or any other traits they both implement).

Traits have an important feature that allow you to simulate inheritance: default implementations of methods. When you define a trait in Rust, you typically only provide the method signatures, like you would in a Java interface, or a C++ purely virtual function. However, you can also provide a method body, as shown in The Rust Programming Language book. This is exactly like a parent class providing a method implementation, and a child class choosing to override it or not.

The other piece of the puzzle is polymorphism. This is a language feature that allows code to operate over many different types, without changing its implementation. Think about sorting for example: as long as you can compare objects, you can sort them. So in concert with generics, Rust can leverage trait objects to provide polymorphism. The Rust Programming Language Book does a better job of explaining this than I can, so go read that section… I’ll wait.

Composition

There is one more thing we haven’t talked about… composition. Most of the time when you come from a language like Java where object oriented programming is shoved down your throat, you always way to reach for inheritance when you want to extend a piece of functionality. In Rust, think first about composition. Composition is when you include an underlying type that has the functionality you’re looking to extend.

In building the distributed Log Store I knew I’d need a way to write to disk the log messages themselves, and an index for the fields inside the log messages. Coming from Java, I initially thought I could/should use some sort of base or abstract class that had the record writing ability, then inherit and specialize for storing the actual data and the index. (If you look at the commit history, this is a lie… I started with just storing the log messages, then realized later I could abstract away.) However, composition actually works better in this scenario. Both the LogFile for storing the log messages and the IndexFile for storing the index contain a RecordFile. Both of these types need this functionality, but they both use it in different ways, and don’t really need to expose the underlying mechanics of the RecordFile.

For example, a RecordFile deals in records that have a size and then an array of bytes ([u8]). The LogFile doesn’t store bytes (well everything is bytes, but there is a higher-minded semantic), it stores JSON objects. The LogFile also doesn’t really care about how long the JSON object is, so it doesn’t need access to the record’s size. The IndexFile on the other hand stores data serialized in another fashion. (Keeping it vague here because I haven’t decided on a serialization format yet.) Also, the IndexFile will actually write information beyond the end of what the RecordFile knows about.

Having “seen the light” of composition, it’s almost hard for me to think about how I would do this using inheritance. That said, composition exists in Java/C++/Python, so I could use this same tactic in any of those languages. I just find that Rust leads you down that path more gently, then Java “forcing” you to use inheritance.

Thanks for reading!!!

Leave a Reply

Your email address will not be published. Required fields are marked *