Skip to content

Instantly share code, notes, and snippets.

@pinkopaque22
Created December 19, 2015 07:18
Show Gist options
  • Save pinkopaque22/2dca22f5cdcfe5dbd1e3 to your computer and use it in GitHub Desktop.
Save pinkopaque22/2dca22f5cdcfe5dbd1e3 to your computer and use it in GitHub Desktop.
Ruby's Advanced Classes
Parent classes should be as generic as possible. That is, they should be able to describe a broad range of things. A simple example of a class that lends itself to parenthood is a class named Dog. The purpose of this class will be to define the general attributes and behavior that all dogs share, regardless of breed. Attributes like having a name, four legs, two eyes, and a tail are shared amongst all dog breeds. Behavior like eating, sleeping, and barking are also shared amongst all dog breeds.
Let's take a look at inheritance in action by defining a Dog class. First we'll initialize the Dog class with a name attribute:
class Dog
attr_accessor :name
def initialize(name)
@name = name
end
end
Let's define a method in the Dog class named speak. It simply returns a string with the dog's name:
class Dog
attr_accessor :name
def initialize(name)
@name = name
end
def speak
"Ruff, my name is #{ @name }."
end
end
It's important to visualize relationships as we're defining classes. For example, we created the Dog class expecting to create child classes for different breeds of dogs. In this respect, the name attribute declared in the Dog class will make sense when applied to any child class. Simply put, our program is assuming that all dogs, regardless of breed, should have names.
Now let's create a child class of Dog:
class Mutt < Dog
end
Because Mutt inherits from Dog, it automatically has all functionality and attributes defined on Dog. This includes the initialize method we wrote. Unless we want to set additional attributes during initialization of a Mutt, Mutt will automatically use the initialize method from its "parent" class.
# After our two class definitions
d = Dog.new('Johnny')
d.name
#=> 'Johnny'
m = Mutt.new('Jimbo')
m.speak
#=> "Ruff, my name is Jimbo."
Create a Apple class. It should take a single argument on initialization -- a boolean value (true or false) asserting whether or not it is ripe. Apple should define a tasty? instance method that returns "Yes" when ripe and "Not yet" when not.
Below it, create a Fuji class which inherits from Apple and adds two methods:
flavor -- Returns "Sweet" if ripe and "Tart!" if not.
color -- Returns "Yellowish red" if ripe and "Green" if not.
Your class should work like this:
not_ready = Fuji.new(false)
not_ready.tasty?
#=> "Not yet"
not_ready.color
#=> "Green"
ready = Fuji.new(true)
ready.flavor
#=> "Sweet"
Bonus - use attr_accessor, the inherited initialize method, and the implied self to avoid directly using any instance (@) variables in the Fuji class.
__Specs__
describe Apple do
describe '#tasty?' do
it "is based on ripeness" do
expect( Apple.new(false).tasty? ).to eq("Not yet")
expect( Apple.new(true).tasty? ).to eq("Yes")
end
end
end
describe Fuji do
describe '#tasty?' do
it "inherits from Apple" do
expect( Fuji.new(false).tasty? ).to eq("Not yet")
expect( Fuji.new(true).tasty? ).to eq("Yes")
end
end
describe "#flavor" do
it "is based on ripeness" do
expect( Fuji.new(false).flavor ).to eq("Tart!")
expect( Fuji.new(true).flavor ).to eq("Sweet")
end
end
describe "#color" do
it "is based on ripeness" do
expect( Fuji.new(false).color ).to eq("Green")
expect( Fuji.new(true).color ).to eq("Yellowish red")
end
end
end
__Code__
class Apple
attr_accessor :ripe
def initialize(ripe)
@ripe = ripe
end
def tasty?
if ripe
"Yes"
else
"Not yet"
end
end
end
p Apple.new(true)
class Fuji < Apple
def flavor
if ripe
"Sweet"
else
"Tart!"
end
end
def color
if ripe
"Yellowish red"
else
"Green"
end
end
end
__Details__
__Code__
__Details__
In this exercise we'll create classes to represent a square, rectangle and circle. Since all three classes are different types of shapes, we'll define a parent class because we assume that they'll share some similar properties. Let's start by defining a parent class and three children:
class Shape
end
class Rectangle < Shape
end
class Square < Shape
end
class Circle < Shape
end
We need to be able to get and set the color of each shape. Let's use our parent class to do this efficiently:
class Shape
attr_accessor :color
end
class Rectangle < Shape
end
class Square < Shape
end
class Circle < Shape
end
We also want to be able to calculate the area of each shape. Rectangles and squares share the same formula for area (width * height), but a circle's area is calculated as Pi * (radius*radius). Since our shapes share two different area formulas amongst them, we can't define an area method in the Shape parent class. We'll want to refactor our code, but let's start by defining area methods for each child:
class Shape
attr_accessor :color
end
class Rectangle < Shape
def area
# width * height
end
end
class Square < Shape
def area
# width * height
end
end
class Circle < Shape
def area
# Pi * (radius*radius)
end
end
The area methods we defined above will require attributes. Let's create getter and setter methods for the attributes needed for the area methods:
class Shape
attr_accessor :color
end
class Rectangle < Shape
attr_accessor :width, :height
def area
# width * height
end
end
class Square < Shape
attr_accessor :width, :height
def area
# width * height
end
end
class Circle < Shape
attr_accessor :radius
def area
# Pi * (radius*radius)
end
end
We'll also need to initialize each class. Remember that each shape must be initialized with a color, which is why we created a color getter and setter in the Shape class. Let's start with the initialize method for Shape:
class Shape
attr_accessor :color
def initialize(color = nil)
@color = color || 'Red'
end
end
This allows us to initialize an instance of shape with a defined color. If the color is not provided, we'll default the color to red with a conditional assignment.
Next we'll want to define initialize methods for the child classes.
Notice how we use super to keep our code DRY.
class Shape
attr_accessor :color
def initialize(color = nil)
@color = color || 'Red'
end
end
class Rectangle < Shape
attr_accessor :width, :height
def initialize(width, height, color = nil)
@width, @height = width, height
super(color) # this calls Shape#initialize
end
def area
# width * height
end
end
class Square < Shape
attr_accessor :width, :height
def initialize(width, height, color = nil)
@width, @height = width, height
super(color) # this calls Shape#initialize
end
def area
# width * height
end
end
class Circle < Shape
attr_accessor :radius
def initialize(radius, color = nil)
@radius = radius
super(color) # this calls Shape#initialize
end
def area
# Pi * (radius*radius)
end
end
Let's take a step back and regard our code. The Rectangle and Square class are looking awfully similar -- this is a bad "code smell." A code smell is something that just "feels" wrong in its implementation. Earlier we questioned the relationship between a square and a rectangle, so let's figure out a DRYer way to structure these classes. A square is really a more specific type of rectangle -- one with equal width and height -- and so it represents a prime candidate for classical inheritance.
Let's make Square inherit from Rectangle:
...
class Square < Rectangle
def initialize(side, color = nil)
super(side, side, color) # calls `Rectangle#initialize`
end
end
...
The width and height attr_accessors, as well as the area method, are all inherited from Rectangle now.
As you can see this is a much DRYer implementation of Square.
We've set up a solid class structure for our program, now here's a challenge for you.
We should be able to call a larger_than? method on each shape. This method should evaluate two shapes and return true or false depending on one shape's area being larger than the other. In other words, the larger_than? method should return true if the receiving object is larger than the argument object:
square.larger_than?(rectangle)
#=> true if the square is larger than the rectangle, false if not
Hint: To find the area of a circle, you can use Math::PI. Remember that the area of a circle is calculated as Pi multiplied by the square of the radius. Math::PI evaluates to Pi and can be used like any other number in a calculation.
Math is actually a module, with PI "namespaced" under it. We could include Math and reference PI directly, or simply call Math::PI to return the value of Pi. If you're curious, talk to your mentor about using modules for namespaced functionality.
__Specs__
describe "Shape" do
describe "larger_than?" do
it "should tell if a shape is larger than another shape" do
class A < Shape
def area
5
end
end
class B < Shape
def area
10
end
end
a = A.new
b = B.new
expect( b.larger_than?(a) ).to eq(true)
expect( a.larger_than?(b) ).to eq(false)
end
end
describe "color" do
it "should be able to get and set color" do
s = Shape.new
expect( s.respond_to?(:color) ).to eq(true)
expect( s.respond_to?(:color=) ).to eq(true)
end
end
end
describe "Rectangle" do
describe "initialize" do
it "should take width, height" do
r = Rectangle.new(1, 2, "Blue")
expect( r.width ).to eq(1)
expect( r.height ).to eq(2)
r = Rectangle.new(5, 6, "Blue")
expect( r.width ).to eq(5)
expect( r.height ).to eq(6)
end
it "should be able to set a color if given" do
r = Rectangle.new(1, 2, "Blue")
expect( r.color ).to eq("Blue")
r = Rectangle.new(1, 2, "Green")
expect( r.color ).to eq("Green")
end
it "should be able to set the default color to Red" do
r = Rectangle.new(1, 2)
expect( r.color ).to eq("Red")
end
end
describe "area" do
it "should return correct area for large rectangle" do
r = Rectangle.new(100, 240)
expect( r.area ).to eq(24000)
end
it "should return correct area for a small rectangle" do
r = Rectangle.new(5, 2)
expect( r.area ).to eq(10)
end
end
end
describe "Square" do
describe "initialize" do
it "should only take a side and color" do
s = Square.new(5, "Green")
expect( s.width ).to eq(5)
expect( s.height ).to eq(5)
expect( s.color ).to eq("Green")
end
it "should set a default color" do
s = Square.new(13)
expect( s.width ).to eq(13)
expect( s.height ).to eq(13)
expect( s.color ).to eq("Red")
end
end
describe "area" do
it "should return right area" do
s = Square.new(15)
expect( s.area ).to eq(225)
end
end
end
describe "Circle" do
describe "initialize" do
it "should only take radius and color" do
c = Circle.new(7, "Brown")
expect( c.radius ).to eq(7)
expect( c.color ).to eq("Brown")
end
it "should set a default color" do
c = Circle.new(8)
expect( c.radius ).to eq(8)
expect( c.color ).to eq("Red")
end
it "should not respond to width or height" do
c = Circle.new(8)
expect( c.respond_to?(:width) ).to eq(false)
expect( c.respond_to?(:width=) ).to eq(false)
expect( c.respond_to?(:height) ).to eq(false)
expect( c.respond_to?(:height=) ).to eq(false)
end
end
describe "area" do
it "returns the area of a small circle" do
c = Circle.new(3)
expect( c.area ).to eq(28.274333882308138)
end
it "returns the area of a large circle" do
c = Circle.new(23)
expect( c.area ).to eq(1661.9025137490005)
end
end
end
#http://ruby.about.com/od/faqs/qt/Nameerror-Uninitialized-Constant-Object-Something.htm
__Code__
class Shape
attr_accessor :color
def initialize(color = nil)
@color = color || 'Red' #why single quote?
end
def larger_than?(shapeType)
if self.area >= shapeType.area
true
else
false
end
end
end
class Rectangle < Shape
attr_accessor :width, :height
def initialize(width, height, color = nil)
@width, @height = width, height
super(color)
end
def area
p @width * @height
end
end
class Square < Rectangle
def initialize(side, color = nil)
@width, @height = width, height
super(side, side, color)
end
end
class Circle < Shape
attr_accessor :radius
def initialize(radius, color = nil)
@radius = radius
super(color)
end
def area
p Math::PI * (@radius * @radius)
end
end
__Details__
InterestCalculator should take four arguments on initialization:
Principal
Rate of interest
Years compounded
Times compounded per year
And it should define the following functions
amount - A simple number representation, rounded to 2 decimals.
statement - A string of the format, "After 5 years I'll have 2000 dollars!"
Make sure to use self (implied or explicit) throughout the class. Instance variables should only be directly accessed in the initialize function, and amount should only be calculated in one place.
Hint - to use instance @ variables only in the initialize method, you'll need to use attr_accessor.
The compound interest formula:
amount = principal * (1 + self.rate / self.times_compounded) ** (self.times_compounded * self.years)
__specs__
describe InterestCalculator do
before { @calc = InterestCalculator.new(500, 0.05, 4, 5) }
describe "#amount" do
it "calculates correctly" do
expect( @calc.amount ).to eq(610.1)
end
end
describe "#statement" do
it "calls amount" do
@calc.stub(:amount).and_return(100)
expect( @calc.statement ).to eq("After 4 years I'll have 100 dollars!")
end
end
end
__Code__
class InterestCalculator
attr_accessor :amount, :statement, :years
def initialize(principal, rate, years, times_compounded)
@principal = principal
@rate = rate
@years = years
@times_compounded = times_compounded
self.amount = principal * (1 + rate / times_compounded) ** (times_compounded * years)
self.amount = amount.round(1) #isnt .round for 2 decimals 1.5?
end
def statement
statement = "After #{years} years I'll have #{amount} dollars!"
end
end
__Details__
Let's create BankAccount, CheckingAccount and SavingsAccount classes. CheckingAccount and SavingsAccount should inherit from BankAccount.
Start by using the following code to stub your classes:
class BankAccount
end
class CheckingAccount < BankAccount
end
class SavingsAccount < BankAccount
end
A BankAccount should have:
an initialize method that takes an initial balance as the only parameter that should be assigned to an instance variable named balance
a deposit method that increases the balance by the amount specified as the parameter
a withdraw method that deducts the specified amount from the balance
A CheckingAccount should inherit from BankAccount and call super(amount) from its withdraw method. It should then charge an overdraft fee of $50 if the amount they withdrew exceeded the account's existing balance. A SavingsAccount should also inherit from BankAccount and call super(amount) from its withdraw method, but not charge an overdraft fee. If the amount attempted to withdraw is greater than the account's balance, cancel the transaction and add the amount back to the balance.
__Specs__
describe BankAccount do
describe "initialization" do
it "takes an initial balance" do
acc = BankAccount.new(283)
expect( acc.balance ).to eq(283)
end
end
describe "#new" do
it "returns the balance" do
acc = BankAccount.new(283)
expect(acc.balance).to eq 283
end
end
describe "#deposit" do
it "returns the new balance" do
acc = BankAccount.new(283)
acc.deposit(10)
expect(acc.balance).to eq 293
end
end
end
describe CheckingAccount do
it "has BankAccount as its parent class" do
expect( CheckingAccount.superclass ).to eq(BankAccount)
end
describe "#withdraw" do
it "returns the new balance" do
acc = CheckingAccount.new(283)
acc.withdraw(283)
expect(acc.balance).to eq(0)
end
it "it charges an overdraft fee if you overdraw" do
acc = CheckingAccount.new(283)
acc.withdraw(284)
expect(acc.balance).to eq(-51)
end
end
end
describe SavingsAccount do
it "has BankAccount as its parent class" do
expect( SavingsAccount.superclass ).to eq(BankAccount)
end
describe "#withdraw" do
it "returns the new balance" do
acc = SavingsAccount.new(283)
new_balance = acc.withdraw(283)
expect(new_balance).to eq 0
end
it "prevents overdrawing" do
acc = SavingsAccount.new(283)
new_balance = acc.withdraw(284)
expect(new_balance).to eq(283)
end
end
end
__Code__
class BankAccount
attr_accessor :balance
def initialize(balance)
@balance = balance
end
def deposit(amount)
@balance += amount
@balance
end
def withdraw(amount)
@balance -= amount
@balance
end
end
class CheckingAccount < BankAccount
OVERDRAFT_FEE = 50
def withdraw(amount)
super(amount)
if @balance < 0
@balance -= OVERDRAFT_FEE
end
@balance
end
end
class SavingsAccount < BankAccount
def withdraw(amount)
super(amount)
if @balance < 0
@balance += amount
p "Preventing Overdraft"
end
@balance
end
end
__Results__
BankAccount initialization takes an initial balance
BankAccount#new returns the balance
BankAccount#deposit returns the new balance
CheckingAccount has BankAccount as its parent class
CheckingAccount#withdraw returns the new balance
CheckingAccount#withdraw it charges an overdraft fee if you overdraw
SavingsAccount has BankAccount as its parent class
SavingsAccount#withdraw returns the new balance
SavingsAccount#withdraw prevents overdrawing
__Output__
"Preventing Overdraft"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment