RailsConf 2016 - Succession by Katrina Owen

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
(fast techno music) (train horn) - The AV has been broken in the other room and that's why we're here, but I didn't think I'd be here, I thought I was going to be in the broken AV, so, the solution to that was to not have presenter notes, and just mirror the display, and at that point, mostly the slides would show. I have a lot of slides. I have like 300 slides, that's okay, we're going to get through it. But, I use my presenter notes, a lot, a lot, a lot, so I spent the night figuring out how to print them at the hotel, now this is a hard problem in computer science clearly, because when you export your Keynote presentation to pdf, it gives you the great big slides which I don't care about at this point, and ity bity presentation notes that I couldn't read, so I wrote AppleScript to generate some ruby code, so that I write a pron thing that would generate a pdf, that I could send by email to the business center at the Clown Plaza. It was great. You want to, I'll show you my notes, they're going to be souvenirs after. Just in case, I realized afterwards that I could have reduced it by 50% if I had exported my pdf to pdf via the file print, thing, and done like a layout two thing, but at three in the morning, I'm not thinking very clearly. Alright, I think it's time, alright. So, there's this thing that we do, and I'm pretty sure we all do it, where you notice that this bit of code over here, and that bit of code over there are basically doing the same thing, except they're implemented differently, and so, you decide to switch everything over to use just one of them. And about 17 tests break. You remember that there's an edge case, and so you quickly patch a conditional, now 24 tests are failing, you realize that there's a missing dependency, and so you pull that in and now you have about 50 failing tests, and it turns out that there's a conflict between two different dependencies, and so you swap them around so that they load in the opposite order Now, almost 80 tests are failing, and by the way that encoding thing that always happens, goes haywire, you are assuming that you've got UTF-8, but you're getting Latin-1, or is it the reverse, I don't know, and the method you're trying to use is accessing stuff that's not a part of the public API and so fuck it, you use send And a whole bunch of stuff blows up and its something to do with unexpected Nils, and it's been hours since the tests were last passing, and now you're considering monkey patching Nil class. Instead you do a get reset hard. The most difficult thing about refactoring, isn't actually making the change, it's knowing where to begin, and what to do next and how to make that change safely. I'm going to use the children's song that goes, I know an old lady who swallowed a fly as an excuse to talk about decision-making in refactoring. The song is incredibly simple, but it has an algorithmic component and this adds just enough complexity that we can talk about real world refactoring principles without bogging you down in real world code, so the entire example is all just text. This is all hard-coded into a here doc, it's wrapped in a method, and then wrapped in a class. Now the very first thing, you need to ask yourself, when you're about to refactor, is whether or not you should do it at all, and in this case, frankly, the answer is no. The song hasn't changed in generations, the code could be appalling, and it doesn't matter, the code works. We're never going to touch it again, stick it in production, and walk away. So, in order to avoid a spurious refactoring, I have invented a spurious requirement: we need to be able to continue to generate the song the way we've always done it, in its traditional form, we like that. In addition to this, we want to generate the song with arbitrary creatures, and so this establishes a perfectly legitimate reason to change the code. It also gives us some information. It tells us a little bit about the axis along which we need to support change. We don't need infinite flexibility, we just need this one type of flexibility. Now at this point, there's another important question that you need to ask yourself and that is, do you have tests, and here again the answer is no, because why on earth would you test a hard-coded string? So, to protect against regressions, copy the here doc into a test at a very simple assertion, and unsurprisingly, this test passes, which means that it is safe to start changing things. That raises yet another question: can you make the change that you need to make without any dirty hacks. Almost invariably, the answer is no. And so, we have this new feature, we want to implement it, the very first thing we're going to do is not implement it, instead we're going to rearrange the code, we're going to find that flexibility that we need, and then we're going to add the feature. Now it's almost never obvious up front what that flexibility is going to look like, and so, to start the process, just take a moment to look at the code for a bit, notice stuff, see what jumps out at you, look for things in the code that you don't like. And then pick one thing. I tend to pick either the thing that I hate the most, or the thing that I understand the best. And what most people remark on here is all of the duplication here in the string. There is duplication between verses, there's duplication within verses, but it's not exact duplication. The bits that change are all embedded inside the bits that stay the same, and so, it's also stuck in this great big here doc which makes it really hard to deal with a small piece at a time, and the first change I'm going to make is to introduce a little bit of indirection. I need to take tiny steps, I'm going to change one small piece at a time, and for this, I'm going to use a case statement, with a branch for each verse. So this means that we have to loop over the range of verses, join them to get the full lyrics, and you can do it all in the same method, but we only really care about the case statement, and so I'm going to go one step further, and isolate it into its own method, and finally we can start touching individual strings. So what I want to do is tease apart the bits that vary, from the bits that stay the same. In other words: some of this is template, and some of it is data, and we immediately run into the problem of naming things. Typically, you extract it, you name it, if you get the names wrong it can make the code dramatically harder to understand which makes the code dramatically harder to change. And the easiest time to get the names wrong, is right now, when we understand the least about the problem. Now of course in this case, even if you get the names right, it doesn't really help, we have everything all mixed together, and so this may seem a little bit odd, C-Style String Formatting using the percent method can be really handy. It provides a clear separation between the static part and the variations. I like that it doesn't matter where in the string the placeholder is, all of the data ends up outside of the template, and we haven't had to name anything. Now if you apply this to each of the verses, the duplication between verses has gone from being similar to being identical. And all of the bits that vary are off to the side, and it's the same for the duplication within each verse. Now each entire verse has all of the templatey stuff, on the right, on the left, Jesus, and all of the data on the right. Now this is a tiny change, but it is noticeable even when squinting at it. This is the here doc that we started out with, and we used a case statement to break up the individual verses with the strings unchanged, and now we've dissected the strings in place to separate the template from the data. Now you might argue that I just shoved code around on the side and pretended to have refactored, a little bit like shoving the food around on your plate, and pretending that you've eaten, kind of pointless. Some people can look at duplication and they can immediately see how to extract methods and objects, and often, I just can't tell, not at first, and so dissecting it like this, gives me a little bit more information without forcing me to commit to anything prematurely. And in this case, separating the template from the data has made the algorithmic part, a lot more obvious. Each verse, grows systematically, adding repetitions and finally, it stops abruptly. Now most of the verses take this phrase, and then they repeat it some number of times, passing in a number of different creatures. Now, it's not a lot of code, but it does seem like a complete thought, and I'd like to name it. There are three very common strategies for naming things. The first is to name it by using some fragment of the implementation, like swallow or to catch, or something equally concrete, and this is the kind of name that quickly gets out of date. You find yourself with a method named blue, it returns the hex code for red. The second strategy is to name it structurally. You have recurring line, or incremental sentence, or middle phrase, this is the kind of name that is absolutely, technically, accurate and totally unhelpful. The problem domain here, is not about phrases, it's about a little old lady who inexplicably swallows a fly and then compounds the problem by swallowing larger and larger creatures. And this particular part of the song is trying to explain the reasoning behind her behavior, it's talking about why she would every do such a thing as swallow a bird, or a dog, or a goat, in other words, her motivation. So we need to call motivation some number of times, each time with different data. Now ignore the fact that cows and goats are vegan, the predator in one verse becomes the prey in the next, and what we really want is a single list so that we're not writing everything twice. Ruby, turns out, has an innumerable method that conveniently yields each consecutive pair given a list of critters, loop over them, pass each pair to the motivation method, join it into a single string. In this chunk of code, also seems like a complete thought. And, I'd like to name it. Here, we're talking about a sequence, or chain of events, it describes a sort of bizarre food chain. So, the algorithmic portion of the code is all isolated into these two methods and there is a trade-off here. We've taken something that was blindingly simple, and we've complicated it, we've paid a price. And, we've gained a number of benefits. We've isolated a small and very cohesive piece of code, we've named an important concept, the food chain. And what's more, this implementation shows the bones of the algorithm, it expresses the underlying structure, of an essential piece of the song. This structure was indiscernible when everything was just straight up strings. So, two things just happened, we extracted and named the algorithmic piece, oh wow, that was two, and one thing didn't happen: we didn't use it anywhere. Technically, this is called a parallel implementation, and that sounds very fancy, but it means that I just didn't know what I was doing, and I didn't want to screw things up, while I was figuring it out. And so after making up a parallel implementation, you should be able to just swap it in, seamlessly. It will just work, and sometimes it doesn't. Here, it just failed, and so that means that there's something that we haven't understood yet about the food chain, and it turns out, one part of the chain is not like the rest What you'll usually see in a code base, when you have a rule with an exception, is a conditional, now these are kind of problematic in a foot in the door kind of way. You have a conditional, you blink, now you have 12. Somebody's going to get hurt. Within the context of a refactoring, conditionals can be a useful tool, first create the conditional, then name what it represents, and then use what you learned to make a decision about the next step. Now that sounds deceptively simple, and there are a couple of pitfalls, the conditional itself should contain the smallest possible difference. Now it makes sense if you think about, if something is the same in both blocks of an if-statement, then it's not really conditional on anything, and so, if we keep just the wriggly bit of the spider, then there's nothing to put in that else branch. Now, you could drop it, that might lead you to think that you're naming just the exception, and you're not. The conditional represents two variations on the same concept. The rule is one variation, the exception is another. You're not naming two different things, you're naming one single idea. So in the song, we're talking about the critter's distinctive features, or some sort of qualifier, it's just that some critters aren't all that distinctive or special. If you leave only the bit about the spider, then the name will almost, inevitably, end up being about the spider. If you name a fragment of an idea, it introduces not just indirection, but misdirection. This is a magic trick, it gives you an illusion of understanding. And this sort of deception makes it very, very difficult to refactor. So by forcing yourself to consider the symmetry, you name the whole concept. This method needs an argument. Prey is not the right name here. Prey is about hunting, this is nothing to do with one animal killing another for food, this is about the critter itself. Take a moment, to look at the conditional. It contains the smallest possible difference, it's symmetrical, containing both the exception and the rule, the name doesn't misinform, it's about a general concept in the song. Notice, that the result of the method depends solely on the argument that's passed. This method could live anywhere, it could be a global function for all we care, it doesn't have anything to do with the song. So, you've got this critter string, and then there's more stuff associated with it, we're trying to decorate it with more behavior, instead of passing the critter to the qualifier method, we should invert it, and send the qualifier message to the critter, and to do that the critter has to be an object. The data can all be extracted from the verse, putting up all the critters and the initializer, and predator and prey are now no longer just strings, they're objects to which we can send messages. And notice that once we found the critter object, the conditional just went away, that won't always be the case, don't be afraid to introduce something that makes you feel a little bit slimy. Refactoring isn't about making the code pretty. Refactoring is about understanding the problem better. Sometimes you have to get a bit dirty to get clean eventually. So the motivation method was so nice just a moment ago, and now, not so much, this chunk sticks out, it's not terrible, but it is too much information at the wrong level of abstraction. And these are intimate, nitty-gritty details and the motivation method shouldn't have to care. If we extract it, then we have to name it, the only thing I could come up with that kind of works is epithet, I will not tell you how long I spent in the thesaurus before I came up with it, and worse I've given this talk three times and I have mispronounced it every time. I just realized a few days ago how you actually pronounce that. So, epithet, since this is all about the critter, it belongs in the critter class, and this is pretty good, a lot has happened. We tried to swap in that parallel implementation, and we failed. Focusing relentlessly on a small symmetrical difference, isolated a meaningful idea. And that idea was the seed of a tiny object, the critter. This abstraction was not at all obvious at the start, but now, having found it and named it, it feels inevitable. So now our parallel implementation is finally complete, we can look at the case statement containing the original implementation, identify the algorithmic repetition and then swap it out with a call to this new implementation. And this time, the change is seamless, the test passes. And this is better, the case statement has gotten noticeably shorter than it was, but we've ended up with more code, and more complexity, and actually, you'll notice that there's a continued up there on the second squinty thing. That's all one class except for three little lines of critter there. So, to be fair, this is pretty mild complexity, there's probably nothing here that you couldn't understand even if you were just a little bit drunk. This whole time we've been dealing with duplication in the strings of the song, but it wasn't obvious duplication. It was kind of, we've been handling it indirectly from the edges, and when we start out, there were fragments of plausible duplication throughout the case statement, now there are whole chunks of unmistakable duplication, even the data has gone from being mostly a jumble to having a recognizable pattern. It's tempting to focus on that sameness, to extract it and to give it a name, resist that temptation for as long as you can. Identify the smallest difference, see if you can make it go away, and then ignore it and look at the remaining differences. Now the difference on the first line, is the name of some critter, we've got a bunch of critter objects, we just need to find the right one, and then get the name of it, and this'll take this tiny difference and make it the same everywhere and we can ignore it, and that leaves us with one, last difference between the different blocks of the case statement and this one is something that we have not yet named. Again, this is about the critter and it's kind of like a little bit of a commentary or an aside, so we'll need to pass more data, when we're creating critters, and then use that abstraction in the case statement, and then this too is identical, and we can ignore it. Something interesting has happened. All of that sameness that we've been ignoring can now collapse into a single block. Sameness, is a trap. It's a distraction. Differences are the parts that are interesting because they're fragments of some larger idea. You want to encapsulate the concept that varies, not the concept that stays the same. And once you've encapsulated the differences, the sameness either evaporates, or it condenses into something obvious. So all of this started with identifying duplication as the ugliest, best understood problem. We added indirection in order to be able to start dealing with individual strings, and then encapsulated the algorithmic portion of the song, discovering the critter object which is tiny and very, very cohesive. And finally, we focused on all of the little differences between the different cases, making them go away, and allowing us to collapse the entire set of identical cases. But, we're not done yet. We start out with a gigantic string, we've extracted and extracted, and extracted, and we've spent a lot of time down in the weeds, it would be very useful to take a step back and look at the bigger picture. So this is the song, it has five methods, three of them are private. All of that private stuff seems like it could be its own thing. Now the song does actually need all the old code, otherwise the test will blow up, but we can ignore it for a moment and just focus on this parallel implementation before calling this from song, we have to do, well, one thing, mainly. It has to actually work, otherwise the test will blow up. And this doesn't actually work. I want to do a second thing, and that's, I want to make the API of that class, the API that I want, and I'm only doing that because it's a hastle to fix it later, and it takes longer, I only have 40 minutes with you and so I'm going to do that first. Any other changes can wait until we've integrated it because then when we run it, the tests will have our back, So, the reason that this is not working yet is that the verse and chain methods, call last(i) on critters, and the verse object doesn't know about critters. And this is pretty easy to fix, set some initialization logic and then give it a reader, and with this change verse, has what it needs. Actually verse has more than what it needs. It doesn't need all of the critters, it just needs the critters for that verse. And this has implications for the API of the class. Notice that verse and chain both take I as an argument, and we can get rid of the parameter and instead get I from the size of the critters array, and it doesn't fix the most egregious thing about the API of this class. This is not the verse's verse, this is the string representation of this object. Now there's a Ruby idiom for that, which is to_s, which is very nice, and it makes the public API perfectly reasonable, especially if we make the last two methods private. So, to swap the parallel implementation into the song class, replace the call to the old verse method with a call to the new class and you don't actually have to call to_s here, because join will do that for you, then you can delete all the old methods. This passes the test. We now have three tiny objects, each one is focused and cohesive, and the critter is minuscule, it's basically a tiny wrapper around the raw data that we initialize it with and then the song class is a little bit bigger. And most of this consists of the array of critter data, and this class knows not very much, four maybe five things. It knows what the raw critter data is. It knows how to transform that data into critter objects. Implicitly it knows who eats whom in the food chain because its the order of the raw data and the array. It knows how to instantiate verses, it knows how to combine verses into a complete song. It no longer knows what those verses are, or how they're constructed. That is the domain of the verse class, which is significantly more complex than anything else in this code, this class knows how the sausage is made, it knows which verse should be long, which verse should be short, how to combine that data into templates, what the algorithm actually is for the food chain. It knows a lot. I'm not saying that you should always split things up into new methods and classes, but it can be useful to ask yourself if this thing that I have were two different things, what would they be, and if you can come up with an answer that's not too far-fetched, it could be worth exploring and you can always inline it if you hate what you get. So, the verse class is looking pretty decent from the outside, but it's pretty gross on the inside. The most glaring thing here is the case statement, which is still sticking around. It has some blatant duplication and as before, ignore the sameness, focus on the smallest difference, create a small focused conditional for just this difference making sure it's symmetrical, and then name it. In this context, this is what the narrator is summarizing or recapitulating everything that has happened until this point, now there are two verses where there is nothing to summarize, because there's no back story yet. The very first verse: nothing has happened yet, so there's nothing to say, and the very last verse the little old lady is dead so there's not much to say about that either. Having extracted the recap, the duplication in to_s can be collapsed and named and this is the incident the verse is about, this is the main deal. To_s tells a pretty good story, with all of the details relegated to private methods. This is a pretty decent division of responsibilities, the blatant duplication is gone, there are a number of smaller annoying things, for example, there are calls to last(I) on critters, and that's totally redundant because the critters that are stored in the verse are already the last(I) critters, so you can delete those and this removes almost all of the places where we reference I, there's one reference left in the case statement so we can switch that out and just switch directly on the critters length, and another little thing is the first critter in the list, which is the main character of the verse and it would be really nice to name it. And so, there is one more thing that I don't like. Go ahead and squint at all of the templatey stuff. Notice that the templates are all strings. They're all red. We're interpolating stuff that varies, stuff that varies is abstractions, that tends to be all black and here there's one string, there's one thing here that's not black and it really sticks out. It's a hard code to string, and you could argue that if it doesn't vary, should it even be data, maybe it should be a part of the template. There are a couple of arguments for using an abstraction here, the first is that we have this exact string in two places, this is the aside for the fly. So we've already named it, we have an abstraction for it. The other really good reason is that the whole point of making all of these changes is that we might not even have a fly, so we're going to just use that abstraction. The verse is looking pretty good at this point, it has boiler plate to set everything up and to_s which is what is called by the outside, everything we care about in terms of the API, and then to_s calls incident, and recap, and then recap calls chain and chain calls motivation, and it can feel like all of these ity-bity, kind of annoying insignificant changes are not worth worrying about, but all of these tiny irregularities and duplications and inconsistencies, they add up. It's hard to measure the impact of one, tiny change, but the difference in terms of clarity and understanding can be staggering. So the code is pretty good. I have one complaint left, and that's the case statement, in the very beginning, I just added it as a way to start getting at the strings, I thought it would be temporary, I hate that it's still there, so the final refactoring is one of the classics from the refactoring book, written by Martin Fowler, it's called replace conditional with polymorphism, and this is a step by step recipe. Refactoring is not about achieving maximum design pattern density. (laughter) (applause) Refactoring, is about balancing simplicity and readability, and changeability, don't refactor because you're embarrassed about your code. This is not about aesthetics. Don't refactor because there's some shiny design pattern you've been wanting to try out, this is not academic. Don't refactor because you imagine a beautiful future with ifs and what ifs, it would just probably really come in handy. Your guess about the future, is as good as mine. Which is to say, total shit. (laughter) We're more likely to be wrong than we are to be right. Refactor because you need to make a change, not a hypothetical change, an actual change. The whole point of the refactoring, wasn't to make the code beautiful, or satisfy some academic itch, or craving, it was to fulfill a new requirement. In addition to the age-old version of the song, the client wants to introduce a jungle theme song and an ocean theme song, and I do recall that there was something about a squirrel. The purpose of the refactoring was to make this not only possible, but easy. So we need to evaluate the code from this perspective. We need to be able to send in any set of critters, and right now we've got the hard-coded critters from the traditional version of the song, and this is fine the client likes the traditional version of the song, wants to be able to keep generating it and all of their code should still work. They also want flexibility, so this is a pretty straightforward change. If we give the constructor a parameter, we can default it to the existing array of data, and then loop over the argument, instead of the constant. And so this would almost work exactly the way you want it. The problem is that we're making assumptions about how many critters are going to be involved, now that is also an easy fix, we can loop up to the size of the array, we still have a problem and this one is not so easy to fix, there's a hard-coded eight down in the verse class, and the verse probably shouldn't know anything about anything, outside of that one verse, shouldn't know about other verses, shouldn't know about how many verses there are. And we have a few options here, most of them are really gross, so let's talk about them. On the one hand, in addition to telling the verse what critters to use, we could tell it how many total critters there are, there's no worse than hard-coding the eight, but also no better. More or less equivalent would be to pass all of the critters and then also pass I and this pushes even more knowledge about the big picture into the verse which is unfortunate, but on the other hand, there's a trade off, the song knows a little bit less that could be good. Another option would be for song to figure out whether the verse should be long or short, and then pass some sort of token to the verse class, which duplicates knowledge. Now, verse knows about the long and short, song also knows about the long and short, and they both have conditionals. Great, not great, songs shouldn't know about long and short templates, verse shouldn't know about total verses, and so what we need is a little shim between the two. Someone who can know about long and short templates and know how to make verses and what they need. We could have two really short, really stupid verses. We'd have this, a short verse, a long verse, neither would have to know about each other, neither would have to know about the total number of verses, and as long as both verses have the same interface, then the song doesn't have to know which verse it's dealing with, so we need to turn one verse, with one conditional containing two branches, into two verses with two recap methods, one for each branch of the conditional. And since the long verse has a chain, it also needs the food chain stuff. So there's a bunch of common stuff and we could duplicate it but that seems like kind of a bad idea so we could stick it in the short verse and then the long verse can inherit from the short verse and overwrite recap, and define the food chain. Now up in song, we need to call this between thing, this shim and it will know about all the critters, and it will switch and figure out what verse it is, and then instantiate it with whatever it needs, return it back to song. Some people would call this, well, over-engineering for one thing, verse builder, factory, I don't really care I like verse for, it totally works. So, we used to have code that was incredibly straightforward we've ended up with code that is still pretty understandable definitely more complex. Now everything used to be all in the same place, now it's spread out across a bunch of tiny, methods, classes, things, we've traded simplicity for flexibility, now if we'd done it right, we should be able to generate lyrics with arbitrary critter data, and the simplest way to check is to write a small test, and when I say small, I mean as small as possible, but certainly no smaller. The test data needs to cover all of our edge cases, everything that's relevant about the song, everything that's interesting about the song, but it shouldn't be realistic, because that wouldn't give us redundant data and it would obscure things and would distract from what's important, so instead of lions and monkeys and lizards, the data should be simple, and contrived and really, really uninteresting. So with this data, we should end up with these lyrics, the first and last verses are both short, the middle verses are long and one of the middle verses has that extra qualifier thingy. So if you stick this whole thing in a variable, you can assert that the code does the right thing, and it doesn't, which was a surprise when I was preparing this talk. It's pure luck that my contrived, really silly, uninteresting boring data happened to uncover a bug. Now this fails because we have the wrong indefinite article that means one a zebra, an alligator, and it's not a big deal if this came up in production, you could fix it in a heartbeat, so, everything passes, slipped that right in Kent Beck once said, make the change easy, then make the easy change. First, refactor the code, change the structure without changing the behavior, keep doing this, until you could add your feature without hacking it in, and often you'll find that the new requirement just takes a moment to implement and can kind of feel like a bit of a miracle, so actually Kent Beck didn't say this, he said almost this, what he said was make the change easy, warning, this may be hard, then make the easy change. Refactoring can feel like a little bit of a dark art. And it kind of is, because there's a lot of gut feeling involved, you're recognizing stuff you don't like, but that gut feeling, that intuition is learned. Read about code smells, watch Sandy's talk which is next. Look at code, do code reviews, practice refactoring, try stuff and then get rid of it and try something else. Ignore the sameness for as long as you can and don't name the fragments and slivers and shards of ideas, name symmetrical differences, name the whole concept, it should feel like you're discovering your abstractions, not inventing them. A good abstraction feels obvious in hindsight. It's already there, it's buried in your code and it's up to you to unearth it. Thank you. (applause) We have four minutes, supposedly that's for Q & A, I'm totally going to take them. The code is up on github, separate commits, one by one you can clone it, download it, look at all the changes, it's actually in a slightly different order, but that's because I kept changing things as I was working on the slides, but it's all, it doesn't really matter which order you do things in, you'll usually end up in the same place. I'm writing a book with Sandi Metz, I made this pretty slide and she's like, nope, that's what it actually looks like, I failed to do the thing where I design my slides afterwards, so I have this like odd red, red thing going on, okay, the book is awesome, it's also about a children's song, that is algorithmic, it's the 99 Bottles of Beer on the Wall, and again, it has that same algorithmic complexity, that gives you enough stuff, difficult stuff that we can talk about good design and refactoring techniques, without teaching you about investment banking and shipping containers and things like that. If you want to practice refactoring, a great place to go is exercism.io, I made that, so the story about exercism, is that oh, lots of languages, you can do exercises the real story about exercism, is that once you've done an exercise, go look at everyone else's code and start developing that sense of gut feel about what trade offs there are, what's easy, hard to understand, and have that conversation with people about how that code could be more interesting. Thank you. (applause) (techno beats) (train horn)
Info
Channel: Confreaks
Views: 15,102
Rating: undefined out of 5
Keywords:
Id: 59YClXmkCVM
Channel Id: undefined
Length: 40min 38sec (2438 seconds)
Published: Sat May 28 2016
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.