1. 2
  1.  

  2. 1

    Here is another solution – creating a class AbbreviatedCampuses that all campuses register themselves with using AbbreviatedCampuses.register self. All campuses also implement an abbreviation method as the single place their abbreviation is written.

    With this method, when you add a new campus, you only need to change one place in the code. But you don’t have to do the slow and hard-to-understand metaprogramming with ObjectSpace or Object.const_get – you explicitly link the new campus to the new registry. And the registration line in each campus class is shorter than the three-line self.campus_like? definition with the ObjectSpace solution.

    class UnknownCampus
      def name
        "Unknown Campus"
      end
    
      def mascot
        "Unknown Mascot"
      end
    end
    
    class AbbreviatedCampuses
      @@campus_like_classes = Hash.new(UnknownCampus)
    
      def self.register(klass)
        abbreviation = klass.new.abbreviation
        @@campus_like_classes[abbreviation] = klass
      end
    
      def self.build_from(abbreviation)
        @@campus_like_classes[abbreviation].new
      end
    end
    
    # The above two classes must be loaded first.
    # The rest can load in any order.
    
    class CampusDetails
      def campus_name(abbreviation)
        AbbreviatedCampuses.build_from(abbreviation).name
      end
    
      def campus_mascot(abbreviation)
        AbbreviatedCampuses.build_from(abbreviation).mascot
      end
    end
    
    class UMNTC
      def name
        "University of Minnesota Twin Cities"
      end
      def mascot
        "Gopher"
      end
      def abbreviation
        "UMNTC"
      end
    
      AbbreviatedCampuses.register self
    end
    
    # snipped some repetitive campuses from this comment…
    
    class UMNTCRO
      def name
        "University of Minnesota Rochester"
      end
      def mascot
        "Raptor"
      end
      def abbreviation
        "UMNTCRO"
      end
    
      AbbreviatedCampuses.register self
    end
    
    class CollegeInTheSchools
      def name
        "College in the Schools"
      end
      def mascot
        ""
      end
      def abbreviation
        "CITS"
      end
    
      AbbreviatedCampuses.register self
    end
    

    Some sample test code:

    puts CampusDetails.new.campus_name("CITS")
    puts CampusDetails.new.campus_mascot("UMNTCRO")
    puts CampusDetails.new.campus_mascot("NOSUCH")
    

    I think the two CampusDetails methods would be better off as class methods, defined like def self.campus_name, since they don’t have any instance variables. But I kept them instance methods in this solution for easy comparison.

    1. 1

      Another solution: if all you need from the campuses is to get their data, you could just use a Hash instead.

      Though this solution doesn’t support custom class behavior, it’s a lot simpler and more concise. This solution could apply before the step in the article when the CollegeInTheSchools class is added, which is presumed to have additional behavior.

      class CampusDetails
        CAMPUS_LIKES_BY_ABBR = {
          "UMNTC" => {
            name: "University of Minnesota Twin Cities",
            mascot: "Gopher",
          },
          "UMNMO" => {
            name: "University of Minnesota Morris",
            mascot: "Cougar",
          },
          "UMNCR" => {
            name: "University of Minnesota Crookston",
            mascot: "Golden Eagle",
          },
          "UMNTCRO" => {
            name: "University of Minnesota Rochester",
            mascot: "Raptor",
          },
          "CITS" => {
            name: "College in the Schools",
            mascot: "",
          },
        }
        CAMPUS_LIKES_BY_ABBR.default = {
          name: "Unknown Campus",
          mascot: "Unknown Mascot",
        }
      
        def campus_name(abbreviation)
          CAMPUS_LIKES_BY_ABBR[abbreviation][:name]
        end
      
        def campus_mascot(abbreviation)
          CAMPUS_LIKES_BY_ABBR[abbreviation][:mascot]
        end
      end
      
      1. 1

        I am not very familiar with Ruby, but why couldn’t something like this work?

        class CampusDetails
          private
        
          def build_campus(abbreviation)
            begin
              Object.const_get(abbreviation)
            rescue
              UnknownCampus
            end.new
          end
        end
        
        1. 1

          You mean, modifying the following two lines from the article?

          # abbreviation[-2,2] gets the last two characters of the abbreviation string
          Object.const_get("UMN#{abbreviation[-2,2]}")
          

          Yes, Object.const_get(abbreviation) should work. I guess the author wanted to enforce that the class started with “UMN”, perhaps as a weak security measure to avoid the name and mascot methods being called on non-university classes. But that behavior seems brittle, because I can imagine a lot of universities that don’t follow that naming convention, so Object.const_get(abbreviation) looks better to me.

          1. 1

            Yes, that’s correct. I thought it had something to do with the point the article was trying to make, just verifying it wasn’t. So if I understand correctly, a factory is like a constructor? In languages with constructors, is there any reason to use factories?

            Also, if the abbreviation thing is some sort of security thing, wouldn’t it be better to check to see if abbreviation doesn’t start with ‘UMN’, then return something like DisallowedAbbreviation instead of UnknownCampus?

            1. 2

              Ruby does have constructors – initialize methods are constructors. Factories are distinct and have a different purpose. The difference is that constructors only construct one type of object, while the job of a factory is to choose the appropriate type before constructing it. Factories delegate to the constructors of the individual classes it chooses between.

              Here is an example with a constructor, Person#initialize, and a factory method, Person.new:

              class Male < Person
                # male-specific method implementations
              end
              class Female < Person
                # female-specific method implementations
              end
              
              class Person
                attr_reader :name
              
                # a constructor
                def initialize(name)
                  @name = name
                end
              
                # a factory
                # it could be called `build` like in the article, but `new` is the conventional name
                def self.new(gender, name)
                  if gender == :male
                    return Male.new(name)
                  else
                    return Female.new(name)
                  end
                end
              end
              
              # how to use the factory (which calls the constructor)
              alice = Person.new(:female, "Alice")
              

              Person.new has been overridden here to act as a factory. If you don’t define .new, the default implementation is used, which creates an empty object of that class’s type and then runs its initialize with the given arguments. I don’t think that default implementation could be called a factory, because it only ever creates one type of object.

              Yes, if the author meant the [-2,2] as a security precaution, prefix-checking and DisallowedAbbreviation would be more accurate and easier to understand.