I have to say I prefer option 3 to option 1 even considering YAGNI. It's better OO, it's more testable, there's more potential for reuse in other parts of your application etc.
Interestingly, when I started as a developer I might have written something closer to the first option. After a few years I would have gone for something closer to the third option. Nowadays, I'd probably go for the second option as I've hopefully learnt to strike a balance in the abstractions I create.
Not quite, the point is that the caller doesn't care how much the loyalty bonus is, only that the bonus it is awarding is for loyalty.
The value of the bonus then gets captured in the domain object. If there is enough related logic to justify having a loyalty bonus object, then you might have
account.award(LoyaltyBonus.new)
Or, if we're going to start slicing things up like this, we might go for:
With even a dash of business logic on the transaction object I think 3 is better code.
It's more testable because we can test the concept of a transaction rather than the net result of incrementing the account balance - e.g. test for a business rule to ensure that we can never have a transaction with a negative credit.
It promotes single responsibility because logic pertaining to the transaction can be pushed back onto the transaction object where it is defined once and where it more naturally sits - e.g. validate the transaction or render yourself for printing.
It promotes reuse because the transaction object could possibly be used in another context far removed from this particular scenario - e.g. any logic for validating or rendering the object would sit within the account object otherwise. It makes sense to bring it out where we could potentially reuse it later.
This is just object orientation 101, and it preaches the flexibility that the OP is against. Good OO code is code that is flexible and contains a certain degree of abstractions by design.
But you've lost the abstraction of crediting somebody! Now you have to know about transactions to give out that credit, and while it might be more flexible, you've lost your encapsulation and abstraction.
Option three would be suitable for the implementation of option two, I'll give you that, but if I'm designing an admin control panel I want to bind a control to credit an account, not to create a transaction to credit something and have an account process that transaction.
The point of using a standardized transaction object is that you can than track/manage all such transactions using roughly the same logic. Consider: crediting an account is merely a negative transaction, so there is no point in creating extra overhead or code to manage crediting transactions separately.
This would allow you, for example, to track and handle all transactions, including crediting transactions, using the same code base, with special transaction-type-specific issues dealt with by the transaction-type object.
Reminds me of an old joke: Programmers are good and pacifist people. A good programmer will never write a function like "bombHiroshima". Instead he will write a function "bomb" that takes Hiroshima as a parameter.
Should you write the function as
I have to say I prefer option 3 to option 1 even considering YAGNI. It's better OO, it's more testable, there's more potential for reuse in other parts of your application etc.Interestingly, when I started as a developer I might have written something closer to the first option. After a few years I would have gone for something closer to the third option. Nowadays, I'd probably go for the second option as I've hopefully learnt to strike a balance in the abstractions I create.