GithubHelp home page GithubHelp logo

phase-3-metaprogramming-mass-assignment-and-class-initialization's Introduction

Mass Assignment and Class Initialization

Learning Goals

  • Understand how to use mass assignment to metaprogram a Ruby class

Introduction

You might recall that metaprogramming is the practice of writing code that writes code for us. So, what does that have to do with mass assignment?

Let's say we want to use the Twitter API to create users for our own application. The scenario is that we are developing a web application and we want our users to be able to sign in via Twitter. Thus, our own users are pulled from Twitter and we need to take the data we get from Twitter—for example a user's name, age and location—and use them to make instances of our own User class. Let's take a look at a code snippet:

class User
  attr_accessor :name, :age, :location, :user_name

  def initialize(user_name:, name:, age:, location:)
    @user_name = user_name
    @name = name
    @location = location
    @age = age
  end
end

Here we have our user class. It initializes with keyword arguments, i.e., a hash of attributes. For the purposes of this example, we won't get into the specifics of how we request and receive data from the Twitter API. Suffice to say that we send a request to the Twitter API and get a return value of a hash full of user attributes. For example:

twitter_user = { 
  name: "Sophie", 
  user_name: "sm_debenedetto", 
  age: 26, 
  location: "NY, NY"
}

With what we've learned of mass assignment so far, we can use the twitter_user hash to instantiate a new instance of our own User class:

sophie = User.new(twitter_user)
# => #<User:0x007fa1293e68f0 @name="Sophie", @age=26, @user_name="sm_debenedetto", @location="NY, NY">

So far so good. But, what if Twitter changes their API without telling us? (How could they? Don't they know who we are?) After all, we are not in charge of Twitter or their API, they can do whatever they want, whenever they want, with no regard to our application which relies on their data. Let's say Twitter makes a change that we're unaware of. Now when we request data from their API, we get this return value:

new_twitter_user = {
  name: "Sophie", 
  user_name: "sm_debenedetto", 
  location: "NY, NY"
}

Notice that the new_twitter_user no longer has an age. Let's see what happens if we try to create new Users using the same old User class code:

User.new(new_twitter_user)
# => ArgumentError: missing keyword: age

Our program broke!

Let's play it with another scenario. Let's say the Twitter API changed and now returns data to us in the following manner:

newest_twitter_user = {
  name: "Sophie", 
  user_name: "sm_debenedetto", 
  age: 26, 
  location: "NY, NY", 
  bio: "I'm a programmer living in NY!"
}

Now let's see what happens when we try to make a new instance of our User class with the same old User class code:

User.new(newest_twitter_user)
# => ArgumentError: unknown keyword: bio

Our program breaks! Clearly, we need a way to abstract away our User class' dependency on specific attributes. If only there were a way for us to tell our User to get ready to accept some unspecified number and type of attributes...

Mass Assignment and Metaprogramming

Guess what? We can achieve exactly that goal using metaprogramming and mass assignment. Let's take a look at how it's done, then we'll break it down together. Here's our new and improved User class:

class User
  attr_accessor :name, :user_name, :age, :location, :bio

  def initialize(attributes)
    attributes.each do |key, value| 
      self.send("#{key}=", value)
    end
  end
end

We define our initialize method to take in some unspecified attributes object. Then, we iterate over each key/value pair in the attributes hash. The name of the key becomes the name of a setter method and the value associated with the key is the value you want to pass to that method. The ruby #send method then calls the method name that is the key’s name, with an argument of the value. In other words:

self.send("#{key}=", value)

Is the same as:

instance_of_user.key = value

Where each key/value pair is a member of our hash, one such iteration might read:

instance_of_user.name = "Sophie"

And have the same result as:

instance_of_user = User.new
instance_of_user.name = "Sophie"

A Closer Look at #send

The #send method is just another way of calling a method on an object. For example, we know that instances of the User class have a #name= method that allows us to set the name of a user to a particular string:

sophie = User.new
sophie.name = "Sophie"

Now, when we use the #name getter method, it will return the correct name:

sophie.name
# => "Sophie"

Let's look at the same behavior using #send:

sophie = User.new
sophie.send("name=", "Sophie")

That is generally considered to be clunky and ugly. It's whats known as "syntactic vinegar". We prefer the "syntactic sugar" of the first approach.

The #send method, however, is a very useful tool for our metaprogramming purposes. It allows us to abstract away the specific method call:

sophie = User.new
sophie.send("#{method_name}=", value)

This is exactly what's happening in our initialize method in the example above, where self refers to the User instance that is being initialized at that point in time.

Taking things further: dynamically setting getters and setters

Let's say we didn't want to specify the individual getter and setter methods using named symbols like so:

class User
  # we don't want to do this anymore :(
  attr_accessor :name, :user_name, :age, :location, :bio

  def initialize(attributes)
    attributes.each do |key, value| 
      self.send("#{key}=", value)
    end
  end
end

Instead, we want to dynamically set those methods so that we have a getter and setter automatically declared for every attribute in the attributes Hash. How could we do this?

First, we need to remember that attr_accessor is a class method just like attr_reader and attr_writer. This means we can dynamically add getters or setters, or both, by doing the following:

class User
  def initialize(attributes)
    attributes.each do |key, value|
      # create a getter and setter by calling the attr_accessor method
      self.class.attr_accessor(key)
      self.send("#{key}=", value)
    end
  end
end

By making that one small change, we can now get and set every attribute on an object instantiated from User.

Conclusion

With this pattern, we have made our code much more flexible. We can easily alter the number of attributes in the class and change the hash that we initialize the class with, without editing our initialize method. Now, we're programming for the future. Our initialize method is flexible and we can leave it alone. That is one major goal of design in object oriented programming — the writing of code that accommodates future change and doesn't require a lot of modification, even as it grows.

Resources

phase-3-metaprogramming-mass-assignment-and-class-initialization's People

Contributors

annjohn avatar bal360 avatar brendamichelle avatar dakotalmartinez avatar drakeltheryuujin avatar ga-be avatar gj avatar hellorupa avatar ihollander avatar imkaruna avatar jonbf avatar kaylee42 avatar kode-tiki avatar lizbur10 avatar nothingisfunny avatar pletcher avatar sarogers avatar sophiedebenedetto avatar

Stargazers

 avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

phase-3-metaprogramming-mass-assignment-and-class-initialization's Issues

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.