Dependency injection into Dry::Struct descendant class

I would like to figure how to inject a dependency into a Dry::Struct descendant class.

Here’s the code:

require 'dry-struct'

module Types
  include Dry::Types.module
end

class User < Dry::Struct
  attribute :name, Types::Strict::String
  attribute :age,  Types::Strict::Int.constrained(gteq: 18)
end

Now I want to inject a logger in the simplest way possible. I would not like to use dry-container partly because I didn’t grasp it yet, and partly because I still adhere to ‘you don’t need a dependency injection framework for dependency injection’ opinion. I’d like to keep it simple.

So, without dry-struct, I’d go with:

class User
  def initialize logger
    @logger = logger
  end
end

User.new(Logger.new(STDOUT))

But sure enough, this approach doesn’t work for Dry::Struct successors.

I came up with adding an extra attribute:

require 'dry-struct'

module Types
  include Dry::Types.module
end

class User < Dry::Struct
  attribute :name, Types::Strict::String
  attribute :age,  Types::Strict::Int.constrained(gteq: 18)
  attribute :logger, Types::Any

  def log_info(message)
    logger.info message
  end
end
irb(main):001:0> user = User.new(name: 'Peter', age: 20, logger: Logger.new(STDOUT))
=> #<User name="Peter" age=20 logger=#<Logger:0x007f987813f6b0 @level=0, @progname=nil, @default_formatter=#<Logger::Formatter:0x007f987813f660 @datetime_format=nil>, @formatter=nil, @logdev=#<Logger::LogDevice:0x007f987813f610 @shift_period_suffix=nil, @shift_size=nil, @shift_age=nil, @filename=nil, @dev=#<IO:<STDOUT>>, @mon_owner=nil, @mon_count=0, @mon_mutex=#<Thread::Mutex:0x007f987813f598>>>>
irb(main):002:0> user.log_info 'hello'
I, [2017-08-23T14:06:23.437863 #6990]  INFO -- : hello
=> true

Please try not to. Structs are meant to be used as pure data structures, they should not depend on any external systems.

My struct got a bit beefy, because I added a whole bunch of methods to it. So, I took the wrong way? In the case above, where do I put user's methods, the things user can do?

@solnic As far as I understand, I shouldn’t name the dry-struct class a User, but rather UserData. And I should have have a separate User class with all the methods.

And then… do I inject UserData instance into User constructor? If so, what’s the benefit of separation of the two classes?

If you call it UserData then what User is supposed to do? Usually, operations that should be logged are located in places of a different kind, e.g. service objects, functional objects and alike. You might want to rearrange your code according to these concepts.

1 Like