Are You Limiting Your Abstractions, or Your Seams?
Here are some fun facts about me:
1.) I drink liquids from cups, glasses, and cans.
2.) I own tables.
3.) My baby recently learned to stand and walk.If you’re reading this, you probably share #1 with me. There’s also a good chance that you share #2 with me. If you don’t, I’d really recommend that you give tables a try, as they are great places to put your cups, glasses, and cans.But #3 is different. It’s what they call a “game-changer.” If you are a parent, you know that once a baby gains mobility, the rules of the house change. Such as: Table edges are no longer such a great place to put cups, glasses, and cans. If you put a drink down on a table within reach of the baby, within five minutes the baby is going to see it, walk up to it, and knock it over. And he’ll be smiling while he does it. If I just turned you against having kids forever -- don’t be like that. You can still use tables. However, those few inches from the edge of the table to the middle become really important. Put a drink on the table, but put it in the middle so the baby can’t reach. See? It’s still hard. My wife and I sometimes forget. Then when the baby comes, we have to rush over and move the cup away. Or we’ll see each other put a cup on the edge and we’ll say, “The baby’s going to knock that over” Or on rare occasions, we put the cup on the edge and we get away with it. Whew! In my house, though, it’s really only a matter of time. You put a cup on the edge of the table, and it’s going to be moved by someone. Change is coming.Limiting Your “Abstractions”I’ve seen a general trend amongst many of the .Net programmers I follow on the internet: “Limit your abstractions.” It sounds like a great idea, as nobody wants too many abstractions in your system. If you do not need an abstraction, you should not create it. I’m on board! Oh, wait, what’s that word I saw again? “Need.” Hmm... now I’m afraid. Saying you need something is very dependent on many facts that are specific to your situation. This is why when someone asks you if you should create an abstraction for [X], the “wise” answer is: “It depends.” Personally, I hate “it depends” because it’s a phrase usually used to throw away principles. Depends on what!?!? Let’s whittle the idea down until we can find the principle! Frankly, I think there are people who’d rather live in this ambiguous “it depends” because it’s a lot easier than finding the answer.So let’s look at how this general “limit your abstractions” idea is getting applied. Let me just thumb through my memory real fast to produce an example I’ve seen multiple times... ok, found it. Oh, look, it’s a controller, and the issue in question is what we put in it. Here’s an example of a controller action for a page that shows all of the products of a website. public ViewResult Index(){
var products = productRetriever.GetAllProducts();
return View(“Index”, products);
}It looks simple, but I see an abstraction: IProductRetriever. That’s brought in, I assume, through dependency injection, and then it’s used by the controller to get the products to show on the page. But we’re supposed to “limit our abstractions,” so let’s see how we get rid of that abstraction. public ViewResult Index(){
var products = MyDatabaseSingleton.Products.Where(x => x.IsActive);
return View(“Index”, products);
}No product retriever abstraction. Gosh, I bet you could instantiate this controller up without any constructor arguments or IoC container. It just hooks to... a database context singleton... then references a property to return an IQueryable of the Products, then …. sigh.... the controller filters out the inactive products.This does things that anybody who has read Clean Code knows you’re not supposed to do. But then the wise “it depends” crowd will reply “Why not?” It works. You can test it by hooking your tests to the database. Slow tests? Nope, we hook the tests to an in-memory database. The controller taking responsibility for filtering active products? Hey, it’s the only place we use products, why make an abstraction just to filter them out? We can always “refactor” later!Now, I’d argue that the controller should be responsible only for orchestrating the interaction between the website and the application. It should only set what page to show and what content to show on it, and that’s it. But in a simple case like this, my solution requires more code. You’d have to make the interface, you’d have to make the implementation for that interface, and you’d have to wire it up in your IoC container. The tightly-coupled takes less code, so why not do that and make them more complex later if necessary?Change Is ComingI can give you one big reason: Change is coming. If this application is actually being used, it’s going to be changed to better fit the needs of its owners and users. And when the changes come, the “abstraction-limited” solution quickly falls apart.But let’s try this out. Let’s say that a new rule comes down, and that I shouldn’t show products that are expired. Now my call has: var products = MyDatabaseSingleton
.Products
.Where(x => x.IsActive)
.Where(x => x.ExpirationDate > DateTime.Now);Now let’s say that if the user is signed in as an admin, then they can see everything. Now let’s say that if a regular customer is signed in, he can see only products that are associated to his company’s account.Now let’s say that if a regular customer is affiliated with a company from Iran, they see no products.Now let’s say that if the price of a product is zero dollars or less, we don’t want the product shown because there’s obviously a problem.Now let’s say that we also want a sitemap page that shows the same products as this page, but with different views.Somewhere along this list, the wise “it depends” developer will say “Of course, now it’s more complex and we have multiple uses, so we should refactor.” This is what I mean when I say it’s easy to be an “it depends” type of developer, because you always have an out.Seams are ImportantWhen you hardcode your database access in your controller, you’re not just limiting your abstraction -- you’re limiting your seams. As defined in “Working Effectively With Legacy Code,” Michael Feathers defined a seam as “a place where you can alter behavior in your program without editing in that place.”The “product retriever” interface is more than just an excuse to use your fancy IoC container -- it’s a seam that we can use to restrict the changes to our product catalog to one place, rather than having them bleed all over our application. I could make every change that I mentioned above without touching the controller again. In fact, the only reason I’d have to change that controller action is if that particular action changed. Business logic is stowed away to the place it’s going to be anyway.Without the abstraction, changes unrelated to the site itself will bleed into the controller as soon as the first change comes through. The only question at that point is how much bleeding the developer will allow before he adds the abstraction that he should have had in the first place.You’re Writing Legacy Code From the StartAnother important idea from Michael Feather’s book is the definition of legacy code as “code without tests.” And I honestly believe that you are headed down that path when you start in a position where you have no alternative but to rewrite when change comes.Think I’m crazy? Well, lets say you take testing seriously and you write the integration tests to cover your database-touching controller. From my original example, you’ll have at least the following three tests:1.) The action returns an index view.
2.) The action returns active items.
3.) The action returns inactive items.If you’re following TDD, you’ll probably have more tests than this, but that’s not what I’m arguing here.Now the changes come. 4.) When admin is signed in, return inactive items.
5.) When non admin is signed in, do not return inactive items.Then more changes.6.) When geolocation suggests Iran, return nothing.
7.) When price is zero, return nothing.
(etc)Notice how you have more tests for data retrieval behavior than for your controller behavior? That’s because your controller is doing more than it should be responsible for.By now, most people would have figured out that a separate class for retrieving products is necessary. But if you’re testing, you can’t just move the code -- you have to make the tests work too. But what tests? All of your tests are tied to a controller. Are you going to painfully rip the database-related tests out into a new test class, rename classes, set up new variables, etc.? Or are you going to introduce the abstraction but keep the tests on the controller? Or are you going to just start over? Or are you just not going to test?I never want to say that testing is bad, but when you don’t respond to the problems they show you and when you don’t respect SRP, the tests you write almost make it harder to refactor. You’re not just wrong -- you’re cemented in with a bunch of test code that continually verifies that you’re wrong. That little bit of time and code you saved by gluing your controller and database together is not much different than putting a glass on the edge of a table in my house. You may get away with it. Maybe the baby will not try to grab it. But you know the baby is around, you know the baby likes to grab, and you know that he’ll spill it if he gets to it. So why not just put in that tiny bit of effort to put the glass out of his reach?If you had the IProductRetriever (or whatever you want to call it) abstraction in the codebase from the beginning, changes would be a lot easier today.If You Really Want to Limit Your Abstractions...Switch to Ruby. Or some other language or system that doesn't require them. That’s what I did, I’ve gone almost six months without creating an interface or touching an IoC container. Due to the flexible nature of Ruby, I don’t have to give up the seams. I’m always capable of hopping into any object or class and modify behavior on the fly. And it’s a lot of fun.I think that’s the real solution to the abstraction problem. Don’t fight the language and don’t give up the tests. If you’re coding in C#, stick with the methods that make testing easy (or possible) and try to make those simpler. Yeah, you’ll have a lot of interfaces. But when change comes, you’ll be ready.

