1

For testing and administration purposes I am looking to build a class to communicate with an API. I've got the connection and authentication down but am struggling with the base structure and size of the class.

My main goal is to keep each application domain split, but still easy to access by one class/connection. I've made an simpler example of what I'm looking for. In reality each domain has its own set of business rules to follow, which is why I want to keep them separate, whilst the API connection stays the same.

For instance, on CLI level I want to invoke:

$ client_one = Api.new("one")
$ client_two = Api.new("two")

$ client_one.Bikes.delete(1)
> deleted bike 1 from one

$ client_two.Phones.new(phone)
> posted phone iPhone to two

My thought proces was to nest modules inside an Api class but I can't get it to work or find the right syntax.

class Api
  def initialize(client)
    @client = client
    @connection = Authentication.get_connection(@client)
  end

  #preferable put each submodule in a separate file
  module Authentication
    def get_connection(client)
      #code to get Faraday connection
    end
  end

  module Bikes
    def new(object)
      #code to post new bike
      @connection.post(object)
      puts "posted bike #{object.name} to #{@client}"
    end
    def delete(id)
      #code to delete old bike
      @connection.delete(id)
      puts "deleted bike #{id} from #{@client}"
    end
  end

  module  Phones
    def new(object)
      #code  to post new phone
      @connection.post(object)
      puts "posted phone #{object.name} to #{@client}"
    end
  end
end

This results in errors like:

NoMethodError: undefined method `Bikes' for #<Api:0x0000000003a543a0>

Is it possible to achieve my goal or are there better 'Ruby' ways to accomplish it?

Furthermore, is it possible to split the submodules to different files? eg:

api.rb
modules
  + -- authentication.rb
  + -- bikes.rb
  + -- phones.rb
Monthy
  • 160
  • 1
  • 7

1 Answers1

1

There are some fundamental misconceptions of how Ruby OOP works in your example, and without a full code sample and the opportunity to interrogate you about what you're trying to accomplish it's hard to guide you to what might be the most appropriate answer. Any answer I give will be based partly on experience and partly on opinion, so you may see other answers as well.

At a high level, you should have classes in modules and not modules in classes. Although you can put modules in classes you better have a good understanding of why you're doing that before doing it.

Next, the modules and methods you've defined in them do not automatically become accessible to instances of the parent class, so client.Bikes will never work because Ruby expects to find an instance method named Bikes inside the Api class; it won't look for a module with that name.

The only way to access the modules and module methods that you have defined is to use them at the class/module level. So if you have this:

class Foo
  module Bar
    def baz
      puts 'foobarbaz'
    end
  end
end

You can do this at the class/module level:

Foo::Bar.baz
foobarbaz
=> nil

But you can't do anything at the instance level:

Foo.new::Bar.baz
TypeError: #<Foo:0x00007fa037d39260> is not a class/module

Foo.new.Bar.baz
NoMethodError: undefined method `Bar' for #<Foo:0x00007fa037162e28>

So if you understand so far why the structure of your example doesn't work, then you can work on building something a little more sensible. Let's start with naming and the class/module structure.

First, Api is a poor name here because you'll typically use Api for something that provides an API, not connects to one, so I would recommend making the name a bit more descriptive and using a module to indicate that you are encapsulating one or more related classes:

module MonthyApiClient
end

Next, I'd recommend adding a Client class to encapsulate everything related to instantiating a client used to connect to the API:

module MonthyApiClient
  class Client
    def initialize
      @client = nil # insert your logic here
      @connection = nil # insert your logic here
    end
  end
end

The relationship between client and connection in your code example isn't clear, so for simplicity I am going to pretend that they can be combined into a single class (Client) and that we are dropping the module Authentication entirely.

Next, we need a reasonable way to integrate module Bikes and module Phones into this code. It doesn't make sense to convert these to classes because there's no need to instantiate them. These are purely helper functions that do something for an instance of Client, so they should be instance methods within that class:

module MonthyApiClient
  class Client
    def initialize
      # insert your logic here
      @client = nil
      @connection = nil
    end

    def create_bike
      # insert your logic here
      # e.g., @connection.post(something)
    end

    def delete_bike
      # insert your logic here
      # e.g., @connection.delete(something)
    end

    def create_phone
      # insert your logic here
      # e.g., @connection.post(something)
    end
  end
end

Note that we've swapped new for create; you don't want to name a method new in Ruby, and in the context we're using this new would mean instantiate but do not save a new object whereas create would mean instantiate and save a new object.

And now that we're here, and now that we've eliminated all the nested modules by moving their logic elsewhere, we can see that the parent module we set up originally is unnecessarily redundant, and can eliminate it:

class MonthyApiClient
  def initialize
    # insert your logic here
    @client = nil
    @connection = nil
  end

  def create_bike
    # insert your logic here
    # e.g., @connection.post(something)
  end

  def delete_bike
    # insert your logic here
    # e.g., @connection.delete(something)
  end

  def create_phone
    # insert your logic here
    # e.g., @connection.post(something)
  end
end

Then you can accomplish your original goal:

client_one = MonthyApiClient.new
client_one.create_bike
client_two = MonthyApiClient.new
client_two.create_phone

Having worked through this explanation, I think your original code is an example of spending a lot of time trying to over-optimize prematurely. It's better to plan out your business logic and make it as simple as possible first. There's some good information at https://softwareengineering.stackexchange.com/a/80094 that may help explain this concept.

I've even skipped trying to optimize the code I've shown here because I don't know exactly how much commonality there is between creating and deleting bikes and phones. With this functional class, and with a better understanding of other code within this app, I might try to DRY it up (and that might mean going back to having a module with a Client class and either module methods or other classes to encapsulate the DRY logic), but it would be premature to try.

Your last question was about how to structure files and directories for modules and classes, and I would refer you to Ideal ruby project structure (among many other questions on this site) for more information.

anothermh
  • 9,815
  • 3
  • 33
  • 52
  • Thanks anothermh! I think i understand your explanation to simplify the construct. Using the domain name in de method name solves my overthinking. In order to split the domains further i could place them in modules (in different files) and include them in the MonthyApiClient class right? – Monthy Mar 24 '20 at 09:51
  • That's one possible approach, yes. – anothermh Mar 24 '20 at 17:38