Created
December 19, 2015 07:18
-
-
Save pinkopaque22/2dca22f5cdcfe5dbd1e3 to your computer and use it in GitHub Desktop.
Ruby's Advanced Classes
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
__Details__ | |
__Code__ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
__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 | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
__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 | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
__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