 All right, but we've got a very special guest here today. Now I have to actually talk softer. And he's here. He's one of our senior front-end guys at NAV, but he has finally seen the light and has let the world know that he really hates JavaScript. So let's give it all up for Corey Brown right here in the second row. You knew it was going to happen, and this is the shit that happens at NAV if you leave your computer unlocked. People impersonate you on Slack. You're lucky you got away this easy. So what are interfaces? Interfaces at their most basic level are the way that two parts of a software system talk to each other. So any code that you write is going to be using an interface of some sort. The question is how formal are those interfaces? How formally are they defined? How are they enforced? What kind of features do those interfaces provide? And in many languages, we have the concept of formal interfaces, whether you're coming from Java, whether you're coming from Go. Many languages have this concept of formal contractual interfaces that actually ensure a measure of type safety. And so this raises the question of what are interfaces good for? In these languages, you get type safety, and they allow a certain level of assurance that a class is going to kind of behave the way you expect it to. It's at least going to have the shape that you expect it to have, that you're gonna have some object or some part of the system that is going to at least expose methods that you know how to call. But there's some things lacking in these formal interfaces. There's some things I don't care for with them. I think they're not as good as they could be and certainly not as good as what we have the ability to get in Ruby and frankly any other language that has a good testing story. And so we look at a couple of examples here of Go and Java. And we can look at these examples and we can see that we have the definition of a set of methods, the arguments that are coming into it, and then return values. And we get the same thing with Java as well. We get this presentation of we have these methods that allow us to call methods in a certain way and get back a certain result. We have this bare minimum set of protections. This very minimal contract about how something that implements these interfaces is going to behave. And so we know that whatever implements these is going to behave at least like this. But is that what we care about? In most of the software systems I cared about and most of the software systems I've built, the types returned by my methods were not my chief concern. That was kind of a minimal expectation that I was going to get back reasonable types. And so I want to ask the question of what do we want from our interfaces? What do we want them to provide for us? What do we expect of interfaces that we build in our software systems? In more strongly typed languages, interfaces act to extend the protections of the type system and that's all they do. They just reinforce the protections that already exist. Now in Ruby we can approximate these kinds of protections in a number of different ways. We can have, what was it, raises in our methods. If they're not giving the right kind of arguments. That's kind of a clumsy way to do this. We can implement tests around this that reintroduce the same kind of protections that we get from formal interfaces. But what do we want out of these? Whenever I think about interfaces, I want more than just type safety. I want something more than just knowing that if I pass this thing an integer, I'm gonna get back an object of a particular type back. And so I want behavioral safety. I wanna know that this method behaves in a way that I care about. In a way that I specify and that I'm able to programmatically and automatically verify over and over and over again. And so what I want from interfaces is to be able to describe what I think my duck should walk like and quack like and otherwise be able to do. So let's examine an example case. Now this is partially lifted for some actual work that I've done at NAB with regards to a service that I've been working on. And with this we want to define some behavior. We wanna specify what it is we're expecting. Now in the project that I'm working on, we have, or we're making use of the repository pattern. How many folks here are familiar with the repository pattern? Okay. How many here are familiar with Rails? Much more folks, all right. So in Rails you have active record. Active record is a pattern. It comes from, or at least, is described fairly completely in patterns of enterprise application architecture. In that same book you have a couple of other patterns. You have the repository pattern, the data map or pattern, a whole shit ton of other ones. And you have multiple ways for being able to interact with your data layer. Active record is just one of those. The repository pattern is an alternative to that. It's a way to represent your data layer as a collection and to interact with it in that sort of way. There are folks who have built the repository pattern on top of active record. You also have other approaches available in the Ruby community that provide for the repository pattern. But what it allows is for you to ask your repository for an object or to pass it an object and for that repository to handle all of the details about how to store that in your data layer, whatever it may be, whether it's memory, disk, database, Mongo, please don't store your data in Mongo. Whatever it may be, your repository takes care of taking your objects that you care about in your system and actually putting them into a persistent or whatever kind of data storage framework you need. So in terms of specifying the behavior for a repository, I want any repository class within my system to expose at least these three methods. I want to expose find by ID, create and update, right? The same sort of things that we saw in the formal interfaces. In addition to this, I want find by ID to return either nil or an entity object that's meaningful to my system for the repository that I'm talking to. I also want my create method to return either an integer ID for the created entity or false if it's not able to create that item. And I want update to return either an integer ID or for that updated entity or false if it's unsuccessful. So at this point, we have this kind of loose specification for what we want from a repository. It doesn't really give us any more detail than what we saw with the formal interfaces. But through good testing, we can actually take this a bit further. And this is where I think Ruby really shines with the ability that we have in the backing that we have as a community to do testing. How many of y'all actually test your code? All right. So how many of y'all write your tests first, all right? You should write your tests first. And I'm gonna actually demonstrate this today in that I'm not gonna show you any implementation code. All right, because the implementation doesn't matter. The specification is what matters. How you get to that implementation can vary. But by defining the behavior first, we're able to know what we want and be able to know once we've achieved it. And so taking this specification idea a bit further and thinking about the behavioral safety that we want from a repository interface, we can say that I wanna be able to use the integer return from create method with find by ID and to retrieve the entity that has the matching attributes of the one that was previously created. So I wanna be able to turn around and take the value from one of these methods, use it to call another and get back what I expect. Further, I wanna be able to use the integer that's returned by update for the same purpose and be able to get back the object that I expect. All right, this is behavioral safety. This is going well beyond what any type safety system is gonna provide us. And it represents what we actually care about in the system. And so this is why I think interfaces are not something that we need to get hung up on if we have good testing practices and good approaches to understanding what we want our software to actually do. So here's the start of our interface specification. I'm gonna use RSpec because I like RSpec, I'm big into BDD and yeah, that's just my bias. You can do everything I'm gonna demonstrate in Minitest, it just looks different. So I'm gonna start with an RSpec shared example. I'm gonna say that I'm creating a set of examples for a repository. I'm going to declare the subject here is the described class. This is a little bit of RSpec sugar that allows me to say whatever class I'm gonna be getting, this is gonna be getting included into or whatever specs it's gonna be getting included into, whatever class is being described there. That is the subject that I'm gonna be interacting with. I'm then going to say that an invalid entity is just some object. The type doesn't matter for me at this point. And then I'm gonna say that my subject, that's what's implied here with the it is expected to respond to is a shorthand for the subject is expected to respond to these methods. Find by ID, create an update. I've got about half of what a formal interface provides me with some tests. And then in a specific context for a business repository that I'm describing, I can say that this object behaves like a repository. And now whenever I run these tests, I will be able to confirm that the business repository at least has these three methods on it. And I can do this with any other class in my system. So if I need to create a mock version of a repository, I can run the same test against that. If I wanna create countless other repositories for different entities in my system, I can confirm that all of them have a similar behavior. But this is just a part of the story. If we wanna add some additional safety, we can define tests around our create method. And we can say that we expect it to return nil when provided an invalid entity. And so that we expect whenever we call, what was it, our repository classes create method and pass it an invalid entity, which in our case is just an object instance, that we get back nil. And that if we, that we want to get back an integer when provided a valid entity, we are then able to assert that as well. Now, we haven't defined valid entity yet. This is another feature that I like from our spec that allows us to actually define that in the context of the test as it's being included. As the shared example is being included in a more specific test context, we can define what a valid entity looks like there. And that then goes into our shared example group and populates its context to be able to allow those tests to be portable across multiple definitions of what a repository is. Now, we can continue this adding more safety by describing our update method in the very same fashion. Now, we do have this line of code here where I'm using instance variable set. That's just for convenience in this particular case. Don't ever do that. Reaching inside your classes and modifying their internal state is a bad idea. But since this is just an example and you should not run any of this code in production, I'm just gonna do that as I can because it's Ruby. But we gain the same type of safety now that we get from a formal interface. And it's all test driven. And we can now apply it to any number of repositories within our system. We can now ensure that these things actually have a certain level of behavioral safety as well, but not a lot. We haven't really gone much farther than what a formal interface provides. But we've got very similar safety checks. And so if we wanna carry on from here, we can start looking at the find by ID method. And this is where we're going to start to see some of those behavioral safety checks coming as well. But here, we wanna implement that last little bit of safety where with an unknown ID, we expect it to return nil. Very simple, very straightforward. We now have essentially exactly the same capabilities as our formal interface would provide us. But we still have some more behavior to deal with. We still have to deal, we still have to deal with these two cases, the behavioral safety, the stuff that we actually care about that is making this more than just a formal interface redefinition in tests. And so how do we do this? First, we'll address find by ID's behavior after an entity is created. So for a call to find by ID, we're going to instant or we're going to create an entity via the repository. We're gonna store off the entity ID for that. And then we're going to call find by ID with that and expect that the ID on the entity that's returned matches the ID that was returned by the create call. And here we're now able to actually get some behavioral safety around how find by ID works with relation to the create method. We're actually able to assert something about what these repositories are gonna be doing without having to care how they do it internally. The implementation could be a hash map, the implementation could be an array, the implementation could actually be talking to a database. It doesn't matter. We're able to assert that whatever the internal implementation is, it meets the criteria and the behavioral characteristics that we actually care about. Now in the last example, we're gonna go through the dance of creating an entity, calling a lambda, our more specific context can provide to us. This is again is going back to that outer context where we define what a valid entity is. In that same context we can define an additional let that actually provides a lambda that is able to specify what it means to change an entity like we see on that second line within the example. We can call that with our valid entity, it'll mutate it in some way and then we will send that into our update method before storing off what was at the, or before coming back and confirming that the entity returned by find by ID matches the entity that we originally mutated. And so again, we're able to capture the behavioral characteristics of this code and provide safety that no formal interface is actually able to afford us. We're able to get more capabilities out of simply testing our code and testing it well and we don't need the type safety. The type safety becomes a secondary concern, a very minimal baseline concern. We can add as much of that kind of checking as we want to reassure ourselves, but if we have behavioral consistency and behavioral specifications, we don't have to worry as much about the typing. And so with that I wanna return to the question that I originally posed at the beginning or I posed it earlier. What do we want from interfaces? If all we want from interfaces is type safety, which is a lot of what interfaces afford in other languages, and I'm not sure that that's really good enough. I'm not sure that that's something that actually adds value to the Ruby programming language. We can get that from tests. And if we focus our tests around behavior, not just the types that are being returned, but around the actual behavior that we care about, what actually matters in our code, we can actually achieve much more, a much richer model and more meaningful specifications for what our software is meant to do. And so with informal interfaces, we can actually have something that's more powerful than what formal interfaces are able to provide us. This is some of why after 12 years of working at Ruby, having worked with Go, having worked with Elixir, having worked with Java, I still always come back to Ruby because the flexibility is empowering. I don't need to craft my code for the sake of the compiler. I can craft my code so that it can be understood by people and I can achieve just as much safety as I want or that I need while still maintaining flexibility. I can take advantage of the reality that Ruby provides a really beautiful duck typing system, where I can specify behavior and care about behavior rather than strict types. And that went by a lot quicker than I expected it to. So you get some extra time back. I'm happy to take questions. If anybody wants to shoot the shit about nav, I'm also happy to do that. Or what was it? If you wanna hear any dirt on Maven link, they're a great place to work too. Use shared examples. I've got a comparison slide in my talk coming up about these. And I'm happy to see that you use them and use them well. But you pass a lot of let, you establish some lets in the whatever spec is gonna use those shared examples. Do you ever find yourself running into cognitive load where you're like, oh, I've got stuff over here but I've got the shared stuff over here? And yes, there is absolutely a cognitive load. And I'm going to, what was it? Go ahead and say something about shared examples. I, what was it? Don't like them. I actually hate them a lot. And over the last several years, this is the only valid use case I've been able to find for shared examples. I have yet to find anything else that they're useful for other than this. What was it? Providing us the kind of capabilities that interfaces provide in other contexts. And in that case, because it's such a limited use case, it's the only case where I would actually look to use shared examples. I'm okay with that cognitive load because I want to keep the shared example as small as possible because it's going to be shared broadly at least in the context that I care about. Does it give you any consternation that you could replace Ruby with JavaScript and give this talk and it would pretty much work? Actually, the interesting thing is is that with the availability of behavior-driven development libraries across most languages, you could actually sub in almost any language, including Java. And what was it? The talk would go just fine. So, no, it doesn't bother me. And of course, what was it? Once WebAssembly? What was it? Gets the point. We can modify the DOM. Who will really use JavaScript anyways? So, are you referring to the pattern that some folks will do to kind of reproduce interfaces where they'll implement a module with a series of methods and then raise not implemented errors? Is that what you're referring to? Yeah, I hate that approach. That's me personally. I'm going to just lay that out there. I really dislike that. But then again, I also really dislike the way that modules get used a lot in Ruby. Like active support concerns like really, really bother me because we don't realize that they are a really dangerous way to introduce multiple inheritance into your code and not realize it because it's polluting your ancestry tree for the classes you're including a module into. If you want a more detailed rundown of that, talk to Brandon. What was it? Him and Jason Carter ran into a really, really fun issue with the order of modules getting included and how it just blew up the test suite. It will do some bad things. So like module include, module extend. I try to avoid that. Module prepend, I'm okay with in a very, if you're gonna do string class evals, just use module prepend. It's easier to test and it will not make people like me just angry beyond belief. I'll just throw a comment on that. If you are going to go that way, please for the love of God, define abstract method error, inherit from standard error. Not implemented is Ruby's way of saying your platform does not support a standard Ruby feature. Not that your class has not been implemented. Now you have some extra time back.