Organizing Code in Classes and Modules

Variables and methods are powerful, but they need to be organized into larger structures to be very useful. As Crystal is a truly object-oriented language, classes are the main tool for doing that. Classes let you define combinations of methods and associated data, which you can then turn into objects with new. When you’ve built classes that work with each other, you can organize them into larger modules. Basic Crystal classes look much like Ruby classes, but Crystal makes some changes.

Class Basics

Classes group publicly visible methods and properties, and can have additional methods and variables inside of them to make things work smoothly. Class names start with an uppercase letter, but the rest of the name is typically lowercase or CamelCase, to contrast with variable and method names. When you create a class, you’ve also defined a new type. This extremely simple example shows defining an empty class, creating an object from it, and checking the type of the object.

 class​ Mineral
 
 end
 mine = Mineral.​new​()
 puts typeof(mine) ​# => Mineral

It’s not much of an object, but it’s easy to add more. When you create a mineral, you should give it a common name and specify its hardness, something like mine = Mineral.new("talc", 1.0). (Hardness isn’t necessarily an integer, so this object shifted to floats.) In Crystal, as in Ruby, that means adding an initialize method that takes those arguments.

 class​ Mineral
 def​ ​initialize​(common_name : String, hardness : Float64)
  @common_name = common_name
  @hardness = hardness
 end
 end
 mine = Mineral.​new​(​"talc"​, 1.0)
 puts typeof(mine) ​# => Mineral

Because the common_name and hardness arguments aren’t yet used beyond simple assignment, the compiler lacks the information it needs to determine their type, and will complain if you don’t specify them. @common_name and @hardness are instance variables specific to the object created here.

Crystal can also save you some typing on the initialize method. If you use the instance variable names, the ones prefixed with @, as the names of the arguments, Crystal just puts the arguments into the instance variables.

 class​ Mineral
 def​ ​initialize​(@common_name : String, @hardness : Float64)
 end
 end
 mine = Mineral.​new​(​"talc"​, 1.0)
 puts typeof(mine) ​# => Mineral

But there’s currently no way to access those instance variables from outside of the object. If you ask for Mineral.common_name, for example, you’ll get undefined method ’common_name’ for Mineral.class. You could create a method called common_name= that returns the value of @common_name, but Crystal offers something easier: getters (and setters). To allow reading and manipulation of the common_name outside of the object, and reading of the hardness, you could write:

 class​ Mineral
  getter common_name : String
  setter common_name
  getter hardness : Float64
 
 def​ ​initialize​(common_name, hardness)
  @common_name = common_name
  @hardness = hardness
 end
 end
 mine = Mineral.​new​(​"talc"​, 1.0)
 puts mine.​common_name​ ​# => talc
 mine.​common_name​=​"gold"
 puts mine.​common_name​ ​# => gold
 puts mine.​hardness​ ​# => 1.0

The compiler can find type declarations you specify anywhere in there, but near the top of the class is easy for humans to find, so this set is on the getter. This version lets you read and change the name of the mineral, and read the hardness. If you try to set the hardness, you’ll still get an undefined method name error because there is no setter. (If you’re a Rubyist, you might have noticed that getter is equivalent to Ruby’s attr_reader, setter is equivalent to Ruby’s attr_writer, and property is equivalent to attr_accessor. They’re just more concise.)

Unlike variables, methods in Crystal classes are visible outside the object by default. They look like the methods you defined earlier but need to be referenced through an object or from inside it. A simple object method would look like:

 class​ Mineral
  getter common_name : String
  setter common_name
  getter hardness : Float64
  getter crystal_struct : String
 
 def​ ​initialize​(@common_name, @hardness, @crystal_struct)
 end
 
 def​ ​describe
 "This is ​​#{​common_name​}​​ with a Mohs hardness of ​​#{​hardness​}
 and a structure of ​​#{​crystal_struct​}​​."
 end
 end
 mine = Mineral.​new​(​"talc"​, 1.0, ​"monoclinic"​)
 puts mine.​describe​ ​# => This is talc with a Mohs hardness of 1.0
 # => and a structure of monoclinic.

To give describe something more to do, the example adds a crystal_struct variable. The describe method gathers the three variables of the object and presents them in a sentence. (It also demonstrates that strings can contain line breaks, something that can be useful or annoying depending on your context and preference.)

In Part II, Chapter 5, Using Classes and Structs, we’ll visit classes in depth and will discuss visibility, inheritance, and the class hierarchy.

Your Turn 6

a. Suppose you want to be able to make Mineral objects for minerals for which you don’t (yet) know the crystal structure. How could you do this? (Hint: Use a union type for the property.)

b. Add a to_s method that makes a String representation of a Mineral object, and use it in the output. (Hint: The current object is self.)

Making Modules

Modules group methods and classes that implement related functionality. For example, the module Random from the standard library contains methods for generating all sorts of random values. A class can include one or more modules—a so-called mixin. That way, the objects of the class can use the methods of the module. Let’s make a module called Hardness that contains a method, hardness, to return that value for a given mineral:

 module​ ​Hardness
 def​ ​data
  {​"talc"​ => 1, ​"calcite"​ => 3, ​"apatite"​ => 5, ​"corundum"​ => 9}
 end
 
 def​ ​hardness
  data[self.​name​]
 end
 end

In this example, our class Mineral now has only the name property, but it includes the module Hardness:

 class​ Mineral
 include​ Hardness
  getter name : String
 
 def​ ​initialize​(@name)
 end
 end

By including that module, you can invoke its methods on any Mineral object:

 min = Mineral.​new​(​"corundum"​)
 min.​hardness​ ​# => 9

A class can also extend a module, but then its methods are called on the class.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.103.204