Polymorphism for different choices. One of the earliest things I noticed was that any method I wrote tended to either make a decision or do something. This newly forming idea that each class and method should do only one thing led me to the habit of classifying methods as "doers" and "deciders". I don't remember where I first saw it - probably on the original wiki at c2.com - but seeing switch statements and if statements replaced by polymorphism really blew my mind. For a long time I tried to do everything I could with subclassing and implementing interfaces just to see what was possible. My code slowly began to be broken into objects that actually did things ("doers"), objects that decided what implementations would be used to do things ("deciders"), and objects that controlled an overall algorithm or flow ("managers") with not a single enum or switch statement in sight. Using polymorphism for conditionals makes code much easier to understand and abstracts unnecessary details.
Design patterns for familiarity. At first I resisted design patterns since none of them actually did anything. I had already seen code where only a small portion actually did work and the majority was excess plumbing and was dead-set on not writing anything like that. It took a while before I realized that the design patterns don't help with solving the domain problems; they help with solving the code problems. They help you identify and structure the pieces of your code that need to work together - they weren't useless baggage at all. In fact I had already stumbled upon the Strategy pattern (my "doers"), the Abstract Factory (my "deciders") and the Template pattern (similar to my "managers"). I've never found a need for some of them (Flyweight? Prototype? Memento?) but I began to see that my code was already doing the things some of the patterns were doing. By extracting those parts into their own classes that followed the pattern names and structures, my code was much cleaner and self-evident. This also broke me away from the naive view that a class represents a real-world noun and realize that a class actually represents a well defined and clearly named role or responsibility. Preferably a single responsibility that is familiar. Design patterns make common responsibilities and structures clear and easy to understand.
Immutability and higher order functions for safety and conciseness. Haskell has taught me a lot of really cool things like monads, monoids, functors, the beauty of concise language, and how to get the most out of the type system. Unfortunately the languages I use on a regular basis are no where near as concise or powerful. Fortunately all modern languages support some variation of higher order functions and immutability. Until Haskell I had never seriously thought about mutating state. After Haskell I became obsessed with it and came to the same conclusions as just about everyone else: eliminating mutable state eliminates many bugs and eliminating accessibility of mutable state eliminates many bugs. Concise code, meaning code that does one and only one thing with minimal boilerplate, is also easier to read, understand, and fit in your head - meaning even fewer places for bugs to hide.
Constructor injection for explicit dependancies. I used to have the constructor set up all the dependencies, wrongly thinking that's what everyone means by information hiding and designing for reuse. Just new-up an instance and start using it: you don't even have to know what it's using behind the scenes. How easy to use! How nice and thoughtful of me! At least until you need it to behave slightly different or need to use it in a slightly different context. Then you have problems. The kind of problems that are only solved by rewriting existing code - a bad and dangerous thing. But by making dependencies explicit you know what's going on. By making dependencies part of the constructor, you can change the behavior to suit the context without rewriting. And by thinking about dependencies as its own concept, your objects become much easier to use. Whether you use an IOC container or just use poor-man's dependency injection, identifying dependancies and making them explicit makes for code that has fewer surprising side effects and is easier to extend.
Refactoring for working with legacy code. I was aware of refactoring but wasn't sure how to apply it to most of the code that I had to work on in a day-to-day environment. Beyond the occasional Rename or Extract Method I didn't really do any refactoring; like design patterns it seemed to often be extra work that didn't actually do anything. Reading Working Effectively With Legacy Code changed that. I had an entire new arsenal of techniques to break dependencies, shrink giant classes, increase test coverage, clarify what the system actually does, and make the code just a little more pleasant to work with. Working Effectively With Legacy Code, and refactoring in general, is great enabler that allows you to bridge the gap between what happened to be and what could be.
Events and messages for decoupled systems. Until I really started using domain events, it seemed like creating a decoupled system was just a lofty goal that you could possibly approach but never get close to. I was wrong. Dependency injection had taught me about about dependencies and how to make them clear but I wasn't sure how to really get rid of them. This is how. With an event system you can remove dependancies between collaborating classes and add functionality without modifying existing code. Not only that, but all the public methods and properties that managed collaboration disappear and your classes expose just their own domain behavior. I still run into issues with sequencing or scenarios where I'm not sure how to do something with events from time to time but simple events and simple pub/sub drastically simplifies and decouples the domain objects and overall architecture.
I could write a post, or probably a small series, on each of these (or link to a hundred other sites). There have been other things that have helped me become a better developer (TDD, DRY, SOLID, debugging tricks, SICP, communicating with others, really listening to users, etc) but these are the ones that really shook things up and moved me in a new direction. What has changed how you program? What are you currently looking into? What do think your next big change will be?
Nice post. Regarding TDD, do you practice it religiously or more on and off? Pure TDD feels cumbersome to me in many cases (in my limited experience).
ReplyDeleteI'd say that I do TDD on and off. It can be very useful when you need a good design and you're writing something new. It can be a lot less useful when you already have a clear idea of how to design something or if there's a lot of existing code that's hard to change. If I really practiced TDD I would have to spend hours, or days, rewriting existing code just so I could write my own test and make a small change. I guess that in theory I agree with TDD but in practice I often don't. I do think it's useful to religiously follow some advice for a while to see for yourself where it works and where it doesn't.
DeleteThis article clues the reader in on the most effective little business outsourcing tips. The audience for this text is little to medium business homeowners. outsource web development company
ReplyDeleteNice post.
ReplyDeleteThanks for this post. You talk about program Revolutions and revelations. I have read your post very carefully. I found more information about program.To know web design ukraine click here. I am waiting for your next post.
ReplyDelete