Design Patterns

Summarized using AI

Ancient Rails

Scott Parker • May 01, 2015 • Earth

The video titled "Ancient Rails" presented by Scott Parker at Ancient City Ruby 2015 discusses the challenges and decisions developers face when dealing with legacy Rails projects. As applications mature, they often accumulate complexities, leading to what Parker refers to as 'legacy code' that becomes increasingly difficult to manage. Throughout the presentation, he shares insights based on five years of experience with aged Rails codebases, emphasizing that strategic decisions can help avoid these pitfalls.

Key Points Discussed:

- Understanding Legacy Code: Legacy code refers to old code that is still in production, which can create ongoing maintenance challenges.

- Avoiding Hacks: Parker advises against implementing quick fixes or hacks that can introduce future complications. He emphasizes the importance of thoughtful coding practices and avoiding monkey patching core classes, as seen in his example of a custom key_grep function that added unnecessary complexity.

- Beware of Frameworks: While frameworks can streamline code, they can also complicate it by introducing hidden dependencies and increasing codebase complexity. Parker shares an anecdote from Groupon, where a controller framework led to unintended consequences and confusion.

- Deliberately Cultivating Patterns: Parker stresses the importance of establishing clear coding patterns early in the development process. Choices made today echo into the future, and establishing best practices can reduce confusion later.

- Commit-Based Documentation: He advocates for using commit messages as documentation to provide context for code changes. Commit messages should be detailed, offering insight on why changes were made to maintain clarity in the codebase.

Significant Examples:

- Parker recounts his experience at Braintree Payments, where the codebase has evolved from 20,000 to over 300,000 lines, showcasing the challenges of maintaining such a large application. He highlights the pitfalls of premature optimization and hacks that create convoluted code.

- The deep_rest framework example illustrates how aimlessly abstracting common behaviors can complicate logic and create new problems instead of solving existing ones.

Conclusions/Takeaways:

- Developers are encouraged to focus on sustaining maintainability by not writing hacks, carefully considering the implications of creating frameworks, and committing to clear patterns in their projects.

- Proper documentation practices, including thorough commit messages, play a crucial role in managing legacy code effectively and enhancing long-term maintainability.

Ancient Rails
Scott Parker • May 01, 2015 • Earth

As Rails enters its tweens, “legacy Rails” projects are increasingly common. Even if you haven’t dealt with such projects, every line of code we leave in production is destined to become legacy code.

As developers, our everyday decisions make it easier or harder to work in a codebase over time. I’ll share some of the decisions, both wise and unwise, that I’ve made or encountered in five years of working with aged Rails codebases. With just a bit of forethought, your Rails projects won’t be “aged” or “legacy” - they’ll be timeless.

Ancient City Ruby 2015

