Dependencies
Some past self version of me is saying, every class and function should be explicit about their dependencies, so that
they are easily testable. John0
would say, “If you have a service that talks to a database, the database client should
be an explicit dependency specified in the constructor. This makes the code easily testable.”
There is another version of myself from 10 minutes ago arguing it’s foolish to be explicit about everything. He’d point to this piece of code he’s just looked at:
// senderV2Cmd sends using PastSelfService
var senderV2Cmd = &cobra.Command{
Use: "senderv2",
Short: "Sends pending messages in the database using PastSelfService",
Long: `Sends pending messages in the database using PastSelfService`,
Run: func(cmd *cobra.Command, args []string) {
messageRepository, err := pastself.NewMessagePGDBWithConfig(psCfg)
if err != nil {
cerrors.PrintAndExit(err)
}
messageSender := pastself.NewEmailMessageSender(psCfg)
userRepository, err := pastself.NewUserRepositoryWithConfig(psCfg)
if err != nil {
cerrors.PrintAndExit(err)
}
log4g := pastself.NewLog4GWithOptions(pastself.Log4GLevelOption(pastself.Log4GDebug))
psSvc := pastself.NewPastSelfService(messageRepository, messageSender, userRepository, log4g)
psSvc.SendOverdueMessages()
},
}
Here, the PastSelfService (psSvc
) accepts four explicit dependencies. But there’s a hidden one. It doesn’t say that
the EmaiMessageSender is using a SendGrid-based implementation. Somewhere in NewEmailMessageSender()
, there’s a
dependency that cannot be injected.
func NewEmailMesageSender(psCfg PastSelfConfig) MessageSender {
sender := &EmailMessageSender{}
sender.emailClient = email.NewEmailClientFromConfig(psCfg.EmailClientConfig)
...
return sender
John1
asked, “Sure, one can pass NewEmailClientFromConfig a configurable email-sender. But how far down do you go? Do
you make parameters of the SendGrid client explicit as well? Do you make DNS configurable? Do you make the the entire
networking stack configurable?”
Its a valid line of questions. When is it ok to “hide complexity” and present a simple facade? At some point, it must be ok to make decisions users.
There was another version of myself, from many moons ago, who thought a lot about this problem without finding a
solution. John2
noticed a lot of people had non-injectable dependencies in their code:
- Logging (e.g., Logger.getLogger(…))
- System clock (e.g., currentTimeMillis())
- Random number generator (random.nextInt())
- OS information (System.getenv())
- The DNS resolver
As a responsible code reviewer, John2
was troubled by these hidden dependencies. It ran against his ideals of “clean
code”. But he couldn’t reject every code review. The code works. People were shipping features. The fact that you cannot
inject a fake Log4j instance during testing really doesn’t seem that important.
John2
at some point, resigned, “Not everything has to explicit.”
So when is it ok have direct dependencies in your code? Like so many questions I’ve asked in life, there is no clear answer.