Am I the only one who finds this post confused and unnecessarily mean? Roughly, the post goes like this:
1. DI is bad, mkay?
2. Ruby offers the best alternative to DI.
3. BTW, programmers *are* their languages.
4. Hence Ruby programmers are the best. QED.
What is odd is that #3 is offered almost as an aside, with nothing more than a link to the wikipedia article on "linguistic relativity" for support, and yet it poisons this post and turns it into a mean-spirited rant. It makes the entire post a personal attack on anyone who dares to disagree with it's assertion.
This is not okay! I happen to dislike DI even more than the OP, but to attack people on a personal level for liking it is just plain mean, and unnecessarily so.
When you assert the identification of self with technology preference, you reenforce a damaging idea that has no merit, which is indeed the very idea that ensures that such discussions often have more heat than light.
Shame on you, David, for using your position to promulgate an idea that is not only useless, but actively damaging to the community of programmers.
Testability isn't the only good thing about dependency injection. It's much easier to reason about OO code when objects only tend to interact with objects that they have been explicitly given. Too much willingness to introduce static dependencies increases the risk of creating classes with too many collaborators and/or responsibilities. This is a good example of how a testable design is often also a just plain good design.
Sure, the given examples of "Time.now" or "new Date" are innocuous and I agree that it's a good thing that Ruby allows us to write this type of code without creating a testability issue. But too many people abuse static dependencies for things like database connections which are:
1. well-suited to being represented as proper objects that are passed around
2. better off confined to a small area of the codebase
This is a complex issue and there's a delicate balance that needs to be struck. Otherwise you can end up with a codebase where a few classes are all up in everyone else's shit. That's when you find yourself in embarrassing situations such as some code not working without a database connection even though there's no real need for that beyond the tangled web of static dependencies chaining it to the database class.
Calling out DI is a bit of a misdirection as most have come to know DI I think.
What he's really arguing against here is more basic: Composition.
Pragmatically the examples makes some sense. Why not stub out any global like this though? Because it breaks encapsulation and makes the program harder to understand.
Instead of lifting dependencies up to the level of method parameters with good composition, now you have to truly grok the entire method before you can have any confidence you even know what to stub out, and wether that's actually going to produce the expected results.
So while I fully endorse this specific, localized, easy to understand example, the poor composition of many Ruby libraries is what tends to make working with Ruby code so damn hard (IMO, see: Bundler). It's not enough to read method signatures. There are no explicit interfaces. Only implicit ones you devine by understanding the full breadth and scope of how parameter A is used in method B, all the way down the stack trace.
Fundamentally here DHH isn't talking about "Dependency Injection". He's talking about Composition and a pragmatic example of breaking Encapsulation. While sure there are 101 ways in which breaking Encapsulation can be a useful, pragmatic technique to employ for the seasoned code-slinger in the small, in the large it makes for more difficult to understand, and therefore less maintainable code.
I find many of these recent posts by DHH a bit ironic considering the subtext of a guy who read the PoEAA, then went off and wrote an MVC web-framework, packed with a nice Table Data Gateway, then proceeded to confuse Unit for Integration tests, and soap-box on the evils of Design Patterns in general.
[EDIT]
PS: The obvious example for making it easy to test, without breaking encapsulation, would simply be to avoid globals and use a default parameter.
def publish!(current_time = Time::now)
self.update published_at: current_time
end
TA-DA. So trivial examples might show how a very shallow example of monkey-patching can be a nice convenience, you also have simple "fixes" that actually take _less_ code to implement.
You could easily come up with deeper stacks, presenting more difficult problems, but then you're not really making a great case for the beauty and simplicity of monkey patching if I have to have such a deep understanding of the side-effects in your code before I can even start making sense of your tests.
I don't like adding that parameter at all. When someone else comes across your method, will they know whether #publish! is meant to have a changeable time in real use? You don't want test-specific functionality leaking into real code.
But it's not "test-specific functionally", is it? The underlying object has a published_at attribute. Don't you think it means something?
The reality, then, is that we don't publish things, but publish things as of certain times. Only by putting the concept of publication time in the interface do we communicate the true semantics of publication.
To look at it another way, if you call the publish! method without understanding how publication time affects the thing you're publishing, aren't you missing something important?
So, to make the semantics of publication clear, the publish! method ought to have a publication-time parameter of some sort. (But it shouldn't be called current_time, as that name doesn't reflect the actual semantics of publication. We don't want to suggest, for example, that it requires time travel to schedule things for future publication.)
It's "test-specific" if the only time you ever specify that parameter is in a test. You're right that published_at is an attribute that can be set, but if you just wanted to set published_at to an arbitrary timestamp, Rails already provides that functionality for you: update_attribute :published_at, some_time.
I agree with everything else you've said around the semantics of publishing something, but those are implementation details of the application at hand. Having a #publish_at!(publish_time) method may or may not be what users need in this application, but the original example (adding a parameter to make testing cleaner) seems like a clear case of writing to the test to me.
My point is that, in this case, it is not possible to understand what it means to "publish" something without understanding the implications of its publish_at time. That is, it's not an "implementation detail" but an essential part of the semantics of publication.
So, yes, if the original author left the concept of publication time out of the publish! method, that was probably a mistake. Likewise, adding a current_time parameter to that interface for the purpose of injection at testing time was also a mistake.
But my point is that both of these mistakes are downstream consequences of failing to recognize that, in this application, the published_at time is an essential part of the publication semantics and ought to be clearly expressed in the interface for publishing things. Had it been been expressed, there would have been no problem during testing because the interface would already work for any given publish_at time and not be hard-coded for the single (although common) case of publishing at now.
In sum, a better interface all around would have been to have the publish! method take a publish_at time that defaults to the current time. You can't really make the interface "simpler" by leaving it out of the method interface: it's essential to publishing something, so it must be there somewhere, if not spelled out clearly in the interface, then hard-coded to some value internally, where all it can do is cause trouble.
def publish!(current_time_TEST_USE_ONLY = Time::now)
self.update published_at: current_time_TEST_USE_ONLY
end
Another way to do it (what I do) is simply run the test, and compare obj.published_at to Time::now. If they differ by more than 100ms, something has gone terribly wrong (if you use Ruby, maybe replace 100ms with 1s).
Please don't do this. It'll work fine for you, but if your tests ever get integrated into a larger system, then tests may start breaking randomly. I might be running tests concurrently, for example. On a virtual machine whose host is a bit more heavily loaded than normal. Running a large test suite is one situation where I don't care about interactivity and can just go away and come back later. Except that with a test that makes assumptions about running time, I can't. And this problem has actually happened to me, so isn't just hypothetical.
I can't count the number of times I've had to re-kick a build because a timing-dependent test like that failed (busy build server, inopportunely timed gc, etc). One of my top programming annoyances.
If you have to do this, you could check that the published time was >= the time at which you started the test, and <= the time at which you are doing the check.
Adam's post does raise an interesting point about the "mass" of the function. Since `publish!` now has parameters, the public API is more complex. Whether or not this trade-off is worth it or not will probably have to be evaluated on a case-by-case basis, methinks.
The APIs are exactly the same. One is explicit, reading its signature is enough to understand what it does, the other one is implicit, requiring reading the method body. The implicit version readability mass increases fast as it makes further transitive calls, which are now all required reading. The explicit version mass is always constant: the signature.
> It's not enough to read method signatures. There are no explicit interfaces. Only implicit ones you devine by understanding the full breadth and scope of how parameter A is used in method B, all the way down the stack trace.
Unit tests are meant to explicitly define the code's interface, and Ruby makes it very easy to write tests. Unfortunately, in practice this doesn't always happen and what you're describing rings a painful bell.
I'm sure my opinion is in the minority here, but all the pages and pages of blog posts that we have collectively created to rail against various design patterns seem like we're just trying so hard to justify bad programming practices.
I don't know ruby so the example given wasn't exactly clear, but it seems like you're able to (temporarily I hope) override the method Time.now to return specifically what you want. While that certainly seems reasonable for testing purposes, I would hate to have that kind of stuff happen in production code. The fact that methods can be overridden at any point in such a manner is a detriment to predictability, on the level of the longjump statement itself. It's hard for me to believe that people actually advocate this stuff as a "good thing".
Yes its true that dependency injection is a substitute for missing language features, namely modifying private data or globally scoped objects at any point. This is a good thing! Dependency injection allows you to have the same power while still giving you the predictability that truly private scope brings. If the cost of this is a few extra lines of code and some indirection in the form of interfaces, I'd say it's well worth it.
"Yes its true that dependency injection is a substitute for missing language features, namely modifying private data or globally scoped objects at any point. This is a good thing!"
The very fact that there are globally scoped objects is an abysmal failure. That there are private data whose state can change "by magic" is also a major defect.
There are much more functional ways to deal with such issues and some people are starting to see the light.
And of course watching people argue over globally modifiying what a function does or modifying by injection what is basically a global var is a bit like watching a blind and a one-eyed argue over who can see better ; )
There is a camp in Ruby that thinks monkey-patching (extending or modifying behavior at run time) is a great feature and defining characteristic of the language that should be exploited.
There is also a camp that thinks it is powerful, but dangerous and makes it harder to reason through a program (since your objects can be changed out from under you).
If you put yourself into the mindset of the pro-monkey-patch camp, it is easy to see how DI comes across as an over-engineered solution to a non-problem.
One thing I am finding more and more important is to try to understand the perspective of the author of a given blog post. While it is more difficult to do (since no one puts a disclaimer stating their views at the top of the posts), it ultimately helps to improve my understanding of competing arguments and how they can align with the problems I face in my own projects.
It is okay to come to the conclusion that an argument is not right or wrong in the absolute sense, but right or wrong for me and my work at the given time.
Use monkey patching in tests, never in production.
Monkey patching is fragile, but in tests, who cares? If it breaks, you see why and you fix it. Tests don't have to be held to the same level of reliability as production code.
"Tests don't have to be held to the same level of reliability as production code"
I personally take issue with that statement. If your tests are not reliable and robust, you will stop trusting them. At the point, why even have tests if you cannot trust them to verify correctness of your program (or at least increase your confidence that the program is not broken)?
If your tests are not reliable and robust, you will stop trusting them.
This seems to be the paradox in much advocacy of strategies like TDD. We start with the premise that our code is likely to contain bugs. In order to detect those bugs, we write a lot of tests, perhaps doubling the size of our code base. Now, we can do whatever we like to the production half of our code base, as long as our tests in the other half all continue to pass when we run them, because magically that testing half of our code base is completely error-free.
Definitely. Tests are really like a dual to the code, in the sense that they are another way of describing what it is supposed to do. Like you say, as the test grows more complicated, the risk that the test code itself has bugs grows too.
The risk is small with simple declarative tests ("the output of f(x) == y") but with complicated systems with large state spaces, you start writing combinatorial tests to try to cover more of the space and incur a bigger risk your test code is buggy.
Few things are as frustrating as fixing a bug that should have been caught by a test and realizing the test wasn't actually working correctly. This is even more acute when working on code where speed is important (where performance is correctness, in other words) and you realize a test has been measuring the wrong things, such as asynchronous runtime showing up in the wrong place.
TDD has always seemed more like a useful tactic to be applied to certain kinds of problems than as a silver bullet.
That's not TDD. You define the expected behaviour first (which still can be wrong, now or in the future - they're based on your current assumptions/spec/other mutable thing), then ensure the code produces output that matches your expectations. Your output is now as correct as your understanding of the solution.
Your output is still only as correct as your specification of your initial assumptions.
For example, lets say you make a mistake in your test so you are checking (result = expected_result), which is always true, instead of (result == expected_result). Now when you write your code, you run the test and it passes.
In this case your code may or may not be correct, and the test, which contains a bug, does not catch it. But the bug is not a fundamental misunderstanding of the problem, rather a simple mistake in writing the test. Following strict TDD doesn't prevent this.
You must see a test fail before you make it pass. It goes "Red -> Green -> Refactor" not just "Green -> Refactor". Even if a valid test case passes on the first time, one should modify the test (or the code) in a known way to cause an expected failure.
Your premise (tests are only as correct as the spec of initial assumptions) is correct, but your supporting example is not.
Tests you only partially trust are a lot better than no tests at all.
Also, the tests can be less reliable by generating false positives. As long as all positives are checked, and the tests are maintained to pass (by fixing the tests and production code as needed), then false positives are not going to hurt your production code's reliability.
As long as your less reliable tests are not generating false negatives, you are fine.
I interpreted the parent's usage of "reliability" to refer to engineering-lifecycle reliability. I completely agree that confidence in tests is critical, but I also agree that taking design shortcuts here and there in test code is fine, if it gets the job done and doesn't adversely impact the production API.
It is one of those fuzzy, learn from experience type of concepts.
In my experience, taking shortcuts in tests breeds more and more shortcuts (as less experienced developers look to existing tests as reference). As the shortcuts accumulate, the tests become more brittle, less effective and harder to maintain. This is the path to having a test suite that you no longer trust.
Any test that requires monkey patching or DI to work is going to have these reliability issues. I've seen plenty of DI'd mock objects that screwed the test.
Use monkey patching in tests, never in production.
My worry about that argument is: how do you audit your code base to make sure you really aren't relying on monkey patching in production code?
This is a concern with any abstraction technique that provides neater code by hiding some part of the behaviour, starting with simply using a programming language at a higher level than assembly.
However, in some cases, particularly more static/compiled languages, there are often tools to enforce encapsulation of various kinds. On that basis, you can systematically prove that the code you're looking at really does honour certain guarantees.
In the dynamic/interpreted languages, this can be harder, because with so much being done at run-time and so much flexibility, almost everything comes down to trust. That makes it more difficult to move from informal/convenient to formal/rigorous code as a project evolves.
Often, that may be a price worth paying. In return we can get early prototypes up and running more quickly than we could with a heavier "engineering" language. But we shouldn't underestimate the long-term implications of that trade-off.
"Use monkey patching in tests, never in production."
"Never" is a big word.
Years ago, I was tasked to change the text label of a file uploader. The site was built using a home-made framework on top of WebForms. It was even built to allow components to be replaced, but since everything touched everything else -- my simple task proved to be impossible. Even one of the original authors of the framework sat at my computer trying to show me, and after a couple hours even he gave up.
Compare that to a problem I was having with a Ruby on Rails site built on top of Spree from versions ago. I wanted to set up a staging server, and (for a reason that turned out to be my fault) I couldn't get the server to stop switching to SSL -- which the staging server didn't have. After spending a little bit of time to solve it, I just monkey-patched the SSL switch to false. Deployed, it worked, and I moved on.
We're really talking about edge cases here, but it's a symbol of a higher issue. Are we to be martyrs to past issues and bugs in code, devising more abstractions just to try to protect one code from another? Or is getting the problem solved in the most efficient way possible the better solution?
I'd use the phrase "it depends on the context" instead of "never."
Monkey patching in tests is particularly problematic when you're running a number of tests one after the other. The number of times I've found tests that appear to be testing something and going green, but are actually doing nothing because they run after some other test that has modified a global and not put it back again afterwards.
> Monkey patching is fragile, but in tests, who cares?
The coders who have to maintain the tests care. Tests are different to production code, but the trade-off where quick-but-fragile code imposes a tax in keeping it working is the same.
I think both camps will feel much happier when something like refinements are added to Ruby.
In Perl I can safely localise all my monkey-patching by using dynamic scoping:
sub today { DateTime->now }
{
no warnings 'redefine';
local *DateTime::now = sub {
DateTime->new( year => 2012, month => 12, day => 24 )
};
# now everything in this scope uses the monkey-patched DateTime::now
say "I'm the stubbed monkey-patched date!" if today->day == 24;
}
say today(); # => DateTime->now()
This article is just so biased and full of odd opinions.
"If your Java code has Date date = new Date(); buried in its guts, how do you set it to a known value you can then compare against in your tests? Well, you don't. So what you do instead is pass in the date as part of the parameters to your method. You inject the dependency on Date. Yay, testable code!"
Seriously? I've never seen things done in this way, ever. The correct way is to have a TimeSource interface and forego the direct use of Date. Simple. At this point it feels like the author's premise of the article has been invalidated.
I don't like using the terms DI or IoC. I prefer to talk about "composition" and "componentisation". Because those are the real goals. Testing is almost a secondary concern in this regard, it is just a nice side affect and bonus of writing well designed software. The primary reason for designing your software in the style of composition and componentisation is for the separation of concerns and the achievement of all the SOLID principles. But let me guess, Ruby hipsters think those are bad too huh?
As an interesting note, I once went to a Hacker News meetup in London and not a single one of the Ruby developers I spoke to even knew what DI or IoC were. Most didn't even know what static typing was either. Or even type inference. This is not really a good sign of a healthy community.
His recommended solution prevents parallelizing your unit tests. Dependency injection is not just testable globals. It's about declaratively defining what global-ish things your class depends on.
You also don't need to provide the date as a parameter to your methods. You can make the class depend on a clock. A clock is a natural thing for a service to depend on, and clearly indicates to outside users that the class needs to know about time.
If it isn't a service, and you're updating some sort of entity, then why should the entity figure out the current time itself? The model should just record that I updated such and such field at such and such time. If it's the current time, that's someone else's responsibility.
Eh, Ruby doesn't do well with parallelism anyway. If you're running your tests in parallel, you're usually running them in different processes. Or on different machines entirely. Either way, monkey patching one of those processes won't affect other ones.
Besides, if there is a testing library that runs multiple tests in parallel in the same processes (which maybe there is and I'm just not aware of it), making your mocking library smart enough to handle that, isn't difficult. Stubbing only for a particular thread, for instance.
Yeah, ruby doesn't do well with parallelism, and if you want parallel tests, you're probably doing it in separate processes, because the ruby community chooses to completely ignore everything that's known about how to write code that behaves well in parallel because "eh, I don't do it, so it must not be important".
Because Time.now is a global method whose behavior you've just (temporarily) modified. Any tests or code that do depend on Time.now returning the actual current time won't work correctly.
Dependency injection subjugates readability and simplicity of data flow for a narrowly defined notion of testability. Even in languages like Java, it's possible to substitute class definitions for mocked ones without having to thread a weird sort of test monad through code that otherwise serves a purpose.
I pretty fundamentally disagree with making production code more complex to make unit tests easier to write. Make the tests more complex instead and think about the natural units for testing. Maybe the natural unit for testing isn't exactly one class.
This is counter to the conclusion of the article: it's true one shouldn't force Java idioms on Ruby but also if the Java idiom doesn't translate well, maybe it's a bad programming idiom in general.
Fair, except that DI in Ruby is pretty dang simple.
def some_method(dependency = SomeClass.new)
dependency.do_something
end
I'd argue that injecting the dependency gives you even more expressiveness and improves readability since you now can name the dependency whatever you want (assuming you give your variables meaningful names and think it's a good thing).
I'm with you on everything you wrote there, except that I think DI has more applications than just unit testing. Inversion of control, whether via DI or otherwise, is potentially useful any time you need more than one variation of a part of your system and the ability to swap them around on the fly.
The thing is, unit tests are another "use case" for a given piece of code. Many people here are saying, "I wouldn't do monkey patching in production, but it's not really a problem for stubbing in test code." And what happens when you want to make different use of that code in production? Sure, "YAGNI, rewrite as necessary" and so on, but ruthlessly applying YAGNI leads to code so inflexible that you need to rewrite a whole component to make a change in one class (or start playing with monkey patching in production code, but I have not seen anyone advocate doing that liberally).
Adding function parameters solely for testing purposes is bad. However, poorly-designed functions are often difficult to unit test.
I don't know the entire backstory, but it appears someone wrote a publish! method that took a publish_time argument that was only intended to be used in testing. The problem is that the original code didn't properly support some publish_time values.
This is a good example of the library vs. framework distinction. Frameworks favor opinion over composition.
"If your Java code has Date date = new Date(); buried in its guts, how do you set it to a known value you can then compare against in your tests? Well, you don't."
You don't if you aren't testing your code properly. If you are, one option is PowerMock.
When looking at code, I like to separate design-time concerns from runtime ones. Tests are a design-time affair, and if you are substantially altering the run-time composition of your application to accommodate such things, the approach is likely wrong.
I can't be bothered talking about DI much, because it's a non-issue. Use it if it's appropriate. You use ruby? Big deal. You're not special; if there's a situation where DI is helpful, use it. If not don't. This isn't a complicated idea.
I was mildly interested in the idea that 'language shapes the way we think'.
After thinking about it for a bit, there's some basis for this idea, perhaps.
You see, the central idea of whorfianism is basically:
- Your language affects the type of mental constructs you use.
- People with different mental constructs behave differently.
So for example (classic example), if you have a language with no concept of sub-day time units, you'll end up with a society that isn't fussed about punctuality. "Turn up in the afternoon"; ok, I'll do that. No need to ask "what time?" because your virtual model doesn't break your calendar up into hour sized blocks; just into day sized ones.
It's a believable theory. There's a great book about this topic (http://en.wikipedia.org/wiki/A_Man_Without_Words) which broadly speaking supports the idea that language is basically a set of representative symbols we use to model concepts.
The issues up for debate is really, to what extent does language influence behaviour, compared to, for example, other factors. That's a much harder question to answer (and as yet unresolved I believe).
Now
The idea that a programming language can do that?
Well.... it's not totally rubbish. I mean, software doesn't exist in the real world; to work on it at all, we have to create a mental model of it.
So it's conceivable that our language provides us with symbols to conceptualise the code we work on, and so we'll behave in a different way if we have different models.
For example, if you have a deep understanding of assembly and lower level programming languages, your model will have more stuff in it, compared to the model of someone who only knows a high level language, whose model will drop down to 'black box' components when it gets down to a certain level.
...and I can totally believe that makes a difference to how you write code.
This applies, however, only to concepts (aka. words, aka. representative mental symbols) and not to programming languages.
See, this is the issue; your domain knowledge helps inform how you generalize and problem solve. That's what ruby is. It's a knowledge domain.
I'm extremely dubious that it provides novel concepts that you don't get from any other programming language. Perhaps in that its a dynamic language it gives a few different ones, to say, that a C++ programmer might have, but broadly speaking:
You are not a unique and special snowflake because you use ruby
...or python. or C++. Or scala. Or C. Using a different programming language DOES NOT change the way you think.
Learning new words and concepts in your existing language (ie. the one you speak) does that. And sure, using a language with new concepts will teach you those new concepts. Like DI for example, that's a concept.
At the risk of being offensive, I've always felt that this "design patterns" cargo cult is Revenge of the Not-Nerds. The people who couldn't hack the harder CS classes because of all the math are striking back with something designed to be as incomprehensible to us as mathematics was to them.
Take the Visitor pattern. I mean, really? I already know how to work with trees. Lisp is all about trees. Ocaml lets us build tagged-union types and pattern match to our hearts' content. Do we really need to dickshit the program with Visitor? WTF does that even mean? Who is visiting and why? Is this the French meaning, where to "visit" someone is patronize a prostitute? (In French, you "pay a visit to" someone, or rendre visite à quelqu'un. You don't "visit" your sister.)
The design patterns cargo cult is horrible. It has such a "how the other half programs" smell about it that I cannot shake the belief that it was designed to make us Smart People pay for something. Anyway, how can it be "best practices" if I can't REPL the code and make function calls and see how the fucking thing works? If you can't interact with the damn thing, you can't really start to understand it, because it's almost impossible to understand code until you know what you're looking at. IDEs just give people a false sense of security about that.
Personally, I like functional programming because it has two design patterns: Noun and Verb. Nouns are immutable data. Verbs are referentially transparent functions. Want side effects? You can have them. Those are a separate class of Verbs: Scheme denotes them with a bang (e.g. set!) and Haskell has them in the IO monad. Real-world functional programming isn't about intolerantly eschewing mutable state, but about managing it.
Now, I'll admit that mature codebases will often benefit from some solution to the Adjective problem, which is what object-orientation (locally interpreted methods instead of globally-defined functions) tries to solve. OO, at its roots, isn't a bad thing. Nor is it incompatible with FP. The Alan Kay vision was: when complexity is necessary, encapsulate it behind a simpler interface. That's good stuff. He was not saying, "Go out and build gigantic, ill-defined God objects written by 39 commodity programmers, and use OO jargon to convince yourselves that you're not making a hash of the whole thing." No, he was not.
The real use case for the visitor pattern is to simulate multiple dispatch in a language that only has single dispatch. In a language like Java, if you want to traverse a tree where each node can be a different type, you don't really want to use a series of if-statements for every single type, so the visitor pattern is used in this case. The visitor pattern allows you to use method overloading instead for each different type instead.
That's one useful case, but the primary use case is keeping your code clean. If it isn't keeping your code cleaner, you're probably doing it wrong.
Serialization, for example, is a spot where the visitor pattern is useful. Take the example of a fairly complex hierarchical document - maybe something like a Word document. You need to support being able to save it to a whole bunch of different formats, and you want to do it as simply as possible.
- Hard-coding the save functionality for every possible format into the document classes themselves would be a mess, and would quickly pollute the data structures with a whole bunch of clutter. That violates the single-responsibility principle.
- Writing completely independent serializers for each format would be somewhat cleaner, but the serializers would end up containing a mess of repetitive code. That violates the DRY principle.
The Visitor pattern is nothing more complicated than a way to keep the code cleaner and more maintainable by factoring all the common elements of the task (saving, in this case), so that your code doesn't end up littered with a bunch of unmaintainable copypasting.
This shouldn't be anywhere near as difficult as what the grandparent suggests. In an object-oriented language, just create an abstract "DocumentSaver" class that has concrete implementations of all the common stuff, and abstract methods for all of the stuff that needs to be different for each file format. Then create subclasses for each of the save formats and voila, you're done.
I think the grandparent is absolutely correct that this is a pattern that is probably unnecessary in a language with algebraic data types and pattern matching. But to say that this means a pattern like Visitor is therefore a hallmark of being a bad programmer is unfair, if not downright risible. Most of us work in mainstream languages - and mainstream languages just don't have those features. For us, Visitor is an easy idiom for reclaiming some of the benefits of that functionality. And frankly, it should barely take more code to whip up a Visitor than it takes to pattern match your way through the problem.
Why do you need all this complexity in order to do that?
I feel like one of Java's problems is that these design patterns have taken it away from the language's intended design. It wasn't intended to be a dynamic language. Most of Java's ugliness is an extremely incompetent Greenspun's Tenth Rule: solving of problems where the solution is "use a different language, like Clojure or Scala".
There are occasions where Java and C++ are the right languages to use, but most of the time when people are using these over-complex frameworky solutions, it'd be much more elegant to use a different language, and the code would be more legible.
Mentally rewriting the beginning of your last paragraph to "Sometimes Java or C++ is the right language to use, but most..." made it make considerably more sense to me.
As it stands, I think the double-aren't is colloquial or a mistake; I honestly couldn't follow the meaning of the overall triple-negative. :) Not trying to be a jerk - I love reading your stuff, but I spent more than a minute thinking about that sentence before just guessing it from the rest of what you said.
Unless these hypothetical developers of yours are time travellers, your proposed solution wasn't an option for more than half of Java's life. Sometimes Java is 90% of what you need and you just get on with the other 10% because the alternative (learning a new language, finding a new team or training an existing team to use it correctly, etc.) is simply not worth it to anybody with real world time/quality/scope constraints.
I don't care if in 2002 I couldn't use Scala. That was 11 years ago and it's not exactly an excuse for not using Scala today.
There are companies working in finance today who are switching entire Java teams to Clojure. It's precisely because they have real-wold time/quality/scope constraints that they are doing so.
Most Java codebase became what Rich Hickey calls "The elephant in the room". By switching to Clojure they can reduce the size of their codebase by a factor of ten, making the elephant easier to manage.
So we're back to square one: the real issue is that there are still people regularly posting links to "design patterns" and "dependency injection" that get upvoted because a large part of HN only knows about Java/C# + ORM + XML + SQL hell...
As long as people will be doing that, you'll be able to use your argument: "but, hey, in 1995 it was all the rage!".
IMHO, Java combines the worst of statically typed and dynamically typed programming languages. To paraphrase Rob Pike, programming in Java is like dotting all the i's and crossing all the t's in a fairly complex contract but at runtime, the contract can be overridden by dynamic classloading. So you do not get the efficiency of a statically typed language (because the compiler cannot make certain assumptions about the types / classes) nor do you get the expressiveness of a dynamically typed language.
In the case of DI, I would observe that no programs in other languages seem to require or even use this pattern. If you need to implement LGPL at the language level, a better abstraction is modules with contracts... or even better a proper metaobject protocol a la Lisp.
DI is also used quite heavily in C#. And in most - if not all - functional languages, one of the flavors of dependency injection (what a C# programmer would call method injection) is so pervasive that it doesn't even have a name. People just think of it as an idiom rather than a design pattern.
If I may speculate, I suspect that the reason why DI is discussed so much more in the big enterprise programming languages isn't because of the languages so much as the problem space. Enterprise software has a tendency to be enormous, live for a very long time, and involve stitching together components from a variety of sources, none of which are being modified or replaced on exactly the same schedule. Hard-coded dependencies always have the potential to be a maintenance hassle, but that kind of situation compounds the problem immensely. The promise of DI - that if you follow it scrupulously you can produce software that's so easy to modify that you might even be able yank out and replace entire modules without so much as a recompile - becomes very attractive in that kind of situation.
It's also a huge win for huge teams. If you make good use of dependency injection then it's easy for different teams to develop different modules that depend on each other in parallel. All you have to do is whip up some stub implementations of the modules that aren't available and get to work.
What you have just said is one of the most insanely idiotic things I have ever heard. At no point in your rambling, incoherent response were you even close to anything that could be considered a rational thought. Everyone in this room is now dumber for having listened to it. I award you no points, and may God have mercy on your soul.
>Take the Visitor pattern. [rant about the visitor pattern]
I don't think the visitor pattern means what you think it means.[1] Also, calm down. It's a beautiful Sunday afternoon. Perhaps you should pay a visit your sister.
It's an anti-pattern. It's a workaround for a serious language defect.
I've written my own DI back in the days (way before Guice existed) but...
Using DI is just a glorified way to have "globals".
One better solution in TFA's example is to pass a high-order function whose "functionality" is to provide the time.
Wanna unit test? Pass in a modified function giving back the time you want.
That's just one way to do it.
DI is a fugly beast disguised as "good practice" but it really ain't. It complicates the code and is basically a hack allowing to more or less be able to test non-functional code. Really sad.
But passing in a function providing the time is a form of "dependency injection". It is a shitty name for the practice, but that's at least one way I saw the term "dependency injection" used.
>One better solution in TFA's example is to pass a high-order function whose "functionality" is to provide the time.
Having a function or an interface as an argument is functionally equivalent. In fact, the interface is more expressive than the function as it describes more behavior than a simple function does. In both cases there is an "inversion of control" where the caller has to specify what object is passed in. So claiming that one is better than the other seems more personal preference than fact.
This is not okay! I happen to dislike DI even more than the OP, but to attack people on a personal level for liking it is just plain mean, and unnecessarily so.
When you assert the identification of self with technology preference, you reenforce a damaging idea that has no merit, which is indeed the very idea that ensures that such discussions often have more heat than light.
Shame on you, David, for using your position to promulgate an idea that is not only useless, but actively damaging to the community of programmers.