Skip to content

Instantly share code, notes, and snippets.

@Rhoxio
Created August 2, 2019 21:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Rhoxio/3b08768ccde827a12881424c55f19ab5 to your computer and use it in GitHub Desktop.
Save Rhoxio/3b08768ccde827a12881424c55f19ab5 to your computer and use it in GitHub Desktop.
=begin
This is a simple example of some Object Oriented Programming focusing on a dynamic Animal class that includes some basic
functionality to model animals being able to eat certain foods and gain stats from said food. This is only an abstraction
to a base level of interaction and isn't wholly representative of the actual reality of feeding animals a diverse diet, but
the overarching concepts of OO pop out in a few ways:
1. Each class has methods that maintain single responsibility. Essentially, they execute a single action or a related set of actions that make sense
for the level of logical abstraction you are going for.
2. Each method on the class is doing one of a few things: ASKING the other class to do something with given data OR amending itself OR
providing specific, structured data to work against without 'demanding' it from the other class by calling specific attributes
on the class. Grouping is important!
3. Maintains its own state and does not have values set outside of the class itself. If there are changes made, the other class is ASKING THE BASE CLASS
TO MAKE THE CHANGES, BUT NOT DIRECTLY FORCING IT TO DO SO IN THE SUPERCLASSES LOGIC AS THE CONSTRAINTS ARE SPECIFIC TO THE CLASS AND NOT TO THE IMPLEMENTATION
AS A WHOLE, more or less.
You don't want the 'Animal' class to forefully set any attributes on the 'Food' class ANYHWERE AT ALL. You want to pass data and ask the
Food to change itself if anything, but literally hard-setting attributes from the Animal class is a bad idea.
The main idea surrounding OO programming is that you want to have your classes interact with one another and send messages, but you should
NOT have any class directly amending another class's attributes without ASKING it to do so through a method on the class to be amended, as this
standardizes the ways in which the data can go in and out of your class.
=end
# We will start with the Animal class initially. The animal has some attributes and data associated with it - namely the
# instance variables (@whatever) in the initialize method. This is how we persist data in runtime (when your code is running)
# without having a database to reference against.
# The data will not 'save' in the conventional sense, but persists in runtime space until the Ruby code is done executing.
class Animal
attr_reader :species, :name, :diet, :energy, :muscle, :fat
# attr_reader is setting up 'getter' methods for all of the attributes present on initialization.
# This allows you to call Animal.name, Animal.species, Animal.diet, etc to retrieve information about the
# animal, but does NOT set up 'setter' methods that would allow you to set attributes by doing something like
# Animal.name = "Carl". We do this because once the animal is created, we simply need to be able to retrieve information as
# this is cleaner OO, and amendments should be passed in as a chunk of data to be processed if this were a specifically clean implementation of OO.
def initialize(species, name, diet)
# This thing throws an error if the Animal class is provided with a value for 'diet' that doesn't meet the level
# of logical abstraction we have established to reach our goal of having an animal that has dietary contraints.
if ![:omnivore, :herbivore, :carnivore].any?(diet)
raise ArgumentError, "Please provide a valid diet type for your Animal: i.e. :omnivore, :herbivore, :carnivore"
end
# Setting up internal data representation and instance-based variable persistence...
@species = species
@name = name
@diet = diet
@hunger = 0
@energy = 10
@muscle = 10
@fat = 10
end
# This is a simple utility method to grab the relevant athletic stats to be returned from methods that amend these attriutes directly
# and need to be grouped as returned as such for things like control flow, validation, and whatever else we might need the info for
# so we don't have to keep constructing it in other methods. (See #eat and #process_nutirition for examples of using it).
def stats
return {
energy: @energy,
muscle: @muscle,
fat: @fat
}
end
# How the animal eats. Notice that I am checking here wether or not the animal can eat the
# food at all before even attempting to process the nutrition, as there's no reason to run code or make checks against anything if
# the initial input is invalid to begin with.
# I also made the animal lose energy out of 'annoyance' if they can't eat the food as this is how I wanted to model the reality that these animals exist in.
def eat(food)
if can_eat.include?(food.type)
p "#{@name} ate the #{food.name}!"
process_nutrition!(food.nutrition)
return stats
else
p "#{@name} turns away from the #{food.name}!"
@energy -= 1
return stats
end
end
# This is encapsulted here as it may be that we want to add nutrition from other items like vitamins, supplements, shots, IV fluid etc. at a later date.
# This is quite literally how the animal's metabolic system is simulated to work, and the values I am dividing by could just as easily
# we replaced by metabolite values instead of static intergers. I am keeping it encapsulated because it is, in fact, a different set of logic and
# actions than simply having the ability to 'eat' a certain food, which could have its own levels of abstraction and ways of doing things
# independent of how the animal processes the nutrition from the food itself.
def process_nutrition!(nutrition)
@energy += ((nutrition[:carbs] / 5) + (nutrition[:sugar] / 3) + (nutrition[:fat] / 12) + (nutrition[:protien] / 2)) / 2
@hunger -= nutrition.map {|k,v| v }.inject(:+)
@muscle += nutrition[:protien] / 5
@fat += nutrition[:fat] / 3
return stats
end
# This is simply set up as a reference to ensure that if an animal has a certain diet, they can eat certain foods.
# I check against this with .include? (see #eat) as it's meant to represent a set of valid data for the Animal to work against.
def can_eat
if @diet == :carnivore
return [:meat]
elsif @diet == :herbivore
return [:vegetable, :fruit, :starch, :seed]
elsif @diet == :omnivore
return [:vegetable, :fruit, :starch, :seed, :meat]
end
end
end
# The food class is fairly similar, but is designed to be consumed by another class or potentially amended by
# things such as GMO engineering, age, or other environmental factors. This was a specific design decision, as
# I figured it was more reasonable to be able to change the property of a food than an animal for what I was trying to
# accomplish.
class Food
# Getter method setup.
attr_reader :name
# Getter & Setter method setup.
attr_accessor :sugar, :fat, :protien, :carbs, :type
# I am passing in a default hash here to ensure that, at the very least, food will have a 0
# value for all of its attributes. This eliminates the potential for nil errors and will make it so that
# non-muliplication math logic will maintain identity.
def initialize(name, type, nutrition = {sugar: 0, fat: 0, protien: 0, carbs: 0})
@sugar = nutrition[:sugar]
@fat = nutrition[:fat]
@protien = nutrition[:protien]
@carbs = nutrition[:carbs]
@name = name
@type = type # i.e. :meat, :vegetable, :fruit, :starch, :seed
end
# Just a simple utility method like Animal.stats. This is atually what the animal is asking for
# when it needs to process the nutrition gained from the food. We keep it here as there may be other modifiers not specific to the
# animal that the food needs to amend on itself. Eventually, more transformations in the data may happen here,
# so its a best to keep this encapsulated as we don't know what we are going to need to make that mechanic work later in development.
def nutrition
return {
sugar: @sugar,
fat: @fat,
protien: @protien,
carbs: @carbs
}
end
end
# We are just making a bat and feeding it some fruit here. Run the code and see how it works!
bat = Animal.new('fruit_bat', "Batty the Fruit Bat", :herbivore)
apple = Food.new('apple', :fruit, {
sugar: 80,
fat: 10,
protien: 5,
carbs: 40
})
guava = Food.new('guava', :fruit, {
sugar: 50,
fat: 20,
protien: 30,
carbs: 10
})
p bat.eat(apple)
p bat.eat(guava)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment