C N Z 2 0 1 5 I S Y O U R C O D E TO O S O L I D ? https://www.flickr.com/photos/pie4dan/4567311801 Hello! I gave this talk in July 2015 at WDCNZ in Wellington, New Zealand.
H I E F C O N S U LTA N T D E V M Y N D U N I T E D S TAT E S C H I E F O F BA L D E A G L E S A N D F R E E D O M http://world-map1.org/map1520769_0_0.htm http://www.coolholidaygraphics.com/flagday/glittergraphics/flagdayglitter2.gif https://www.devmynd.com/images/team/sarah-mei-cutout.png I’m Sarah Mei. This is my dorky work picture, and I’m the Chief Consultant at DevMynd. I’m from the United States, specifically San Francisco. The US, as you know, is the chief of bald eagles and freedom. I copied that directly from the state department website…
R K O N N O R M A L C O D E BA S E S https://www.flickr.com/photos/pmarkham/5361277692 I’m usually working with teams managing large codebases that have become unwieldy and hard to change. This is a common problem, right now, among developers of all persuasions. In addition to the Ruby and JavaScript folks, I’ve heard it from folks who do .NET, python, CSS, Java, and PHP. This isn’t a new problem in the industry. IBM was having this problem in the 70s. It’s interesting that it’s again at forefront of our collective consciousness right now, though.
is easier to write than it used to be. There was the explosion of dynamic languages in the past 10 years, but also new frameworks in compiled languages, and the addition of usable functional languages. All of this has made it easier than ever to generate code in volume. You can achieve a large, unwieldy codebase faster than ever before! Perhaps even when you’re still a small company. You don’t need to be the size of IBM, anymore, to have their codebase problems.
teams failed at planning. Many teams need, at first, the type of very fast iteration that something like Rails brings. Companies that planned ahead for a large codebase, and put structures in place, couldn’t iterate as quickly. Some of them don’t exist anymore as a result. For a business, a messy codebase that works is better than a well-structured one that doesn’t. So we should all be so lucky as to have these problems.
need to make working in these codebases suck less, because this type of thing is what the vast majority of developers work on. We need to make working on this code easy and fun again. Some people attempt that maneuver this with microservices. That rant is another talk entirely.
this. Carving services out of a monolith and carving objects out of a large class are the same skill. If you haven’t been doing good object design in your main codebase, I guarantee you’re not going to design a good set of services. The skill is much easier to practice in a single codebase, where, if you’re wrong, you just have adjust object boundaries. If you have to adjust service boundaries when you’re wrong, the learning is much more expensive.
better at object-oriented design. It’s perhaps not as shiny as microservices, but from a cost perspective, it’s more responsible. Not coincidentally, we’ve been seeing a resurgence of interest in object-oriented design in communities that weren’t that interested a few years ago - particularly the dynamic language communities. There are now lots of books, and blog posts, and conference talks (including this one) about software design.
E X P E RT AT E V E RY T H I N G S E R I O U S LY, G E T I T A M A Z I N G B O O K http://www.sandimetz.com/ http://www.poodr.com/ This is the best of the modern takes on object design. Sandi Metz wrote this fantastic book on understanding objects. I highly recommend you read it, even if you’re not a Ruby developer.
E X P E RT AT E V E RY T H I N G S E R I O U S LY, G E T I T A M A Z I N G B O O K http://www.sandimetz.com/ http://www.poodr.com/ Now, I love this book, and all the blog posts, and all the conference talks. But what I do in my client work doesn’t really look anything like in there. The book uses example code being written from scratch to show you how to put the right boundaries around your objects. At first, going into large, monolithic, messy codebases, where no one heeded Sandi’s advice, and moving things around didn’t seem like ‘object-oriented design’ to me. For a long time I called it ‘refactoring.’ It turns out, though, that those are not separate ideas.
D I N G H OW TO A R R A N G E C O D E https://www.flickr.com/photos/matley0/3616669592 Before we go any further, I want to define some terms. Let’s start with software design. I’m not talking about architecture, or systems, just within a single codebase - software design is really nothing more (or less) than deciding how code is arranged.
D I N G H OW TO A R R A N G E C O D E https://www.flickr.com/photos/matley0/3616669592 Many people think that software design is something completely separate from programming. But in reality, when you’re programming, even if you’re not consciously making any decisions, you’re still doing design. Every time you put a function in this object and not that one, you’re doing design. You can try to do it ahead of time, and some people do, but the vast majority of software design done by developers in our industry is inline. Just like programming, you’ll be bad at it at first, but just like programming, you get better at software design the more you practice.
N T E D D E S I G N G R O U P I N G R E L AT E D F U N C T I O N A L I T Y I N O B J E C T S https://www.flickr.com/photos/mwanasimba/2901201955/ Now let’s be more specific and define object-oriented design. OOD is deciding how code will be arranged, grouping related functionality in objects. This does not sound anything like the wikipedia definition of OOD. It’s pretty abstract. Perhaps it will help us to talk about what object-oriented design is NOT.
E AT U R E A WAY O F T H I N K I N G O B J E C T- O R I E N T E D D E S I G N I S https://www.flickr.com/photos/56627607@N05/15635620289/ 1. OOD is not a language feature. It’s a way of thinking. You can write object-oriented code in CSS. You can write it in C, or JavaScript, or Java, or C#, or Ruby. Languages with explicit syntax support for objects are what you’ll hear people call “object-oriented languages.” That just means “languages in which it is more convenient to make objects”, but that’s too long for a wikipedia page title, so… Object-oriented design is a way of thinking about code arrangement. In some languages it’s easier to express than others, but it’s possible anywhere. Buy me a beer and ask me about object design in Haskell some time.
E AT U R E A WAY O F T H I N K I N G O B J E C T- O R I E N T E D D E S I G N I S A D E S T I N AT I O N A M E A N S TO A N E N D https://www.flickr.com/photos/56627607@N05/15635620289/ 2. Object-oriented code is not a destination. It’s a means to an end. You don’t write object-oriented code for its own sake, or because it’s somehow morally or professionally “better.” No particular way of arranging code is inherently better than any other way. Object-oriented design is a means that we use, mindfully, to move us toward some goal. And for most of us, that goal is ease of change.
what they want. They imagine one thing but they change their mind when they actually see it in action. Or the business shifts focus. Or a key person is replaced. Or it’s Tuesday - the only constant in software development is that the end goal shifts as we build it. And it wouldn’t do us any good to wait, because the act of building is what causes it to shift. In theory, object-oriented design makes it easier to respond to shifting requirements.
tell you that. But I’ll bet you most people in this room have been on a project where the code was parceled out into objects, and that made it harder to respond to shifting requirements, harder to change, rather than easier. To figure out how that happens, we have to take a step back and consider our goals when we’re designing software.
L OW C O S T O F U N D E R S TA N D I N G C O S T O F C H A N G E There are two useful axes to consider when we’re looking at different ways to design software. On the bottom, we have the cost of understanding the code (low or high). This is how hard is it to figure out what’s going on. On the side, we have cost of changing the code (low or high). Every choice about how you arrange code - every choice about software design - goes in one of these quadrants.
L OW C O S T O F U N D E R S TA N D I N G C O S T O F C H A N G E Let’s start with writing long procedures. Someone does a GET on /calendar to see their calendar for the month, and a procedure is executed - a list of instructions. It determines the date range, fetches events within that range from the database, draws the right shaped grid, places the dates on them and returns that page to the user.
L OW Procedures C O S T O F U N D E R S TA N D I N G C O S T O F C H A N G E It’s pretty easy to understand what happens in a procedure. Cost of understanding is low. Everything that happens is right there in an ordered list. The tradeoff, in a project with lots of long procedures, is that the cost of change is high. So procedures belong in the upper left. The biggest devil there is duplication, which forces you to change multiple places in the code to make a single logical change. You’ll know you have this problem if you end up touching every file in the project to make a change that seems like it should have been simple.
L OW Procedures C O S T O F U N D E R S TA N D I N G C O S T O F C H A N G E Some projects, having been burned by the high cost of change with procedures, go completely in the other direction. The system is made of innumerable tiny objects that each don’t do much. When you do that GET on /calendar, a RouteReceiver picks up the call…
RouteResolver that looks at the URL and figures out that it needs a CalendarRouteResolver, which it gets from a CalendarRouteResolverFactory, and the CalendarRouteResolver looks at what you’re requesting and instantiates a GetCalendarIndex object, which sends your params to a CalendarParamManager…and so on. The sequence of events isn’t written down anywhere in the code. You just have to trace it through to figure it out. Little pieces of functionality are spread across many classes.
L OW Procedures Set of Small Objects C O S T O F U N D E R S TA N D I N G C O S T O F C H A N G E A system like this is harder to understand than a procedure. The cost of understanding is high. A list of instructions will always be easier to understand than a set of objects. However, once you understand the system, the cost of change is low. Assuming you’ve got the right abstractions (a notion we’ll deal with in a moment), it’s relatively easy to take, for example, one params handler out and start using another. So a set of small objects goes down in the lower right.
L OW Procedures ⚠⚡ Set of Small Objects C O S T O F U N D E R S TA N D I N G C O S T O F C H A N G E Now let’s talk about this quadrant here. Danger zone! Code with a high cost of understanding AND a high cost of change is the worst of both worlds. There are two types of codebases here, both of which are distressingly common.
L OW Procedures Set of Small Objects C O S T O F U N D E R S TA N D I N G C O S T O F C H A N G E Really big objects The first is a codebase made up of really big objects. Perhaps the framework dictated an initial set of classes, and all behavior just sort of accreted onto them. The huge objects always seem to be the ones that are core to the application. They change with almost every commit, and changes go wrong easily, because all that functionality in one place means unintentional interference is almost a given.
L OW Procedures Set of Small Objects C O S T O F U N D E R S TA N D I N G C O S T O F C H A N G E Really big objects The wrong set of small objects The other type of codebase is small objects gone wrong, which is what happens when you try to break down a big class but don’t get the object boundaries quite right. Then it’s both hard to understand, because it’s objects, and hard to change, because for one logical change you still have to make changes in lots of different places. The wrong set of small objects is the worst-case scenario. Really big objects are bad, but not that bad.
Procedures Set of Small Objects L OW C O S T O F U N D E R S TA N D I N G C O S T O F C H A N G E Really big objects The wrong set of small objects So there are two questions here. The first: how can we get to the lower left? Can we? Is that the perfect solution that cannot exist? The second: how do we move our big lumbering codebases out of the upper right? It doesn’t really matter what direction we move; anywhere will be an improvement. But usually, when a codebase is this size, reducing the cost of change is worth increasing the cost of comprehension. So most people want to move down into sets of small objects.
Procedures Set of Small Objects L OW C O S T O F U N D E R S TA N D I N G C O S T O F C H A N G E Really big objects The wrong set of small objects This is where they start reading about things like SOLID and design patterns, hoping they can figure out how to make the move. Let’s talk about patterns briefly before we dive into SOLID.
W E M A I L User Let’s say you have a User class, and when a new user is created, meaning someone has signed up for your service, it automatically sends them an email to get them to confirm their account. Normally this is fine. If your user class is small and your object graph is uncomplicated, then it’s fine to leave this here. But a lot of times, the user gets to be one of the biggest classes in the system, and it can be annoying to have it send email every time you make one. You have to find ways to turn it off when you’re creating a user in your tests, and every test requires you to create a user - so to make it easier to turn off, you want to separate user creation from email sending.
You create a new class UserObserver, and you move the email functionality over there. UserObserver gets a creepy set of googly eyes so it can observe the User class.
! E M A I L N E W Now when a new user is created, the UserObserver notices, and sends email. That’s cool, right? You’ve reduced the size of your User class and made it easier to turn off email sending. You’ve made the code easier to change. But you’ve also made it harder to understand. You used to only have to look one place to see everything that happened when a user was created. It all happened in the User class. Now you have two places to look. Because it’s in a separate class, other people may not know it exists, let alone that they have to turn it off, and then be unpleasantly surprised when the users they create get emails.
L OW E A S I E R TO C H A N G E H A R D E R TO U N D E R S TA N D C O S T O F U N D E R S TA N D I N G C O S T O F C H A N G E If you’re starting out with a codebase in the upper right quadrant and you apply the observer pattern as we just talked about, you move it down here. (That’s the confounded face emoji, in case you were wondering.) You’ve made it easier to change but harder to understand. Applying a pattern improves changeability but worsens understandability.
then start looking for opportunities to apply them. They assume that making less-structured code into patterns is always a good idea. They don’t realize that everything has a cost. And determining whether it’s worth it is the hard part. At what point does lower cost of change outweigh higher cost of understanding? There’s no single answer. It’ll be different in different parts of your code, and it will be different at different times in the same part of the code.
awesome! Who doesn’t want “solid” code? Or to be a “solid” programmer? There are many object-oriented principles. Academia has been studying object orientation for decades. However, academics tend not to deal in volumes of code, so most of the principles are highly academic. In the 90s, Robert Martin took the five principles that seemed the most relevant to working software developers, and assembled this acronym. Let’s talk about what each letter means.
U T I L I T Y S O L I D We’ll be filling in this chart as we go. We have three columns: 1. The name of the principle 2. A summary of what it means (not what it says) 3. A measure of how useful it will be in our everyday developer life. The first letter of SOLID is S!
O N S I B I L I T Y P R I N C I P L E S O L I D https://www.flickr.com/photos/shenamt/8582808329/ S is for the single responsibility principle. It says that a class should have one responsibility, or to put it another way, one reason to change. This was first articulated by Rebecca Wirfs-Brock in the 80s. It’s a fancy way of saying that smaller things are easier to understand, and harder to mess up, than larger ones.
U T I L I T Y S S I N G L E R E S P O N S I B I L I T Y O N E R E S P O N S I B I L I T Y P E R C L A S S M E D O L I D As far as utility goes, it’s sort in the middle. The difficulty hinges on the definition of “responsibility.” If you’ve got a class that finds users, persists users, validates users, allows access to its related objects, and contains business logic related to users, you could plausibly say it’s got one responsibility: it manages the user.
U T I L I T Y S S I N G L E R E S P O N S I B I L I T Y O N E R E S P O N S I B I L I T Y P E R C L A S S M E D O L I D You could equally plausibly say that all of those things are separate responsibilities that all belong in different classes. The principle doesn’t give you guidance because there is no universal right answer. Sometimes it makes sense to put all that together; other times it doesn’t. And that shifts over time, even for the same codebase. The answer to every question in software development is “it depends.”
D P R I N C I P L E S O L I D https://www.flickr.com/photos/28481088@N00/4063800774/ O is for the open/closed principle. This is usually stated as “a class should be open to extension but closed to modification.” Bertram Meyer came up with this in the 80s. It’s a fancy way of saying that editing existing code is more difficult and more error-prone than just adding new code, so: arrange your codebase such that we can add new functionality just by writing new code.
U T I L I T Y S S I N G L E R E S P O N S I B I L I T Y O N E R E S P O N S I B I L I T Y P E R C L A S S M E D O O P E N / C L O S E D D O N ’ T E D I T C O D E ; A D D N E W C O D E L O W L I D As far as utility goes…it sounds great, doesn’t it? But it’s hard to conceive of how it could ever happen in a codebase of significant size. It’s not super practical day-to-day.
T U T I O N P R I N C I P L E S O L I D https://www.flickr.com/photos/vpickering/14416940341/ L is for the Liskov substitution principle. This is perhaps the most academic of the SOLID principles. It is a precise, mathematical statement. Here it is:
of type T. Then Φ(y) should be true for objects y of type S where S is a subtype of T. Let theta(X) be a property provable about objects X of type T. Then theta(Y) should be true for objects Y of type S where S is a subtype of T. …ok…
T U T I O N P R I N C I P L E S O L I D https://www.flickr.com/photos/vpickering/14416940341/ It’s a fancy way of saying: anywhere you can use an instance of a class Foo, you should be able to use an instance of class Bar that subclasses Foo. And nothing should go wrong. The Liskov Substitution Principle was formulated by Barbara Liskov in 1987, when she was in her late 40s.
U T I L I T Y S S I N G L E R E S P O N S I B I L I T Y O N E R E S P O N S I B I L I T Y P E R C L A S S M E D O O P E N / C L O S E D D O N ’ T E D I T C O D E ; A D D N E W C O D E L O W L L I S K O V S U B S T I T U T I O N I N H E R I TA N C E . I T ’ S A T H I N G . L O W I D The Liskov substitution principle basically says “inheritance…it’s a thing.” It seems pretty obvious to us at this point, but when it was first articulated, it was not at all so obvious. The LSP was such a good idea that, since it was introduced, we have baked it into our languages. It’s now part of the air we breathe, and we don’t even notice it’s there. It’s had a huge impact on the way we write software, which, I would guess, is why Martin chose to include it in SOLID, despite its academic nature. However, while it is fundamental, it’s not really a great source of practical help day-to-day. Utility is low.
G R E G AT I O N P R I N C I P L E S O L I D https://www.flickr.com/photos/66126733@N04/8477516123/i I stands for the interface segregation principle, which says that classes should only have to depend on the part of an interface they actually need. It’s a fancy way of saying “don’t let changes to unrelated functionality in a class affect other things that use it.”
E I N T E R FA C E S ? Musician AlbumCreator recordSong editSong mixSong sellMerch playSetList Gig Let’s say you have a class Musician that has five methods on it: recordAlbum, editSong, mixSong, playSetList, and sellMerch. It has two classes that consume it: AlbumCreator, and Gig. They use different sets of methods and are not related to each other.
E I N T E R FA C E S ? Musician AlbumCreator recordSong editSong mixSong sellMerch playSetList Gig driveVan Then you add another method - driveVan - that is only used by Gig. You’d expect at this point that Musician and Gig would have to be recompiled, but it turns out all consumers must be recompiled. Even AlbumCreator, which didn’t change at all. With just two consumers - who cares? But what if Musician had hundreds of consumers that had to be recompiled every time you make any change? You can see how it’s a huge pain. It makes re-running a test, for one thing, a very long process.
E I N T E R FA C E S ? StudioActions AlbumCreator recordSong editSong mixSong sellMerch playSetList Gig driveVan VenueActions Interface segregation says you should break Musician into two different classes - StudioActions and VenueActions. That way, when a new method is added to VenueActions, AlbumCreator won’t need recompiling.
U T I L I T Y S S I N G L E R E S P O N S I B I L I T Y O N E R E S P O N S I B I L I T Y P E R C L A S S M E D O O P E N / C L O S E D D O N ’ T E D I T C O D E ; A D D N E W C O D E L O W L L I S K O V S U B S T I T U T I O N I N H E R I TA N C E . I T ’ S A T H I N G . L O W I I N T E R FA C E S E G R E G AT I O N M A K E T H E A P I S M A L L H I G H D Anyway - it makes more practical sense in compiled languages, so many of us who primarily use dynamic languages dismiss it as unrelated. However, it does have a really really useful core idea: if different consumers of a class use non-overlapping sets of methods, that’s a sign that that class has multiple responsibilities (see S). So I put its utility high, relative to the other principles we’ve looked at here.
I N V E R S I O N P R I N C I P L E S O L I D https://www.flickr.com/photos/hansmaulwurf/13702053625/ And finally, D, which stands for the dependency inversion principle. Not dependency injection, which is one particular implementation of this idea. Dependency inversion says “depend on abstractions rather than concretions.” This is a fancy way of saying: if you want to make a new thing, a new instance of a class, inside another class, think about creating it outside and just passing it in to the constructor, instead. This has the effect of moving all your choices towards the edges of your system.
U T I L I T Y S S I N G L E R E S P O N S I B I L I T Y O N E R E S P O N S I B I L I T Y P E R C L A S S M E D O O P E N / C L O S E D D O N ’ T E D I T C O D E ; A D D N E W C O D E L O W L L I S K O V S U B S T I T U T I O N I N H E R I TA N C E . I T ’ S A T H I N G . L O W I I N T E R FA C E S E G R E G AT I O N M A K E T H E A P I S M A L L H I G H D D E P E N D E N C Y I N V E R S I O N M O V E C H O I C E S T O E D G E S O F S Y S T E M H I G H There are lots of “dependency injection” frameworks. Angular, .NET, Java. They make it easier to test classes. Given that most of them have essentially become gigantic global state, though, it’s sometimes a bit difficult to tie implementations back to the principle. But dependency inversion does seem to be, theoretically, something a lot of people see. Utility high.
U T I L I T Y S S I N G L E R E S P O N S I B I L I T Y O N E R E S P O N S I B I L I T Y P E R C L A S S M E D O O P E N / C L O S E D D O N ’ T E D I T C O D E ; A D D N E W C O D E L O W L L I S K O V S U B S T I T U T I O N I N H E R I TA N C E . I T ’ S A T H I N G . L O W I I N T E R FA C E S E G R E G AT I O N M A K E T H E A P I S M A L L H I G H D D E P E N D E N C Y I N V E R S I O N M O V E C H O I C E S T O E D G E S O F S Y S T E M H I G H Now that we’ve filled it all in, let’s look at this chart for a moment. SOLID has principles of vastly varying degrees of utility, or concreteness. Utility, or immediately applicability to code you’re writing today, is one end of a spectrum. The opposite end is abstraction, which describes a general rule that sounds like a good idea, but is hard to connect to code you’re looking at in your editor.
U T I L I T Y S S I N G L E R E S P O N S I B I L I T Y O N E R E S P O N S I B I L I T Y P E R C L A S S M E D O O P E N / C L O S E D D O N ’ T E D I T C O D E ; A D D N E W C O D E L O W L L I S K O V S U B S T I T U T I O N I N H E R I TA N C E . I T ’ S A T H I N G . L O W I I N T E R FA C E S E G R E G AT I O N M A K E T H E A P I S M A L L H I G H D D E P E N D E N C Y I N V E R S I O N M O V E C H O I C E S T O E D G E S O F S Y S T E M H I G H O and L are the most abstract, I and D are the most concrete, and S sits in the middle. We’ve got at least three different levels of abstraction here. None of them, even I or D, seems like it’s actually useful day-to-day for refactoring code. It’s nice to say make the api on an object small, but … that ship has sailed. We need things that are more concrete to guide our everyday programming. But it’s not clear how to find them.
we talked about earlier, are more concrete, but still don’t help us with when. We need something else - our last missing puzzle piece - to help us actually move our code forward. Here’s an idea that may be useful.
T I C S https://www.flickr.com/photos/teegardin/6150427712/ “Strategy” and “tactics” are military concepts. I didn’t grow up in a military family, so for most of my life, I thought of these two words as essentially synonymous. But they’re not. A strategy is a high-level objective that will move us closer to some goal. A tactic is something you do on the ground to achieve that strategy. Here’s an example of how these are different.
OA L : P R O M O U N TA I N E E R S T R AT E G Y: C L I M B T H I S M T N TA C T I C S : T H E R O U T E U P https://www.flickr.com/photos/frenchy/222016256/ Your goal: be a pro mountaineer. Your strategy: climb this mountain that few people have climbed before. You’re starting from the bottom (lower left) and need to make it to the summit (middle top). There are no trails - you need to figure out your own path up. The strategy is where you want to be at the end of the day, and your tactics are how you get there. Tactics includes the planned route, and contingency plans, and are guided by (and may be changed by) the strategy as you walk up the mountain.
OA L : P R O M O U N TA I N E E R S T R AT E G Y: C L I M B T H I S M T N TA C T I C S : T H E R O U T E U P 1 2 3 https://www.flickr.com/photos/frenchy/222016256/ Possible routes include: 1) walking along the treeline and then going up the right-hand ridge. 2) climbing this rockface, sliding along the little shelf there, and then along the left-hand ridge to the top. 3) just going for it, straight up the face. They all involve different tactics - walking vs. climbing vs. rappelling, etc. The actual route you take will depend on the weather, your skills, your gear, and many other things.
N TA I N E E R S T R AT E G Y: C L I M B T H I S M T N TA C T I C S : T H E R O U T E U P \ O / https://www.flickr.com/photos/frenchy/222016256/ Once you choose a route, it’s still probably not what’s actually going to happen. If you get to the base of the ridge and discover that an avalanche has made it too perilous to go up that way, you change tactics, because your strategy of climbing the mountain is no longer in line with your original tactics. Part of your tactics includes determining when to change tactics.
N TA I N E E R S T R AT E G Y: C L I M B T H I S M T N https://www.flickr.com/photos/frenchy/222016256/ What would happen if all you had was a strategy? All you know is “ok, there’s that mountain, I need to get to the top of it.” If you set out to achieve this strategy without working out any of little steps you can take to get there…it’s not likely to work out for you very well. You may get to the top accidentally, but it’s more likely that you’ll try a few fruitless paths, find yourself in a valley you can’t get out of, and then have to signal to the park rangers to airlift you out. You need to have tactics in mind, or you probably won’t achieve your strategy.
A B L E S O F T WA R E S T R AT E G Y: TA C T I C S : O O D E S I G N S O L I D https://www.flickr.com/photos/sunfox/5084875405/ Our actual goal is changeable software, rather being a pro mountaineer. Changeable software is the promise of objects. One way to think about strategies & tactics for changeable software is that our strategy is object oriented design. Our tactics would then be things like SOLID & patterns. This is how object-oriented design is taught, particularly in academia, and it is how most developers look at it, whether or not they could articulate it.
A B L E S O F T WA R E S T R AT E G Y: TA C T I C S : O O D E S I G N S O L I D https://www.flickr.com/photos/sunfox/5084875405/ There’s a problem, though, with this picture of the world. “Object-oriented design” isn’t actually a strategy. Remember, a strategy describes what we want our world (in this case our codebase) to look like at the end of the day. “Object-oriented design” doesn’t describe that. On the other hand, SOLID is reasonably good at describing what our codebase should look like when it’s “finished.” Hmm.
U T I L I T Y S S I N G L E R E S P O N S I B I L I T Y O N E R E S P O N S I B I L I T Y P E R C L A S S M E D O O P E N / C L O S E D D O N ’ T E D I T C O D E ; A D D N E W C O D E L O W L L I S K O V S U B S T I T U T I O N I N H E R I TA N C E . I T ’ S A T H I N G . L O W I I N T E R FA C E S E G R E G AT I O N M A K E T H E A P I S M A L L H I G H D D E P E N D E N C Y I N V E R S I O N M O V E C H O I C E S T O E D G E S O F S Y S T E M H I G H Single responsibility principle, for example, would make a good strategy. It describes a state of our codebase in which there is one responsibility (at the right granularity!) per class. If we had that, our codebase would be more changeable. Open/closed describes this codebase utopia where you never have to edit code to add features. In fact all these principles are, at some level, descriptions of when you know your code is “right.” “If your code were like this, it would be more changeable.”
A B L E S O F T WA R E S T R AT E G Y: TA C T I C S : S O L I D P R I N C I P L E S https://www.flickr.com/photos/sunfox/5084875405/ SOLID is a great set of strategies. Our problem is that we’ve been using them like tactics. For example, you can’t apply single responsibility principle directly to a thousand-line class. A class like that has a muddy set of abstractions spread across multiple methods each that are hard to distinguish. When you squint at the class and envision how you’d break it up, you’ll most likely be wrong. The abstractions are, by definition, hard to see, because if they weren’t, you’d have done something about it already.
A B L E S O F T WA R E S T R AT E G Y: TA C T I C S : S O L I D P R I N C I P L E S https://www.flickr.com/photos/sunfox/5084875405/ Trying to just eyeball a large class and see what the right abstractions are is like trying to head towards the summit without planning a route ahead of time. But we do this a lot! I’ve been on many teams where a class got too monstrous, so they’d take a week off from doing feature work. They’d spend that week eyeballing the class and trying to refactor it into the right abstractions. Teams do these a lot. I call this a “stop-the- world” refactoring.
A B L E S O F T WA R E S T R AT E G Y: TA C T I C S : S O L I D P R I N C I P L E S https://www.flickr.com/photos/sunfox/5084875405/ What’s wrong with a stop-the-world refactoring? Refactoring is part of being a good software engineer, right? Taking time to clean up & make code better is important. Right?
interesting things happen when you stop the world to refactor: 1. The product team is unhappy. You’re taking a whole week “off,” from their perspective, meaning you’re not working on anything they can see. 2. Because you’re time-limited, you feel pressure at the end of the week to break that class up somehow, even if you’re not entirely sure yet where the right boundaries are. #2 is exactly how you end up with one of these.
A K E A S TO P - T H E - WO R L D R E FA C TO R I N G WO R K I S TO N OT D O I T . http://www.imdb.com/title/tt0086567/ The only way to make a stop-the-world refactoring work is to not do it. It’s like global thermonuclear war. The only winning move is not to play. When we apply strategies as though they were tactics, we end up with these ham-handed, mostly unplanned, unpopular changes that, more often than not, leave us in a worse place than before.
refactorings, we’re doing this. And it’s frustrating. Ever wonder why good developers who started out in an OO language are turning to functional programming? Certainly novelty is part of it, but there’s also a strong undercurrent of fundamental criticism of OO. They say it just doesn’t work. And that’s because there’s a summit there, and they can see it, and it seems like they should be able to get to it. But they can’t.
gets us up there in explicit, small steps. We need things we can do every day, as we’re doing feature work, to inline the refactoring time and allow the right objects emerge from the mess over time. Right now we don’t have very many of tactics. One thing we do have, though, is lots of rules that tell us what not to do.
Demeter that tells us that `post.comments` is ok, but more method calls chained on the end of it gets a big ol’ NOPE. When we make long chains of method calls on different objects, we’re introducing coupling we don’t want. So, ok, don’t do that.
when something might be wrong with our code. We have are very few ‘do’s. Most of our rules at this level are ‘don’t’s. We need ‘do’s. We need a set of tactics. Let’s make one! I’ve made my own acronym. :D
which is cool. But when I’m trying to go up it, what I really need is stability. I want it to, for example, not be a volcano, not avalanche out from under my feet, not drop boulders on me, etc. I need it to be solid, sure. But at a more immediate level, when I’m making my way on the ground, I need stability. The same is true in my codebase. I want to know that if I make a change in one part of the code, it won’t cause an avalanche on the other side of the mountain.
Y O U R C O D E https://www.flickr.com/photos/pedrosimoes7/169983321/ S: smell your code. Study code smells and the other ‘don’t’s, so that you can start identifying things you could fix. Martin Fowler’s Refactoring book is great for this. I’ve worked with teams who would pick one code smell for a lunch & learn each week, and discuss and look for examples in their code. You want to start noticing & naming the problems in the code you look at, even though you won’t be fixing them all yet.
M S F I R S T S TA B L E https://www.flickr.com/photos/ian-arlett/13706787684/ T: tiny problems first. In messy code, there will be lots of smells. They’ll overlap and intertwine and interfere with each other. It will be hard to see what should be made with all this mess. The best way to get started is to pick a really small problem that you know how to solve in a very concrete way, and just fix that. For example, rename a variable whose usage has diverged from its name.
P R O B L E M S F I R S T Pick the smallest problem to fix even though you see enough of the bigger problems to start guessing their answers. Your goal is to see the large problems better, by clearing away the small problems obscuring them. The more information you gather about these larger problems, the more likely your eventual abstractions will be right. Proper abstractions are worth waiting for. Let them emerge from the code you have by clearing away the easy cruft.
T S S TA B L E https://www.flickr.com/photos/horiavarlan/4273968004/ A: augment your tests. You will almost certainly have to do this to be able to refactor large classes. You need integration tests at one level higher than the class you’re working on, and you need these in place before you start doing any of this. For example, in a server-side MVC application, if you want to refactor a big controller method, you’ll need view-level integration tests in place that exercise it. If you want to refactor a big model, you’ll need controller-level integration tests.
E N T T E S T S In general: test behavior, not implementation. That’s why you go one level up. You need tests that describe the behavior you want to keep. This may mean getting rid of some lower-down unit tests, and/or writing a new set of a tests at a level you haven’t had them before.
https://www.flickr.com/photos/mjonasson/19228720295/ B: back up when it’s useful. When the code has an abstraction in it that is no longer serving you well, sometimes the most useful thing to do is to ‘rewind’ the code into a more procedural state, put all the duplication back, and start again. It is much, much harder to move from a procedure to the right set of objects, than from the wrong set to the right set. Don’t get caught by the sunk cost fallacy. Don’t forge ahead with a set of objects that don’t even fit now, let alone the future.
T B E T T E R T H A N Y O U F O U N D I T https://www.flickr.com/photos/spierisf/6989462184/ L: Leave it better than you found it. During any one expedition into the code to add a feature or fix a big, you won’t be able to fix all the problems you see. (Can’t hug every cat! [Hello memes from 2008.]) Sometimes the only thing you have time to do alongside your stated goal is rename a method so it describes its behavior better.
I T B E T T E R T H A N Y O U F O U N D I T That action seems so small, doesn’t it? Our instinct is to save a bunch of those up and do them at once. Don’t give into that stop-the-world temptation, though. Fix one thing, the smallest thing, while you’re working on a story that concerns the code. I was a girl scout, and one of our rules when we were out camping was that we always left a camp site in better shape than when we arrived. Over time this made a better experience for everyone, including ourselves.
I T B E T T E R T H A N Y O U F O U N D I T When you change that one method name, you’re removing a little piece of cognitive dissonance. The next person to come through this code will be able to understand it a little more easily. Maybe the big abstraction will suddenly be obvious. Or maybe they’ll just fix the next smallest thing. The key insight is that little things add up over time.
R E A S O N S S TA B L E https://www.flickr.com/photos/an_untrained_eye/5012331537 E: expect good reasons. Assume past developers had good reasons to write the code they did. Some code looks so horrible that we think, “what idiot wrote this? Why would anyone ever do it this way?” And then you run git blame and find out it was you, six months ago. Or it was one of the devs you really admire.
C T G O O D R E A S O N S There are forces at work on the code beyond the developer’s experience and technical skill. Deadlines, relationships with other groups like product, QA, and operations, company financial situation…all these and more leave their fingerprints on the codebase. Start thinking about the social pressures that affect your codebase, and many things will make a lot more sense.
C O D E T T I N Y P R O B L E M S F I R S T A A U G M E N T T E S T S B B A C K U P L L E AV E I T B E T T E R E E X P E C T G O O D R E A S O N S These are little things you can do to inline-refactor large classes as you’re making progress on features. You don’t have to stop the world. You can rebuild trust with your product team, and as a bonus, you’re much more likely to end up with the code you want. It’s a cycle. Keep fixing the small problems, and the solutions to the large ones will become obvious.
C O D E T T I N Y P R O B L E M S F I R S T A A U G M E N T T E S T S B B A C K U P L L E AV E I T B E T T E R E E X P E C T G O O D R E A S O N S You might ask, don’t you still have to stop the world sometimes? After all, once you’ve cleaned up enough of the small problems so that the solution to the larger problem becomes obvious, you do have to still actually solve the larger problem.
about this. “For each desired change, make the change easy (warning: this may be hard), and then make the easy change.” When you remove small problems, the big problems become obvious, AND they become much easier. You amortize the cost spent fixing the big problems and as a result you end up with higher-quality solution. And when you see it, will look, in retrospect, so completely #@$ %ing obvious.
may be hard.” The STABLE cycle won’t always prevent you from extracting the wrong objects, or trying a fix that doesn’t work. You’ll still have go down some paths that lead to dead ends. But in this process, that’s expected. Back up & try another. That’s why “back up” is one of the essential steps in this process.