Skip to main content

CSC 322.01, Class 39: Rethinking object-oriented design (6)

Overview

  • Preliminaries
    • Notes and news
    • Upcoming work
    • Extra credit
    • Questions
  • Inheritance vs. mixins
  • Issues with multiple inheritance
  • A detour
  • Close code reading
  • Another detour
  • Exercise

Preliminaries

News / Etc.

  • Do we need mentor sessions next week? Probably not.
  • Please invite your community partners to our final presentations at 2:00 p.m. on Friday, 11 May 2018. (Our normal classroom.)
  • I responded to all of the journals that were submitted by 7:30 p.m.
  • Monday’s class: Work on presentations.
  • Wednesday’s class: EOCEs and debrief (and work on presentations)
    • Advice will be useful
  • Friday’s class: Presentations

Upcoming work

  • No more readings.
  • This week is your last week of reports.
  • Presentations a week from today.
    • About ten minutes + five minutes for Q&A.
    • Make sure that your computer works in this class room or that you can use the computer that is in the room..
  • Final portfolios due end of finals week.
  • Project instructions due end of finals week.

Good things to do (Academic/Artistic)

  • Public science symposium TODAY
    • Up-goer five posters TODAY
    • Up-goer five poster reception 3pm TODAY (we’ll go)
    • Talk TODAY at 4:00 by Minute Physics Alum Celebs

Good things to do (Peer)

  • The Grinnellian Festival TOMORROW on commencement stage. Your classmates are probably performing at about 5:30 p.m.

Good things to do (Misc)

  • Senior week stuff
  • Waltz

Less good things to do (Misc)

  • Alice
  • Rocky Horror Picture Show

Friday PSA

  • Please be moderate
  • The bubble is contracting; be safe
  • Don’t damage other people; consent is essential

Questions

Inheritance vs. Mixins

I was interested to see that many of you approached this question conceptually, in terms of what and how you can model. I was actually considering the issue more in terms of what it gives you as programmer.

  • A lot of OOD is finding ways to keep (make) your code DRY
    • Also maintainable, reusable, …
  • Both provide mechanisms to automatically delegate method calls to something else. (Effectively “inherit methods”)
    • I believe both support both instance and class methods, but I have not verified that carefully.
    • Class methods are the static methods in Java. They are associated with the class as a whole rather than individual objects.
    • Instance methods are associated with individual objects.
    • Integer.parse(String) vs 5.square()
  • Inheritance also permits you to inherit fields. (As far as I can tell, mixins do not normally include fields, probably for some reasons we consider below.)
  • You can have multiple mixins but at most one superclass.
  • You can explicitly delegate a method call to a superclass, even if you’ve overridden it.
    • It is much more complicated to delegate a method call to a mixin.
class A
  def initialize(args)
    puts "Initializing A"
  end

  def speak
    "eh"
  end
end

class B < A
  def initialize (args)
    puts "Initializing B"
    super
  end
     
  def speak
    "be"
  end

  # ...
  def something
    # I want to call the speak method from my superclass
    A.speak  #no
  end
end

Issues with multiple inheritance

A central issue: What happens when both classes have a method with the same name but different implementations.

class Super
  def flies
    true
  end
end

class Uber
  def flies
    ["fruit", "horse"]
  end
end

class Sub < Super, Uber
end

What happens when you do something like the following

s = Sub.new
s.flies

Worse yet, what happens when field names overlap/collide?

In Java

public class Point {
  double x;
  double y;

  public void hshift(double amt) {
    x += amt;
  }
}

public class AlphabetEncoding {
  int a = 1;
  int b = 2;
  ...
  int x = 24;
  int y = 25;
  int z = 26;
}

public class Insane extends Point extends AlphabetEncoding {
  ...
}
class Point
  attr_reader :x, :y

  def initialize
    @x = 0
    @y = 0
  end

  def hshift(amt)
    @x += amt
  end
end

class AlphabetEncoding
  attr_reader :a, :b, :c, ...., :x, :y, :z
  def initialize
    @a = 1;
    @b = 2;
    ...
    @x = 24;
    @y = 25;
    @z = 26;
  end
end

class Stupid < Point, AlphabetEncoding
end

Do mixins help with those kinds of issues? If so, how?

module Tom
  def describe
    "American Actor"
  end
end

module Chex
  def describe
    ["wheat chex", "pretzels", "bagel chips", "extra gluten"]
  end
end

class Mixup
  include Tom
  include Chex
end

What happens with Mixup.new.describe?

  • In Ruby, there are clear priorities. The most recently added mixin (module) gets priority.
  • Mixins are less the solution than a clear policy on what it means to mixin multiple classes.

Detour

Note that we might have chosen to define the silly AlphabeticEncoding class as follows.

class AlphabetEncoding
  def valid(sym)
    sym.length == 1 and "a" <= sym.to_s and sym.to_s <= "z"
  end

  def respond_to_missing?(method)
    valid method
  end

  def method_missing(method, *args, &block)
    if valid method
      1 + (method.to_s.ord - "a".ord)
    else
      super
    end
  end
end

Close reading

Few of you read the text as closely as I wanted. Here’s the code we started with, adapted from https://github.com/skmetz/poodr/blob/master/chapter_7.rb

class Schedule
  def scheduled?(schedulable, start_date, end_date)
    puts "This #{schedulable.class} " +
         "is not scheduled\n" +
         "  between #{start_date} and #{end_date}"
    false
  end
end

