loading...

Intro to OOP: Ruby 'One to Many' vs 'Many to Many'

mzakzook profile image mzakzook ・3 min read

As a Ruby beginner, I started off learning the basics - hashes, arrays and methods. These building blocks will continue to show up again and again. But how do these 'building blocks' come together?

When faced with the challenge of modeling real-world paradigms, Classes opened new doors to the possibilities of code. In Ruby, the use of Classes to create relationships is known as Object Oriented Programming (OOP). An individual instance of a Class is known as an Object or Instance.

When designing the relationships among Classes I find it helpful to first distinguish: am I dealing with a 'one to many' relationship or a 'many to many' relationship?

An example of a 'one to many' relationship is: a sports team and its players. A player only has one team (let's disregard outliers like Bo Jackson) and a team has many players. To connect classes that fall into this category, I would add an attribute, intended to store an entire object of the 'one' class, to the 'many' class. For our team example this means adding a 'team' attribute, intended to store a team object, to the Player class.

class Player
    attr_accessor :team
    attr_reader :name, :position
    @@all = []

    def initialize(name, position)
        @name = name 
        @position = position
        @@all << self
    end
end

class Team
    attr_reader :name, :city
    @@all = []

    def initialize(name, city)
        @name = name
        @city = city
        @@all << self
    end

    def players
        Player.all.select { |player| player.team == self }
    end
end

With the above 'players' method inside the Team class I am able to find a team's roster (all of its players), and this is possible because of the 'team' attribute inside the Player class.

An example of a 'many to many' relationship is: a social media app and its users. A person uses many social media apps and a social media app has many users. This scenario calls for a Join class. Let's run through how it looks...

class User
    attr_reader :name, :email
    @@all = []

    def initialize(name, email)
        @name = name
        @email = email
        @@all << self
    end
end

class App
    attr_reader :name, :url
    @@all = []

    def initialize(name, url)
        @name = name
        @url = url
        @@all << self
    end
end

class Account
    attr_reader :user, :app
    attr_accessor :username, :password
    @@all = []

    def initialize(user, app, username, password)
        @user = user
        @app = app
        @username = username
        @password = password
        @@all << self
    end
end

In the above example, I created three classes: a User class, an App class and an Account class. The User class only captures information about a person: her/his name & email. The App class only captures information about a social media app: its name and url. And the Account class captures all User and App information (by initializing an Account object with a User object and an App object), as well as information that is specific to an account: its username and password. A user does not inherently know about the app(s) she/he uses and an app does not inherently know about its users, but they are connected through accounts.

With an 'Accounts' join class in place, I am now able to keep track of all user/app relationships, and call on methods belonging to either class. For example, if I wanted to see all of the apps for a specific user I could write...

class User  
    ...
    def apps
        my_accts = Account.all.select {|acct| acct.user == self}
        my_accts.map {|acct| acct.app}
    end
end

Designing your Class relationships from the start will allow you to hit the ground running when you're ready to write your code. And realizing when you're dealing with a 'One to Many' vs a 'Many to Many' relationship should simplify your design process.

Posted on by:

mzakzook profile

mzakzook

@mzakzook

Code, food, travel and dogs. LA -> Oakland.

Discussion

markdown guide
 

Thank you for taking the time to write this. There are a few problems with this approach though if you don't mind.

  • User objects know about the Account class, Account.all, and Account.all.select, causing three couplings.
  • Exposed class variables are effectively global variables and global variables cause a lot of tricky errors when you least expect it. Any globally accessible thing should be constant at runtime.
  • It seems like a re-implementation of an object-relational mapping but without the relational database. ORMs are more of a necessary evil than a good pattern. OOP prefers direct references between objects rather than filtering through global lists.

Hopefully that is helpful. I would recommend, if you haven't already, Sandi Metz for learning more about OOP as her writing is very accessible and informative.

Regards,
kwstannard