Let's say there are two classes, one is the user class, which contains the user information; the other one is the payment transaction class. The scenario is simple, if the user's age is > 65, create type A payment transaction; otherwise, create type B payment transaction.
There are several ways to do this:
- Create a method not belongs to user nor transaction, just call CreateTransaction. The logic is stated in this method:
func CreateTransaction(user, transaction) {
if user.GetAge() > 65:
transaction.CreateA()
else:
transaction.CreateB()
}
- The other option is the create a method for the user class:
class User {
...
func CreateTransaction(transaction) {
if user.GetAge() > 65:
transaction.CreateA()
else:
transaction.CreateB()
}
}
Then there is a CreateTransactionController method that calls the function like:
func CreateTransactinController(user, transaction) {
user.CreateTransaction()
}
My question is, is the option 1 considered as procedural programming since the logic is actually not owned by any object? (Or anemic pattern?) Is the difference between 1 and 2 be just different place to put the logic?
Thanks!
Since you marked this question as DDD, I will answer how a model, driven by the Domain, would implement this.
The question to answer is whether a
Transaction
is enclosed within theUser
object. If it is enclosed, it means that you always go through the user's record to fetch transactions (and never access transactions directly). If a transaction by itself has a lifecycle, can be accessed directly, controls other parts of the domain, and so on, it cannot be enclosed within aUser
and is a full-blown aggregate.Enclosing the
transaction
within theuser
would mean that the user owns transaction-related operations, so option 2 would be the right way.If
transaction
is a different aggregate, you could use aDomain Service
(like your option 1), but it is incorrect because you handle two aggregates (user
andtransaction
) simultaneously. You are better off enclosing this functionality within theTransaction
aggregate.The next question to address is how you would decide the type of transaction. One way is:
This is commonly how you handle changes that depend on attributes from multiple aggregates. You go ahead and change the state of the aggregate in the system, but check at a later point in time for the related aggregate data, and reverse the change if things are not congruent.
A better way is to create a
Specification
whose explicit task is to derive the right payment type based on the user's age. The specification encloses your business logic (> 65), gives context to the age-driven requirement, and acts as a central place where you would control the logic.You can read more about specifications here: https://martinfowler.com/apsupp/spec.pdf