module Schedulable
  attr_writer :schedule

  def schedule
    @schedule ||= ::Schedule.new
  end

  def schedulable?(start_date, end_date)
    !scheduled?(start_date - lead_days, end_date)
  end

  def scheduled?(start_date, end_date)
    schedule.scheduled?(self, start_date, end_date)
  end

  # includers may override
  def lead_days
    0
  end

end

class Vehicle
  include Schedulable

  def lead_days
    3
  end

  # ...
end

class Mechanic
  include Schedulable

  def lead_days
    4
  end

  # ...
end

require 'date'
starting = Date.parse("2015/09/04")
ending   = Date.parse("2015/09/10")

v = Vehicle.new
v.schedulable?(starting, ending)
# Note: schedulable? calls scheduled with self as the first parameter,
#   which calls scheduled in the Schedule class, which takes
#   start_date and end_date as parameters.
#   puts "This #{schedulable.class} " +
#        "is not scheduled\n" +
#        "  between #{start_date} and #{end_date}"
# I expect to see
#   This Vehicle is not scheduled 
#     between 2015-09-01 and 2015-09-10
# Metz shows us
# This Vehicle is not scheduled
#   between 2015-09-01 and 2015-09-10
#  => true

m = Mechanic.new
m.schedulable?(starting, ending)
# I expect to see
#   This Mechanic is not scheduled 
#     between 2015-08-31 and 2015-09-10
# This Mechanic is not scheduled
#   between 2015-02-29 and 2015-09-10
# Whoops!  There wasn't a February 29, 2015
#  => true

How are the two outputs different?

Why are the two outputs different?

Why might I want you to read the outputs closely?

  • You learn from doing so.

Another detour

module Schedulable
  attr_writer :schedule

What does the attr_writer line in the Schedulable module do?

  • Gives you a setter for the schedule field in anything that imports Schedulable.

Why does Metz include it?

  • I’m not sure. It seems dangerous to me.

Why not use attr_accessor?

  • That provides a getter and setter.
  • Perhaps we don’t want the client to get the schedule. If the client can get the schedule, the client might decide to work directly with the schedule, rather than through the interface that our object provides. We want to add lead days.
  • Or perhaps because we’re already defining a schedule method.
module Mutate
  attr_writer :x
end

class Thing
  include Mutate
end

t = Thing.new
t.x = 5 # Okay, even if not the intent of THing
t.y = 5 # Not okay, not defined
t.x # Not okay, not defined
t.y # Not okay, not defined

An Exercise

Metz started the chapter with a question about how we’d handle Recumbent Mountain Bikes. But she never quite answers it. So it’s our job. (And no, most of you did not do a sufficient job.)

Review

Here’s the code we started with, adapted from https://github.com/skmetz/poodr/blob/master/chapter_6.rb

class Bicycle
  attr_reader :size, :chain, :tire_size

  def initialize(args={})
    @size       = args[:size]
    @chain      = args[:chain]     || default_chain
    @tire_size  = args[:tire_size] || default_tire_size
    post_initialize(args)
  end

  def spares
    { tire_size: tire_size,
      chain:     chain}.merge(local_spares)
  end

  def default_tire_size
    raise NotImplementedError
  end

  # subclasses may override
  def post_initialize(args)
    nil
  end

  def local_spares
    {}
  end

  def default_chain
    '10-speed'
  end

end

class RoadBike < Bicycle
  attr_reader :tape_color

  def post_initialize(args)
    @tape_color = args[:tape_color]
  end

  def local_spares
    {tape_color: tape_color}
  end

  def default_tire_size
    '23'
  end
end

class MountainBike < Bicycle
  attr_reader :front_shock, :rear_shock

  def post_initialize(args)
    @front_shock = args[:front_shock]
    @rear_shock =  args[:rear_shock]
  end

  def local_spares
    {rear_shock:  rear_shock}
  end

  def default_tire_size
    '2.1'
  end
end

class RecumbentBike < Bicycle
  attr_reader :flag

  def post_initialize(args)
    @flag = args[:flag]
  end

  def local_spares
    {flag: flag}
  end

  def default_chain
    '9-speed'
  end

  def default_tire_size
    '28'
  end
end

What do we see as differences between the various kinds of bikes?

The big question

How do we model all of this when we may have recumbent mountain bikes? What classes and modules would you create?

And another set of notes for Sam

These are my attempt to determine whether I can choose which module’s method to call when there’s a name overlap.

module A
  def comment
    '#{@user} says "eh"'
  end
end

module B
  def comment
    'Where can #{@user} be?'
  end
end

class C
  include A
  include B
  def initialize(name = "Sam")
    @user = name
  end

  def both
    A::instance_method(:comment).bind(self).call + "\n" + 
    B::instance_method(:comment).bind(self).call
  end
end
module Flag
  def post_initialize(args)
    puts "Flag"
  end
  def announce
    "Flag me down!"
  end
end

module Shocks
  def post_initialize(args)
    puts "Shocks"
  end
  def announce
    "So shocking!"
  end
end

class Bicycle
  def initialize(args)
    # Get the list of included modules (which I'm calling the parts)
    @parts = self.class.included_modules
    @parts.delete(Kernel)

    @parts.each do |part|
      if part.instance_methods.include?(:post_initialize) then
        part.instance_method(:post_initialize).bind(self).call(args)
      end
    end
  end

  # Override!
  def local_parts
    []
  end

  # Override
  def post_initialize(args)
  end
end

class RecumbentMountainBike < Bicycle
  include Flag
  include Shocks
end