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
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.
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
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.
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?
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.
Here is another solution – creating a class
AbbreviatedCampusesthat all campuses register themselves with usingAbbreviatedCampuses.register self. All campuses also implement anabbreviationmethod 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
ObjectSpaceorObject.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-lineself.campus_like?definition with theObjectSpacesolution.Some sample test code:
I think the two
CampusDetailsmethods would be better off as class methods, defined likedef self.campus_name, since they don’t have any instance variables. But I kept them instance methods in this solution for easy comparison.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
CollegeInTheSchoolsclass is added, which is presumed to have additional behavior.I am not very familiar with Ruby, but why couldn’t something like this work?
You mean, modifying the following two lines from the article?
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 thenameandmascotmethods 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, soObject.const_get(abbreviation)looks better to me.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
abbreviationdoesn’t start with ‘UMN’, then return something likeDisallowedAbbreviationinstead ofUnknownCampus?Ruby does have constructors –
initializemethods 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:Person.newhas 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 itsinitializewith 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 andDisallowedAbbreviationwould be more accurate and easier to understand.