Waxing on about object orientedness and how that jives with the Rust world
This commit is contained in:
parent
3ccb20eafa
commit
2d404d2c7f
|
@ -0,0 +1,98 @@
|
|||
---
|
||||
layout: post
|
||||
title: Considering object-orientedness from the Rust perspective
|
||||
tags:
|
||||
- opinion
|
||||
- rust
|
||||
- software
|
||||
- deltalake
|
||||
---
|
||||
|
||||
A very simple question in a community channel earlier this week sent me deep
|
||||
into reflection on software design. I started writing software as is
|
||||
classically understood as Object Oriented Programming (OOP), with Java, Python, Ruby,
|
||||
Smalltalk. Design has been mostly about creating those little boxes that
|
||||
encapsulate behavior and state: the object. [Rust](https://rust-lang.org) in
|
||||
contrast I wouldn't describe as an object-oriented programming language, to be
|
||||
honest I'm not sure what we call it. It's not functional programming and it's
|
||||
not object-oriented programming as I understand it. It's something else which
|
||||
is the key to why Rust is so enjoyable.
|
||||
|
||||
The simple question from [Mr Powers](https://github.com/mrpowers) was:
|
||||
|
||||
> Just noticed an interesting delta-rs / delta-spark difference. Delta Spark doesn't let you instantiate a Delta Table with a specific table version, but delta-rs does.
|
||||
>
|
||||
> * delta-rs: `DeltaTable("../rust/tests/data/simple_table", version=2)`
|
||||
> * delta-spark: `DeltaTable.forPath(spark, "/path/to/table")` - no version argument available
|
||||
>
|
||||
> Are there any implications of this difference we should think about?
|
||||
|
||||
The difference may seem trivial, one appears to have an optional "constructor"
|
||||
parameter and the other does not, who cares? But that's _not it_.
|
||||
|
||||
[Will](https://github.com/wjones127) responded correctly with:
|
||||
|
||||
> _I think the distinction to make is that `DeltaTable` represents a table at
|
||||
some particular time, and not the table in general_
|
||||
|
||||
|
||||
The thing is, I was there when the first API was written. I remember the design
|
||||
discussions and considerations we evaluated. I didn't catch the subtle change
|
||||
of thinking that was happening at the time.
|
||||
|
||||
When I am working in Ruby or Python, I find myself thinking about how to
|
||||
represent state and behavior as this black box. "How would I represent this in
|
||||
a diagram with boxes and arrows?"
|
||||
|
||||
Take a filter for example, a filter is almost always just _behavior_ but when I
|
||||
might design something like that in Ruby or Python, `Filter` becomes a base
|
||||
class which may or may not end up having state too. The base class becomes the
|
||||
means for describing "things which behave like this" but the very nature of
|
||||
defining class implies state.
|
||||
|
||||
Most object-oriented languages follow my beloved Smalltalk where everything is
|
||||
an object which contains both behavior and state, even when that doesn't quite
|
||||
make metaphorical sense.
|
||||
|
||||
Coming back to the question posed.
|
||||
|
||||
The reason this simple design difference seems so impactful to me is when I
|
||||
consider the Spark (Scala) implementation, it's design _bugs me_. It bugs me in
|
||||
a way that it wouldn't have, prior to starting to use Rust. Delta tables are
|
||||
constantly evolving as new writes occur, as new transactions are being written
|
||||
the idea of what the table _is_ also changes with the underlying data. This
|
||||
is especially the case when a metadata change is committed to the transaction
|
||||
log. Therefore making an _object_ encapsulate the concept of an ever-changing
|
||||
Table itself presents this jarring conflict: if I have this object, what is the
|
||||
actual nature of the object? How does (or does not) this object change over
|
||||
time?
|
||||
|
||||
Writing and reasoning about this, I think I have a better sense of what makes
|
||||
Rust so pleasing to work with. The ownership model and borrow checker _do_ make
|
||||
things much easier, but the nature of a program is **not** object-oriented, nor
|
||||
is it functional, but something else. It accommodates the current reality of
|
||||
software development which is inherently multi-modal.
|
||||
|
||||
At our disposal we have:
|
||||
|
||||
* _Functions_ which do things, and can be grouped into modules, etc.
|
||||
* _Structs_ which contain state, but like objects in other languages can have
|
||||
associated behaviors. Unlike in those object-oriented languages these cannot be
|
||||
_extended_. This forces the Rust developer to design structs around the state
|
||||
first and foremost. We are encouraged to take this data first approach and when
|
||||
combined with mutability and ownership rules, Rust programs tend to have fewer
|
||||
large evolving objects or object hierarchies.
|
||||
* _Traits_ which allow defining behaviors and grouping them in a hierarchy
|
||||
separated from data and state. This separation allows us to consider behaviors
|
||||
which might have slight variations but otherwise present a similar interface
|
||||
such as the filters example that I mentioned above.
|
||||
|
||||
I could wax on and on about how important traits are from a design standpoint.
|
||||
Being able to group and "inherit" behaviors separate from data is liberating.
|
||||
|
||||
The mental contortions I found myself doing in a more object-oriented world are
|
||||
no more. Nor am I going down the "functional programming all the things" rabbit
|
||||
hole. Rust has a lot of both to offer but I find that its structure has led me
|
||||
to _better_ designs because it has just the right amount of multiple different
|
||||
programming models thoughtfully mixed together.
|
||||
|
Loading…
Reference in New Issue