To demonstrate Structs and Records - lets assume we have a User
class that can receive messages posted to it.
require "./message_record"
class User
private getter name : String, email : String
def initialize(@name, @email)
end
def to_s
"#{name} <#{email}>"
end
def post_message(message : Message)
puts message.to_s
end
end
1. Structs - Introduction
I like to think of these as flexible immutable data types - with very few if any methods.
Structs (& Records) are basically classes, but: * they are immutable (once created the values don’t change) * they are passed by value NOT by REFERENCE (this means the data is COPIED)
Good uses include: * pass immutable data between Fibers * build Value Objects (like representing money which has a value and a currency).
2. Structs
Structs are immutable classes focused on presenting / passing data.
2.1. Data Structs (no methods)
The simplest Data Struct contains a getter
and an initialize
method - as seen below:
struct Message
getter sender, receiver, topic, text
def initialize(@sender : User, @text : String, @receiver : User? = nil, @topic : String? = nil)
@text = text.strip
end
end
Below is an example of how the struct is used.
require "./user"
struct Message
getter sender, receiver, topic, text
def initialize(@sender : User, @text : String, @receiver : User? = nil, @topic : String? = nil)
@text = text.strip
end
end
user_1 = User.new(name: "user_1", email: "[email protected]")
user_2 = User.new(name: "user_2", email: "[email protected]")
mesg_1 = Message.new(sender: user_2, text: "Hi")
mesg_2 = Message.new(sender: user_1, text: "Hoi", topic: "Greet")
mesg_3 = Message.new(sender: user_1, text: "Hoi", topic: "Greet", receiver: user_2)
user_1.post_message(mesg_1)
user_2.post_message(mesg_2)
user_2.post_message(mesg_3)
# # UserUnpackPost: A User Decorator responsible to unpack User Messages Posted
# class UserUnpackPost
# def initialize(@user : User)
# end
#
# def post_message(message : Message)
# text = message.text
# topic = message.topic
# sender = message.sender
# receiver = message.receiver
# output = [] of String
# output << "-" * 30
# output << "From: #{sender.to_s}"
# output << "To: #{receiver.to_s}" unless receiver.nil?
# output << "Topic: #{topic.to_s.strip}" unless topic.nil?
# output << text.strip
# output << "-" * 30
# puts output.join("\n")
# end
# end
#
# UserUnpackPost.new(user_1).post_message(mesg_1)
# UserUnpackPost.new(user_2).post_message(mesg_2)
# UserUnpackPost.new(user_2).post_message(mesg_3)
Run this using:
$ crystal src/structs_n_records/message_struct_simple.cr
This works - but the messages print in a pretty unsatisfactory manner. It’s not great design if User class needs to know the internal structure of Message to print.
One solution would be use a Decorator/Wrapper class which knows the details of the Message internals. Uncomment the decorator code to see the effect (but ideally Message should not many ANY other part of code know its implementation details). The next section shows a much better approach.
2.2. Struct with Methods - (responsible for its own internal structure)
Data Structs work well for the purpose of passing immutable data as seen above, but it is also convenient if other consuming classes don’t need to know the internals of the struct.
To fix this structs
can also define methods — they should be at a minimum and should not attempt to mutate the state. Below is an example that adds the to_s
to allow the User
class to easily consume / print the Message (in a much cleaner way where Message class knows how to present its data).
require "./user"
struct Message
getter sender, receiver, topic, text
def initialize(@sender : User, @text : String, @receiver : User? = nil, @topic : String? = nil)
end
def to_s
output = [] of String
output << "-" * 30
output << "From: #{sender.to_s}"
output << "To: #{receiver.to_s}" unless receiver.nil?
output << "Topic: #{topic.to_s.strip}" unless topic.nil?
output << text.strip
output << "-" * 30
output.join("\n")
end
end
user_1 = User.new(name: "user_1", email: "[email protected]")
user_2 = User.new(name: "user_2", email: "[email protected]")
mesg_1 = Message.new(sender: user_2, text: "Hi")
mesg_2 = Message.new(sender: user_1, text: "Hoi", topic: "Greet")
mesg_3 = Message.new(sender: user_1, text: "Hoi", topic: "Greet", receiver: user_2)
user_1.post_message(mesg_1)
user_2.post_message(mesg_2)
user_2.post_message(mesg_3)
Run this using:
$ crystal src/structs_n_records/message_struct.cr
In this way we can add (possibly remove) data and features from the Message all transparently to the User