00:00:00.299 So let's give him a round of applause and get this thing back on track.
00:00:08.880 That's right, we're going to get everything back on track. Quite the opposite, I'm going to be talking mostly about how we've taken things way off track in the past.
00:00:14.880 Basically, yeah, this is just a presentation of a bunch of mistakes and dumb things we did. That's really my only claim to fame.
00:00:21.779 I'm not being humble; the reputation I have made is being a guy that reacts well to his own mistakes.
00:00:26.820 So yeah, I'm pretty good at it.
00:00:31.980 Let's talk about me for a little bit just to get some context around this. I work at a company called Braintree Payments, where we do online credit card processing.
00:00:38.579 It's a big Rails app; most of what we do is on a Rails app called the Gateway. I'll discuss that a bit more here in a bit, but it's been around since 2008 and it's quite large.
00:00:50.700 Before that, I spent several years at Groupon, a similar story with a big Rails application that grew quickly and had a big team.
00:00:57.480 So it also grew to a sizeable extent, which introduced some challenges and pain points.
00:01:08.280 Prior to those dark ages, I'm not going to talk about it.
00:01:13.439 Fun fact about me: I have never been paid to write new Rails code. I only seem to come into pre-existing applications that already have some age on them.
00:01:20.280 In that, I increase their bloat and age.
00:01:27.659 I believe in not wasting time, so I'm going to go ahead and tell you all the big insights up front.
00:01:32.700 And then if you want to leave, you're not going to hurt my feelings.
00:01:39.960 First, don't write hacks. No matter what the temptation is, try not to do it.
00:01:45.659 Beware of building frameworks, whether you're creating one from scratch or extracting your code. There's a lot of hidden complexity that sneaks up on you and can be detrimental.
00:01:52.680 Deliberately cultivate patterns in your application; this is really important to do early on.
00:01:59.460 And then do what I call 'commit-based documentation.' We'll talk about that here in a bit.
00:02:06.659 So let's sort of recap what I'm talking about. First, I want to be super clear: everything I'm discussing are actual things we've done in production. I'm showing actual code or representations thereof when I need to protect privacy.
00:02:13.319 I want to be clear that everyone involved, myself included, had really good intentions. Everyone is trying to do better. We had no design in this application at all, and it's easy to go straight from that point.
00:02:27.480 I also want to clarify that the things I'm going to talk about are totally blameless—some of the people who have done these things include me but also some of the best software developers I've ever worked with.
00:02:33.239 People that I would work with again in a heartbeat. So I want to make it clear that as I discuss these examples and poke fun at some of these things, I do it out of a desire to help others avoid repeating the mistakes I’ve made or that I’ve observed.
00:02:56.459 So, enough prefaces. Let’s start actually talking about what I want to discuss.
00:03:01.500 This is a picture of a monolith. When I say monolith, it's easy to think of that sleek obsidian thing from 2001, which nobody quite understands, but that’s not actually right.
00:03:13.440 This is a monolith in the real world; it refers to something carved from a single piece of stone. When we talk about big, gigantic Rails applications, that’s how I want you to think about it.
00:03:19.860 The key differences are that the original artist, creator, or engineer of this design had an idea in their mind, but this also represents a series of compromises regarding reality.
00:03:25.319 Design is compromised in the real world, and many things get lost in that translation.
00:03:31.440 So a lot of what we're going to be discussing is akin to time travel.
00:03:37.319 To put it in some concrete numbers, this is roughly the lines of code in the primary Braintree code base that I'm going to talk about today. Year over year, we started in 2008 with around 20,000 lines of code and we’re now north of 300,000.
00:04:05.879 In 2015, we thought, 'Wow, this is a lot of code; we really have to start decomposing this.' We figured we had to do better the next year. Spoiler alert: we didn’t.
00:04:13.260 But it turns out that decomposing a giant application is hard and challenging. It's a work in progress.
00:04:23.600 Once I have some good advice or at least a pattern of mistakes we made in this, I’ll give another talk sometime.
00:04:30.479 Currently, about three percent of the lines of code in our code base are from 2009 or earlier. My initial gut reaction to seeing that is: awesome! Great! That means most of the code is new and I don’t have to worry about it because we’re replacing old code at a pretty good rate.
00:04:52.199 However, it's not really true if you consider the total amount of code we wrote in 2009; one-third of it is still running in production. That’s actually much higher than I was expecting and this came as a surprise.
00:05:09.840 It indicates that the code we write for applications that are successful and don’t go away is going to stick around. It’s going to haunt us.
00:05:21.000 In our current code base, about 43 percent of the lines of code are older than a year. A year is an interesting metric—first because it's a nice number, and second, it represents the termination of the half-life of a line of code.
00:05:31.259 What I mean by this is that anybody who worked on that code has definitely forgotten any reason why they wrote it. Now, the only way you can find out more is from the code itself.
00:05:42.240 With an asterisk that we’ll talk about later.
00:05:54.479 Finally, before making a point, I want to provide one other definition up front: when I talk about maintainability, I'm referring to the vague, hand-wavy term that I'm not going to be able to improve in the situation.
00:06:06.600 What I mean throughout this discussion is minimizing the cost of making changes in the future. I’m not referring to error resiliency or anything like that; I’m simply talking about making it easy for me six months or twelve months from now to come in and make changes and revisit the decisions we made.
00:06:19.800 There have been some attempts to formalize this, but this is a gnarly formula—it’s the Halstead volume times 5.2 times the natural logarithm of that times cyclomatic complexity, lines of code, blah, blah, blah.
00:06:38.100 This doesn't actually work that well because I don't think the number of comments in a codebase significantly impacts maintainability; this has also been somewhat debunked.
00:06:51.419 So, the tl;dr of that is: this is hard to measure and I apologize for being vague and imprecise.
00:07:02.940 All right, piece of advice one: don’t write hacks.
00:07:08.639 I’m going to switch gears a little bit; I’m going to talk about a core Ruby function—definitely a real thing that exists that everybody uses all the time.
00:07:14.039 I'm going to talk about the method key_grep on Hash.
00:07:19.259 Now, key_grep—you might be asking if you’re not super familiar with this function—what it does is it takes a hash, such as {a: 5, Apple: 2, B: 7}, and takes a regex.
00:07:27.240 Optionally, you can pass in a block, which is what I'm doing here, and it, of course, returns the matching keys.
00:07:33.599 If you're not super familiar with it, I'll forgive you; you're probably more familiar with the base form without the block.
00:07:39.600 That just takes a regular expression and, of course, returns a two-dimensional array of matching results.
00:07:44.460 All right, so I've been misleading you a little bit—of course, like key_grep is a bizarre function.
00:07:49.500 It does not exist in Ruby; this is something we wrote. This has been in the Braintree codebase for quite some time.
00:07:55.860 This is hard to explain, but you basically pass in a pattern and can optionally get back the key, the value, and the regex match.
00:08:02.639 Oh dear, I touched the cable; don't touch the cable, Scott. I'm so sorry; man, I've broken everything.
00:08:08.819 See, I told you I'm good at creating problems. Right, truth in advertising.
00:08:13.880 I need a young priest and an old priest. Okay, all right; I will not touch anything with the exception of the right arrow key again.
00:08:21.840 So yeah, I'm not going to dive into exactly how this works. Again, that's probably a conversation for a different time.
00:08:26.879 The key question is: why did we do this? Because I really have no good explanation for why this was a method we felt was so important that we built it into Hash.
00:08:32.219 There’s nothing here that couldn’t have been like this could have been key_grep as a module that could have been tied to hash, but we didn’t do that.
00:08:37.680 Why is that a problem? It doesn’t really solve a problem for us—certainly not a problem that’s likely to come up time and time again.
00:08:42.719 When we bake things into core modules like that, when we monkey patch something, it becomes widely available.
00:08:48.779 It has high fan out combined with a high likelihood for change. This is sort of the genesis of a maintenance nightmare.
00:08:56.100 What I mean by this is, let's take this class A—it doesn't really matter what it is, class, or module—whatever it provides some contract to the world.
00:09:01.740 When we monkey patch something, like Hash, Hash is everywhere; it's very easy to use that throughout our applications.
00:09:07.320 But by monkey patching Hash, we’re writing code that is almost certainly going to change. The probability that it survives from 2009 to today is pretty low.
00:09:13.680 In fact, we cut this out at one point, but it was there longer than I would like to admit.
00:09:19.500 This creates really challenging refactoring and a difficult problem for us to solve— we shouldn’t make challenging problems.
00:09:25.200 We have enough challenging problems as it is without creating new ones.
00:09:31.920 Okay, you're probably saying, 'Scott, okay, that’s just a ridiculous contrived example; surely there are better things to talk about?'
00:09:42.940 Okay, let’s talk about something a little more concrete. This is actual Ruby code; I’m no longer going to mislead and lie to you, I don’t think.
00:09:49.140 Here we have an array; we're calling map to string on it—great! This came out in Ruby 1.8.7.
00:09:55.500 At Groupon, we liked this—this is open source, so I am showing it. Normally, I would not show code from other employers, but you can actually find this out on GitHub if you really look for it.
00:10:01.680 We liked it, but for some methods it’d really be better to pass in an argument rather than just saying 'I’m going to call to string' I could say 'I’m going to call to string with something else'.
00:10:10.560 Here's a concrete example: map plus, and then we can call it with something like the square brackets convention we came up with, so it returns 2, 3, 4.
00:10:16.920 Okay, well this is already a more sane example and I can see why this is useful; this is a problem I run into in Ruby.
00:10:22.079 However, this wasn't enough for us. No, we needed more power.
00:10:28.440 Not every method has every function defined. In a perfect world, it would, but the world isn't perfect.
00:10:36.960 In this case, we have an array of mixed types; we’ve got five and we've got Steve. We want to call 'days ago' on the things that know how to respond to days and ago.
00:10:51.660 So, we introduced this convention of using a minus sign, which signifies that if it can do it, we can call on it.
00:10:58.200 Of course, anything you can’t add on to that gets treated under those same rules and conventions.
00:11:04.800 So if we call '5 Steve map days ago,' you get a date and a nil.
00:11:10.560 Again, this can be useful in certain circumstances but it's not a great idea.
00:11:16.920 And why did we do this? It saves me time right now. Okay, this is already a way better argument than the last one.
00:11:22.140 It saves me time right now, and so this isn't a bad argument against writing software.
00:11:27.720 But it is a bad reason to follow this hackish approach.
00:11:34.200 Looking back at Ruby 1.8.6, this is the definition of symbol. This is a super blurry definition; sorry about that.
00:11:40.200 You don't really need to read it, but it didn't define simple to proc.
00:11:46.920 When Ruby 1.8.7 came along, we defined simple to proc, and now we have this new extension.
00:11:51.660 Time passes; we're using this a lot, but now Ruby 1.9.1 comes along and adds a few new methods on symbols.
00:11:58.680 We're continuing to add more implications, but now they've defined open brackets on the symbol—why would they do this?
00:12:08.040 The root cause is, while I’ve never actually used that in production, I’ve relied on this totally different definition of what this method accomplishes.
00:12:17.100 This turned out to be an upgrade nightmare. We reached a point where I thought, 'Well, I don’t want to be the person upgrading this code base; this is going to be bad.'
00:12:23.640 I did this—this is a travesty I perpetrated. I disagree with Ruby on principle; I don't like how Ruby works, and I tried to change it from inside Ruby.
00:12:32.100 This is an okay version; however, this is not the problematic code I’ll talk about that later. This was a result of an earlier problem I created.
00:12:42.600 I created some more problems while attempting to solve it, but this was a pretty convoluted situation.
00:12:49.699 Importing data from some CSV file: we had a recurring issue with data imports from untrusted sources.
00:12:59.760 We came up with a DSL to say we were going to clean the field names—with something that sanitizes names.
00:13:06.300 I mentioned my dark ages when I started as an enterprise consultant and worked in statically typed languages.
00:13:13.620 I mostly don’t miss that, but one thing I miss is you could refer to functions without invoking them; it drives me crazy.
00:13:19.800 In Ruby, I couldn't easily invoke a method without invoking it; there weren't great mechanisms to do that.
00:13:25.740 You can wrap things in a proc or block, but it’s not the same because everything has the same contract; it’s just both and it's repetition.
00:13:31.140 So I changed how Ruby worked in this one place.
00:13:37.200 The old version I commented out here. The new version involves a bare reference to this method, and we’ll figure out how to invoke it later.
00:13:43.620 This was a gross workaround that never worked smoothly, and it was a bad idea.
00:13:50.460 This also falls down in interesting ways when you want to pass in something with currying.
00:13:57.060 This required a ton of hacks to make it work, and it was a bad idea.
00:14:03.600 Similar reason: I disagree with Rails on principle because we’re talking about a framework, not a language.
00:14:08.940 Let’s talk about Active Record. When we call User.find(1), it gives back some instance of a model called Scott, okay, fair enough, straightforward.
00:14:18.960 However, calling 'id' results in a runtime error; don’t call 'id' on Active Record models, use 'pk' instead.
00:14:27.000 There are a couple of reasons for this, one being principle and one being pragmatic. The principle was 'id' is an overloaded term; 'pk' is really how this should be tackled in terms of classical database design.
00:14:39.000 The other reason relates to an old quirk that has since been fixed, but in older versions of Ruby, every object had a method definition on an 'id' which could be called on anything.
00:14:47.280 Active Record overrode that so that when you call 'id' on Active Record, you get its canonical database representation—its primary key.
00:14:55.039 As a result, you could run into edge cases where, if you ended up with nil instead of an Active Record option and call 'id' on it, you’d get variable outputs.
00:15:04.680 This led to bugs that really bugged us, so we wanted to create something called 'pk'.
00:15:09.000 However, this required a phenomenal amount of hacks. We still have this running in our database. You can change code, but databases are forever.
00:15:23.280 So, once you've got something running in production, that’s it.
00:15:27.240 And what's troubling about this is that anything we bring in makes that same assumption about Rails, because that’s how Rails works.
00:15:32.760 This approach also adds pressure to other dependencies in our code, so something that relies on 'id' needing to function correctly.
00:15:43.080 So these reasons often weren't good enough. I may disagree on principle, but it saves me time right now.
00:15:50.700 What I want to emphasize is: there are a couple of edge cases where I’m contradicting myself.
00:15:57.360 Time box changes to fundamental code—I'm not providing an example here, but I will talk through it.
00:16:04.920 When we were upgrading this same Gateway application to Rails 3.2, we ran into an issue.
00:16:11.280 Because of some things we do in Rails, the 'flash' hash wasn’t going to run nicely for both versions of this code to run simultaneously.
00:16:18.480 So, we realized this would be a problem existing in production for about an hour.
00:16:25.920 We could either change all invocations accessing flash, or we could temporarily monkey patch this with what we called the BT flash hash.
00:16:32.640 We chose the latter, and it turned out fine.
00:16:39.240 Backports, if you have critical functionality that hasn’t been released yet, you can backport it. If you need something for Ruby 1.9 and you're on Ruby 1.8.
00:16:47.760 Hopefully, nobody is on 1.8 at this point.
00:16:53.960 If you’re recording this stuff, it might be a good reason to consider.
00:17:01.680 That said, unless you’re dealing with something where the cost of forking is pretty high—like Ruby or Rails—you should consider a fork rather than monkey patching.
00:17:09.240 Even when you do this, it still carries some risk.
00:17:15.539 You have to test the heck out of it—test the happy paths, the unhappy paths, the 'why in God's name would anybody do this?' paths.
00:17:21.040 Make sure you test every conceivable scenario so that when some of your assumptions change, you've already identified it.
00:17:28.720 You should also explicitly call out these hacks as hacks; one pattern I really like is called 'app/hacks.'
00:17:35.100 That way, people know there be dragons—and by dragons, I mean zombie dragons, which you shouldn't touch.
00:17:41.280 Additionally, you should comment thoroughly. Self-documenting code is not enough to handle these kinds of situations.
00:17:47.760 While it tells you how it works, I can tell you where it was used, but I can’t tell you why somebody did this.
00:17:53.880 If nothing else, find a way to create a time bomb spec—that is, something that will break when your assumptions change.
00:18:00.840 For example, we wrote a spec that, if a week passes and this code still exists, it will fail.
00:18:07.560 Because we told ourselves that the only way we’re keeping this in production is if it’s time to take it out.
00:18:12.600 Piece of advice number two: beware of building frameworks.
00:18:14.940 We’ve all seen this quote a million times about regular expressions and how it creates new problems. I feel the same way about frameworks.
00:18:22.140 When extracting frameworks, it doesn’t mean they’re the wrong solution, but it introduces an extra level of complexity that can be easy to overlook.
00:18:35.220 My spiritual guru, Sandy Metz, said, 'Dependencies are killing you.' When we create frameworks, we introduce new dependencies and connections in our code base.
00:18:43.020 So we must be super careful when doing this. Hampton mentioned this morning that determining what even is a framework is a challenging task.
00:18:55.680 I’m going to give it my best shot: it's a subsystem providing generic functionality that can be customized by clients.
00:19:03.600 The reason I like this is that it serves my purposes: the last phrase there about customization by clients—that’s what gets you, or at least that’s what's gotten me.
00:19:09.900 So let's talk about a related concept—Martin Fowler has an idea of design stamina.
00:19:19.100 Essentially, there's this hypothetical good design line where you don’t lose productivity over time because you have the correct design.
00:19:26.640 On one side, there’s this line with no design, which is easy to get started with but will lead to many problems over time.
00:19:32.880 When we talk about building frameworks, we move that line closer to the good design line; we're talking about a long-term investment to facilitate future development.
00:19:43.680 We must be very cognizant of how and when we're paying those costs.
00:19:49.200 This is a concrete example: this is based on something I made up; I can’t really show you the original source.
00:19:53.760 Here’s a painful story we learned at Groupon: here's a movie controller, which is a bog-standard Rails 3 controller—not very interesting.
00:20:04.420 As applications grow, you're probably writing this controller or variations of it a zillion times.
00:20:10.920 So naturally, we think: hey, that's repetition! We don’t repeat ourselves; we’re good software developers.
00:20:17.040 Thus, we’re going to build a framework called deep_rest, and now we just say what resource we're working with.
00:20:23.880 It also has this special edge case where we need an additional instance variable set for new actions so that the views have it.
00:20:29.220 Everything looks great so far. However, with version two, we realize that not all users should be able to create and modify these movies.
00:20:34.380 Let’s say we’re building something like IMDb; we only want admin users to be able to do that.
00:20:44.040 So we establish that index and show don’t require login, while everything else will require it.
00:20:50.280 However, we have a Robin Hood problem: there are five million movies called Robin Hood, and our support users keep adding them.
00:21:00.280 It becomes tricky to untangle duplicates, so we need to check for duplicates occasionally and take action based on one of these values.
00:21:07.440 Now we've got this before filter that operates on an action which hasn't actually been defined, and it calls us to check for duplicates.
00:21:13.080 Check for duplicates renders a new template, and it’s not clear how 'new' plays into any of this at all because it’s no longer explicit in this controller; it all feels implicit.
00:21:20.520 To complicate matters, now we don’t just have admin users; we trust most of our usual internal users to create new movies or make edits.
00:21:28.560 This leads us to introduce 'roles'—a special non-role key called public, which defines a lack of role.
00:21:35.460 We then specify actions available for those: support and admin can access a set, while admin can only delete.
00:21:41.220 This is a pretty painful place to be now for many reasons, particularly as it complicates the normal happy path.
00:21:48.720 Most controllers will still follow that original example we began with, but users of deep_rest need to think about access and all sorts of additional things.
00:21:55.560 A couple of mistakes were made here: firstly, we created a framework for a straightforward problem.
00:22:03.960 Making that decision to create a framework is a trade-off discussion—where we can accept complexity and where we can't.
00:22:10.680 In this specific case, we identified a simple repetition of similar controllers but didn’t fully understand the problems they would address.
00:22:16.920 As we progressed, we just kind of added features without carefully considering refactoring.
00:22:22.620 While it’s easy to talk about needing constant refactoring, it’s critical when creating a framework.
00:22:28.920 Your design choices can greatly affect downstream clients, so you must consider their impact.
00:22:35.180 One way to tackle this complexity is to let people opt-out of the framework.
00:22:39.600 Either specify they’re in or they’re out—there’s no middle ground.
00:22:46.720 This isn’t code we wrote, but it’s something you can find as open source. I really admire the design of this restful controller.
00:22:52.700 You can say, 'Here’s the solution for my movie controller,' and if you want to modify an action, you just override the method.
00:22:58.920 This allows you to solve the simple problem while letting others opt out without being obtrusive.
00:23:05.760 Okay, let’s discuss that importer again; this thing was a bane in my existence. I made so many mistakes and learned a ton.
00:23:12.480 But this is the initial version of the interface I’m talking about: making it accepting 45 optional arguments.
00:23:19.020 In a true Rubyist style, I’m speaking to you: my method doesn’t take 45 optional arguments; it takes one.
00:23:23.280 Nope, it’s not that great; here's the problem: we define an import, and the interface takes a delimiter.
00:23:31.680 You can see it’s either a CSV or TSV; it’s a format but not even in delimiter—that's a misnomer.
00:23:38.100 We also added a location or stream option—there must be one of the two, but neither can be optional.
00:23:45.480 Next, we have fields: we've got to have something named; so, we could pass an array of names.
00:23:52.620 Alternatively, we can pass a symbol, which infers it from the first row of the CSV or point to a path on disk for external mapping.
00:23:59.460 Finally, we have validations—things will say whether to import this row; if field headers are defined, pass in a hash referencing field names.
00:24:06.900 Otherwise, you want to pass a bare array; this is a very problematic interface.
00:24:13.020 Here’s a diagram showing some interface A and several classes relying on that interface.
00:24:18.120 Let’s say we add a new caller, like the smile emoticon, which adds new requirements to the previous base case.
00:24:23.520 Well, when you change the original interface, you must consider everything relying on that same interface.
00:24:30.900 By introducing the smile emoticon, oh no, this complexity has crept into the whole system.
00:24:36.780 This makes it challenging to change, and a significant change often happens when you’ve lost all original context.
00:24:44.640 For maintaining good practices, let's turn to the SOLID principles.
00:24:51.419 We have single responsibility, open-closed, list of substitutions. The one Rubyists don't talk about is dependency inversion.
00:24:58.620 The open-closed principle should guide us: when you write code, it should be open for extension but closed for modifications.
00:25:07.580 This is difficult to achieve; I’m lucky if I get it right a couple of times a year.
00:25:15.000 Even more so, the interface segregation principle states that no client should depend on an interface they don’t use.
00:25:21.600 Originally, many of us thought this doesn't apply to us, as Uncle Bob emphasizes recompilation's impact on C++.
00:25:27.000 While recompilation can be challenging, there's also the maintenance headache that we need to be aware of.
00:25:34.560 For our interfaces, we realize that clients are not using most of them as they are designed to be mutually exclusive in specific ways.
00:25:40.620 What the interface segregation principle does is promote the creation of adapters, so clients only interact with the functionalities they require.
00:25:47.760 Next, let’s explore an example where this works: Active Record. I believe Active Record handles this quite well.
00:25:53.640 Imagine I have this fictional order class that belongs to a customer. As the consumer, you don’t need to know the underlying mechanics.
00:26:00.180 It’s a straightforward solution, as Active Record knows how to translate that to maintain its current set of reflections.
00:26:06.120 You just care about the logic directly relating to the belongs-to behavior.
00:26:12.120 This is especially important when you look at the 'has' and 'belongs_to_many' relationships that can be much more complex.
00:26:19.140 This is a great example of good software design.
00:26:26.760 One last sad tale: pray for him. We built something at Braintree that we don’t recommend.
00:26:34.560 Like most of these things, it started with good intentions and clever insights.
00:26:41.400 We have these forms for users that consist of an email and a password. This is a hypothetical scenario; these forms are not real.
00:26:49.080 Still, there's a model for User with similar attributes, like email and password, that should ideally be consistent.
00:26:55.620 But no—the short answer is no. Let’s fast forward to the end of this story since I don't want to take forever retelling tales of woe.
00:27:01.620 We attempted several implementations to get this to work. This is our under review form.
00:27:08.160 Among several issues to point out, we incorporated the idea of a theme mixed with all these name fields and text fields.
00:27:15.120 We realized some forms display differently and that had to be integrated somehow.
00:27:21.420 Instead of having a direct relationship with a model, we came up with a population model method that knows how to convert this form into a model.
00:27:29.280 There are weird edge cases we account for because it doesn't apply cleanly.
00:27:36.180 This became extremely challenging. Not all repetitions are equal.
00:27:43.080 I think of these in terms of three categories: the most benign is test repetition, which I don't worry too much about.
00:27:51.420 Test repetition is fine because it makes other changes to production code easier.
00:27:57.720 Then there's incidental repetition—the first type of concern, like described with the movie controller where they demonstrate structural similarities but lack conceptual overlap.
00:28:04.320 This can be solved by building a framework, so it's a good idea to establish a happy path and invite people to adopt it.
00:28:11.880 However, this can lead to actual conceptual repetition, where two entities represent the same idea.
00:28:20.040 This is an area where frameworks can address major issues, but it also introduces complexity.
00:28:27.060 Frameworks bring failure modes because they are often orthogonal to the problem domain.
00:28:35.940 Certainly, I’m building something like IMDb while working on a framework for controller overlap.
00:28:42.600 Most frameworks aren’t addressing business problems instead of technical ones.
00:28:49.320 Proceed cautiously, and if you're working in a pure monolith, avoid doing things without a clear purpose.
00:28:55.440 You can do this well, however. An example of doing it well, I’ll bring it up but won’t dive deep into details.
00:29:00.720 One thing that we’ve done well at Braintree is creating a framework for serializing API error codes.
00:29:07.740 Our product is software-as-a-service that we provide to our customers, and as we’ve been splitting the monolith into separate services, more services impact what error codes we send.
00:29:14.520 We built a shared framework between projects to ensure all services handle this systematically.
00:29:21.600 We understood our clients at the time so when building it, we knew what it needed to look like.
00:29:29.280 Thus, don’t be too quick to jump to create frameworks—frameworks should not be approached casually.
00:29:35.760 Deliberately cultivate patterns. As a recap: we see exponential code growth.
00:29:41.700 Facts about organizations experiencing this: headcount rising tends to follow similar trends.
00:29:47.580 No matter how diligent you are, this growth outpaces organic knowledge sharing.
00:29:54.840 Even at Braintree, we pair; no code goes to production without pairing, and yet it’s not enough.
00:30:00.600 New team members will use existing code as a sample of best practices.
00:30:07.560 The implication is: our chosen patterns have a long-lasting impact; they echo throughout time.
00:30:13.560 Even if our choice is to have no patterns, that too can create confusion.
00:30:20.520 Therefore, we must choose patterns intentionally and make them first-class concerns in application design.
00:30:27.120 We should make it clear what the chosen patterns are and ensure they’re the easiest option available.
00:30:34.080 When considering frameworks, think about patterns that come from enterprise or application architecture.
00:30:40.920 These higher-level design patterns can enhance understanding of Rails and help improve service encapsulation.
00:30:49.200 Be obvious and make it easy; it’s easy to think Rails is all about controllers, models, and views.
00:30:55.440 However, we should include a variety of services, ensuring that the people consuming them are also utilizing those patterns.
00:31:02.700 You also need to build other services while accounting for these actions that utilize those same patterns.
00:31:11.160 Lastly, let’s touch on commit-based documentation: we have code acting as documentation.
00:31:16.500 Self-documenting code is great, and we want to minimize comments but recognize their necessity.
00:31:23.160 There’s something missing in the balance, so let’s recapture the earlier point about the monolith.
00:31:30.780 While we understand how it works, we don’t always understand why it was initiated to begin with.
00:31:38.880 For example, here’s a commit from our codebase: my colleague Tony was correcting a typo and linking it back to a specific Trello story.
00:31:48.960 Similar example from years ago when we had a different work tracker; let's reference something connected.
00:31:56.040 These stories are part of your documentation—your commit messages are vital.
00:32:08.700 We should not delete or archive these messages as they provide context for us, giving insight into past decisions.
00:32:16.680 Regardless of how close you are with coworkers, ensure those stories contain a comprehensive context.
00:32:23.280 Recounting the importance—we want changes to be done in one atomic commit so it remains manageable in the future.
00:32:29.760 So to summarize: don't write hacks, beware of building frameworks, deliberately cultivate patterns, and utilize commit-based documentation.
00:32:35.640 I work for a company called Braintree; we do online payment processing. If you're a merchant, I would love to discuss our services.
Explore all talks recorded at Ancient City Ruby 2015
+4