As in all object-oriented languages and much the same as in Ruby, Crystal provides for single inheritance, indicated by: subclass < superclass. Putting properties and methods common to several classes into a superclass lets them all share functionality. That way, you can use all instance variables and all methods of the superclass in the subclass, including the constructors. You can see this in the following example where PDFDocument inherits initialize, name, and print from Document:
| class Document |
| property name |
| |
| def initialize(@name : String) |
| end |
| |
| def print |
| puts "Hi, I'm printing #{@name}" |
| end |
| end |
| |
| class PDFDocument < Document |
| end |
| |
| doc = PDFDocument.new("Salary Report Q4 2018") |
| doc.print # => Hi, I'm printing Salary Report Q4 2018 |
You can also override any inherited method in the subclass. If the subclass defines its own initialize method(s), they aren’t inherited anymore. If you want to use the superclass functionality after you overrode it, you can call any method of the superclass with super:
| class PDFDocument < Document |
| def initialize(@name : String, @company : String) |
| end |
| |
| def print |
| super |
| puts "From company #{@company}" |
| end |
| end |
| |
| # doc = PDFDocument.new("Salary Report Q4 2018") |
| # => Error: wrong number of arguments for 'PDFDocument.new' (given 1, |
| # => expected 2) |
| doc = PDFDocument.new("Salary Report Q4 2018", "ACME") |
| doc.print |
| |
| # => Hi, I'm printing Salary Report Q4 2018 |
| # From company ACME |
Crystal’s type system gives you more options here. Instead of overriding, you can define specialized methods by using type restrictions, such as print in PDFDocument:
| class PDFDocument < Document |
| def initialize(@name : String, @company : String) |
| end |
| |
| def print(date : Time) |
| puts "Printing #{@name}" |
| puts "From company #{@company} at date #{date}" |
| end |
| end |
| |
| doc = PDFDocument.new("Salary Report Q4 2018", "ACME") |
| doc.print(Time.now) |
| |
| # => Printing Salary Report Q4 2018 |
| # From company ACME at date 2017-05-25 12:12:45 +0200 |
Ruby doesn’t have native support for interfaces and abstract classes like Java or C#. In both Ruby and Crystal, the concept of an interface is implemented through modules, as you’ll see in the next chapter. But Crystal also knows the concept of an abstract class, so if you’re a Rubyist, the following will be new to you.
Not all classes are destined to produce objects, and abstract classes are a good example. These serve instead as a blueprint for subclasses to implement their methods. Here you see a class, Rect (describing rectangles), forced to implement all abstract methods from class Shape:
| abstract class Shape |
| abstract def area |
| abstract def perim |
| end |
| |
| class Rect < Shape |
| def initialize(@width : Int32, @height : Int32) |
| end |
| |
| def area |
| @width * @height |
| end |
| |
| def perim |
| 2 * (@width + @height) |
| end |
| end |
| |
| s = Shape.new # => can't instantiate abstract class Shape |
| Rect.new(3, 6).area # => 18 |
If one of the methods (say perim) isn’t implemented, the compiler issues an error like the following:
| error: "abstract `def Shape#perim()` must be implemented by Rect" |
This lets you create class hierarchies where you can be confident that all necessary methods are implemented.
You can create more intricate structures as well. In the following example, class Document is called a virtual type because it combines different types from the same type hierarchy—in this case, different documents:
| class Document |
| end |
| |
| class PDFDocument < Document |
| def print |
| puts "PDF header" |
| end |
| end |
| |
| class XMLDocument < Document |
| def print |
| puts "XML header" |
| end |
| end |
| class Report |
| getter doc |
| |
| def initialize(@name : String, @doc : Document) |
| end |
| end |
| |
| salq4 = Report.new "Salary Report Q4", PDFDocument.new |
| taxQ1 = Report.new "Tax Report Q1", XMLDocument.new |
This virtual type is indicated by the compiler as type Document+, meaning that all types inherit from Document, including Document itself. It comes into play in situations like the one that follows where you’d expect d to be of a union type (PDFDocument | XMLDocument):
| if 4 < 5 |
| d = PDFDocument.new |
| else |
| d = XMLDocument.new |
| end |
| typeof(d) # => Document |
Instead, d is of type Document. Internally the compiler uses this as a virtual type Document+ instead of the union type (PDFDocument | XMLDocument), because union types quickly become very complex in class-hierarchies.
If you call a method in a subclass of Document, you get an error:
| salq4.doc.print # => Error: undefined method 'print' for Document |
To remove this error, simply make the class Document abstract.
| abstract class Document |
| end |
| |
| salq4.doc.print # => PDF header |
➤ Shape: Subclass Shape with classes Square and Circle. (Hint: Use PI from the Math module with: include Math.)
3.141.7.240