4 ms·
Yet another time it comes up. But now, after functional programming being commonplace, we at least know the answer to the "why" question. Consider the "classic
by atemerev 11y ago
Yet another time it comes up. But now, after functional programming being commonplace, we at least know the answer to the "why" question.
Consider the "classical OOP" method signature:
email.send()
vs "the anemic way"
emailSender.send(message: Email)
If there is only one way to send email (SMTP), things works fine. But let's assume we have to implement another way (e.g. Mailgun API). In the "anemic" case everything is simple: extract interface from EmailSender, add new implementation, all other method signatures remain unchanged. Refactorings are simple and can be done easily in any modern IDE. However, in "classical OOP" way, we have to do something like:
email.send(sender: EmailSender)
Which changes Email::send signature, which leads to breaking API changes and extensive manual refactoring of the entire codebase.
The "anemic way" had won because it is much more reusable and maintains API stability. Incidentally, this approach have smoothed the transition to functional languages — "NounVerber" objects became modules (collections of functions), and value objects became immutable data types.
- orf 11y ago> Which changes Email::send signature, which leads to breaking API changes and extensive manual refactoring of the entire codebase. Only if you decide to change the signature, there are other ways to achieve the same result.
- atemerev 11y agoFor example?
- to3m 11y ago"GetMailPolicySingleton()->SetSendingMechanism(Email::SMTP)" ;)
- rescripting 11y agoSometimes you're sending via SMTP, sometimes via MailGun. If you don't remember to call GetMailPolicySingleton()->SetSendingMechanism() before every email.send() you end up using whatever policy was set last.
- taspeotis 11y ago> Singleton What if two threads need to send mail via SMTP and Mailgun...
- anton_gogolev 11y agoSingletons have nothing to do with reentrancy or lack thereof.
- anon1385 11y agoA 'sending mechanism' property on the email object that defaults to SMTP?
- smanzer 11y agoThis. There may be other reasons to dislike the email.send() formulation, but it seems like the need to support other mail APIs could be handled relatively easily within this type of OO approach through the use of default values.
- throwawaykf05 11y agoI suppose you could add more methods, e.g sendEmailViaMailGun. But that results in the invoker having to have if-else logic (itself a code smell) for selecting which method to call, which is bad for SRP. You could try to fix that by encapsulating that logic into an EmailRouterStrategy class, but that's just another dependency you have to inject, which is not ideal according to TFA.
- mistermann 11y agoCouldn't you just add a new SendMethod optional parameter defaulted to the method that was used prior to the refactoring? No breaking API changes, no extensive manual refactoring of the entire codebase, as far as I can tell. This is how I always did refactoring for years, it's immediately obvious to support developers wtf is going on, and there's no requirement to have any "complicated" architecture on every single object in your entire application. Instead, the code largely resembles the simplicity of the real world being modeled. Is this approach is so dangerous (as we're constantly told) why has it never caused me any problems? Maybe I'm missing something but I've never read an article that explains what it is I'm missing in a way that clicks.
- anton_gogolev 11y ago> Couldn't you just add a new SendMethod optional parameter Adding an optional parameter to an already existing method is not always a backwards-compatible change. For example, in C# this will effectively change method signature and break API.
- mistermann 11y agoHoly cow it seems it seems you're right: http://haacked.com/archive/2010/08/10/versioning-issues-with-optional-arguments.aspx/ http://haacked.com/archive/2010/08/10/versioning-issues-with... http://haacked.com/archive/2010/08/12/more-optional-versioning-fun.aspx/ http://haacked.com/archive/2010/08/12/more-optional-versioni... ...yet I've used this technique for years in VB.Net without issue....weird.
- zeveb 11y agoExactly! This is why Lisp has dynamic variables: (let ((*emailer* mailgun-client)) (send email)) Or, using Go contexts (which are just a way of hacking in dynamic variables): ctx = email.NewContext(ctx, mailgunClient) email.Send(ctx) And in email.Send: emailer, ok := EmailerFromContext(ctx) if !ok { emailer = defaultSMTPEmailer }
- throwawaykf05 11y agoTFA actually suggests having a "RenderAsEmail" method on the "Report" object! That right there should have been a hint that this line of reasoning is flawed. It would result in things like mixing presentation logic along with business logic in the same class. The author realizes this but does not seem to see it as a red flag.
- hcarvalhoalves 11y agoSpot on. You replace your "anemic way" with first-class functions and namespacing, it ends up being the same thing. With currying, you do away with OO and parameter initialization altogether. @curry def SMTPSender(host, message): ... emailSender = SMTPSender(host='localhost') # partially evaluated fn with `host` fixed emailSender(message=message)
- buffoon 11y agoThis is pretty much how we deal with this in a DI framework anyway. The constructor of the transport takes the host and the send method takes the message. emailSender = SMTPSender(host='localhost') # in container emailSender.send(message) # in implementation The consumer of the SMTPSender only interacts with the send() method.
- buffoon 11y agoThe anemic way is modeled correctly anyway. An email doesn't send itself nor does it interact with the transport. The transport (MUA/MTA for example) is responsible for delivering it via whatever method it happens to support. The container delivers the transport to the code. The consuming code doesn't care what the transport and method does, merely introduce the two and let them make sweet email love. The article stinks of circa 2002 purist babble.
- vishbar 11y agoNoun-verbers are also really helpful for testability, not just refactoring. Using a decent mocking framework (or just rolling some custom mocks) I can test almost all my business logic through unit tests, reducing the time that tests need to execute and increasing their reliability
- smrtinsert 11y agoExcellent summary. What an oop language like Java really needs these days are immutable value classes.
- setheron 11y agodid you read the blog post even ? He was calling for mutability in the classes and for the state to be closer to the data object itself.
- agumonkey 11y agoI remember being torn reading articles about anemic [considered harmful] around 2004 because being paranoid, immutable, functional, data first fits my mind a lot more. It's hard to swim the trends.
- ngrilly 11y ago> The "anemic way" had won because it is much more reusable and maintains API stability. Incidentally, this approach have smoothed the transition to functional languages — "NounVerber" objects became modules (collections of functions), and value objects became immutable data types. I agree. By the way, you can do that even in imperative languages. I do it all the time in Python or Go.