logoalt Hacker News

mrkeentoday at 11:16 AM0 repliesview on HN

> For example, in Swift, if you declare a function to throw an exception, then by God every call to that function, all the way up the stack, must be adorned with a do-try block, or a try!, or a try?.

Funnily enough, Uncle Bob himself evangelised and popularised the solution to this. Dependency Inversion. (Not to be confused with dependency injection or IOC containers or Spring or Guice!) Your call chains must flow from concrete to abstract. Concrete is: machinery, IO, DBs, other organisation's code. Abstract is what your product owners can talk about: users, taxes, business logic.

When you get DI wrong, you end up with long, stupid call-chains where each developer tries to be helpful and 'abstract' the underlying machinery:

  UserController -> UserService -> UserRepository -> PostgresConnectionPoolFactory -> PostgresConnectionPool -> PostgresConnection
(Don't forget to double each of those up with file-names prefixed with I - for 'testing'* /s )

Now when you simply want to call userService.isUserSubscriptionActive(user), of course anything below it can throw upward. Your business logic to check a user subscription now contains rules on what to do if a pooled connection is feeling a little flakey today. It's at this point that Uncle Bob 2017 says "I'm the developer, just let me ignore this error case".

What would Uncle Bob 2014 have said?

Pull the concrete/IO/dependency stuff up and out, and make it call the business logic:

  UserController:
      
      user? <- (UserRepository -> PostgresConnectionPoolFactory -> PostgresConnectionPool -> PostgresConnection)
      // Can't find a user for whatever reason? return 404, or whatever your coding style dictates

      result <- UserService.isUserSubscriptionActive(user)

      return result
The first call should be highly-decorated with !? or whatever variant of checked-exception you're using. You should absolutely anticipate that a DB call or REST call can fail. It shouldn't be particularly much extra code, especially if you've generalised the code to 'get thing from the database', rather than writing it out anew for each new concern.

The second call should not permit failure. You are running pure business logic on a business entity. Trivially covered by unit tests. If isUserSubscriptionActive does 'go wrong', fix the damn code, rather than decorating your coding mistake as a checked Exception. And if it really can't be fixed, you're in 'let it crash' territory anyway.

* I took a jab at testing, and now at least one of you's thinking: "Well how do I test UserService.isUserSubscriptionActive if I don't make an IUserRepository so I can mock it?" Look at the code above: UserService is passed a User directly - no dependency on UserRepository means no need for an IUserRepository.