40
loading...
This website collects cookies to deliver better user experience
stack = []
# add to top of stack
stack.push(1)
# => [1]
# get top value from stack
stack.pop
# => 1
# ...and the stack is empty
stack
# => []
# add to bottom to stack!
stack.unshift(2)
class Stack
def initialize
@stack = []
end
def push(value)
@stack.push(value)
end
def pop
@stack.pop
end
end
class SymbolStack
def initialize
@stack = []
end
def push(sym)
unless sym.is_a?(Symbol)
raise TypeError, "can only push symbols onto stack"
end
@stack.push([sym, clock_time])
end
def pop
sym, pushed_at = @stack.pop
[sym, clock_time - pushed_at]
end
private def clock_time
Process.clock_gettime(Process::CLOCK_MONOTONIC)
end
end
Symbol
typesprivate
to hide the internal details of clock_time
, as callers of the class don't need to worry about that.String
class as the first example:str = String.new
str << "test" << "ing...1...2"
name = ARGV[1]
.to_s
.gsub('cool', 'amazing')
.capitalize
str << ". Found: " << name
puts str
builder = TextBuilder.new
builder.append("test")
builder.append("ing...1...2")
modifier = TextModifier.new
name = modifier.gsub(ARGV[1].to_s, 'cool', 'amazing')
name = modifier.capitalize(name)
builder.append(". Found: ")
builder.append(name)
puts builder.as_string
String
in Ruby is exceptionally flexible, and in the above case it certainly pares things down, but at what cost? If you did break things into another focused class would it be used more than once, or only in this one spot? The book discourages breaking it out unless it gets a lot of use, and I would be inclined to agree.You might notice I prefer prefix-dot rather than postfix-dot for line-breaked methods like:
something.
other_method.
another
# versus
something
.other_method
.another
Why? Better diffs, harder to miss dots, and Josh Cheek did a phenomenal job explaining for the rest of the reasons.
That said, that same logic could be used for Haskell style commas and that still feels odd to me:
# Usual way
h = {
a: 1,
b: 2,
c: 3
}
# Haskell-ish way
h = {a: 1
,b: 2
,c: 3}
...which still feels off. Anyways, point being preferences aren't exactly consistent all the time either, mine certainly aren't.
ReportContent
and ReportFormatter
potentially:# Perhaps start with a single report type that does it all
report = Report.new(data)
puts report.format
# But later we may need several format types:
report_content = ReportContent.new(data)
report_formatter = ReportFormatter.new
puts report_formatter.format(report_content)
report_content = ReportContent.new(data)
report_formatter = ReportFormatter
.for_type(report_type)
.new
puts report_formatter.format(report_content)
prepend
, which changes the order of the call-chain and makes reasoning about the object model much more complex. Granted I think this was useful as alias_method_chain
was doing some equal if not far more complicated things to the object chain.class OpenClosed
# Be careful, `methods` is a real method
def self.meths(m)
m.instance_methods + m.private_instance_methods
end
# Overriding any inclusion that adds methods
def self.include(*mods)
mods.each do |mod|
unless (meths(mod) & meths(self)).empty?
raise "class closed for modification"
end
end
super
end
singleton_class.alias_method :prepend, :include
# Extend acts different so it needs to be overridden
# for singleton_class rather than self.
def self.extend(*mods)
mods.each do |mod|
unless (meths(mod) & meths(singleton_class)).empty?
raise "class closed for modification"
end
end
super
end
end
meths(self).each do |method|
alias_name = :"__#{method}"
alias_method alias_name, method
end
method_added
which catches all new method definitions to undo overwriting:check_method = true
define_singleton_method(:method_added) do |method|
return unless check_method
if method.start_with?('__')
unaliased_name = method[2..-1]
# Normally I avoid parens, but it makes it clearer what's
# the condition and what's the body in cases like this.
if (
private_method_defined?(unaliased_name) ||
method_defined?(unaliased_name)
)
check_method = false
alias_method method, unaliased_name
check_method = true
raise "class closed for modification"
end
else
alias_name = :"__#{method}"
if (
private_method_defined?(alias_name) ||
method_defined?(alias_name)
)
check_method = false
alias_method method, alias_name
check_method = true
raise "class closed for modification"
end
end
end
end
class Max
def initialize(max)
@max = max
end
def over?(n) = @max > 5
end
class MaxBy < Max
def over?(n, by: 0) = @max > n + by
end
by
allows it to be used in place of Max
.instance_of?
will break for subclasses:if obj.instance_of?(Max) # MaxBy won't work here
# do something
else
# do something else
end
obj.class == Max
kind_of?
instead:if obj.kind_of?(Max)
# do something
else
# do something else
end
===
.class CurrentDay
def initialize
@date = Date.today
@schedule = MonthlySchedule.new(
@date.year,
@date.month
)
end
def work_hours = @schedule.work_hours_for(@date)
def workday? = !@schedule.holidays.include?(@date)
end
before do
Date.singleton_class.class_eval do
alias_method :_today, :today
define_method(:today) { Date.new(2020, 12, 16) }
end
end
after do
Date.singleton_class.class_eval do
alias_method :today, :_today
remove_method :_today
end
end
class CurrentDay
def initialize(date: Date.today)
@date = date
@schedule = MonthlySchedule.new(date.year, date.month)
end
end
schedule
to be injected:class CurrentDay
def initialize(
date: Date.today,
schedule: MonthlySchedule.new(date.year, date.month)
)
@date = date
@schedule = schedule
end
end
class CurrentDay
def initialize(
date: Date.today,
schedule_class: MonthlySchedule
)
@date = date
@schedule = schedule_class.new(date.year, date.month)
end
end
require 'cgi/escape'
class HTMLTable
def initialize(rows)
@rows = rows
end
def to_s
html = String.new
html << "<table><tbody>"
@rows.each do |row|
html << "<tr>"
row.each do |cell|
html << "<td>" << CGI.escapeHTML(cell.to_s) << "</td>"
end
html << "</tr>"
end
html << "</tbody></table>"
end
end
class HTMLTable
class Element
def self.set_type(type)
define_method(:type) { type }
end
def initialize(data)
@data = data
end
def to_s
"<#{type}>#{@data}</#{type}>"
end
end
%i(table tbody tr td).each do |type|
klass = Class.new(Element)
klass.set_type(type)
const_set(type.capitalize, klass)
end
end
to_s
method instead:def to_s
Table.new(
Tbody.new(
@rows.map do |row|
Tr.new(
row.map do |cell|
Td.new(CGI.escapeHTML(cell.to_s))
end.join
)
end.join
)
).to_s
end
Element
is certainly only doing one thing the book mentions that they're also pretty similar.class HTMLTable
def wrap(html, type)
html << "<" << type << ">"
yield
html << "</" << type << ">"
end
def to_s
html = String.new
wrap(html, 'table') do
wrap(html, 'tbody') do
@rows.each do |row|
wrap(html, 'tr') do
row.each do |cell|
wrap(html, 'td') do
html << CGI.escapeHTML(cell.to_s)
end
end
end
end
end
end
end
end
String
I'm not quite sure I like it for the look, but I'm also a bit frontend-oriented in some cases.HTML.generate do
strong 'test'
br
ul do
li 'a'
li 'b'
end
table do
thead do
th 'Name'
th 'Age'
end
tbody do
tr color: '#FF0' do
td 'Brandon'
td 30
end
tr color: 'yellow' do
td 'Alice'
td 42
end
end
end
end
TSort
in Ruby? Well it underpins the entire dependency chain resolution for Bundler and a lot of other things you're probably using, but chances are you'll rarely if ever use it directly.%i"table tbody tr td"
may be hard to read when compared to %i(table tbody tr td)
which makes it more distinct as a collection, but that's preference. I was having a lot of React flashbacks reading through that area, which definitely biases me towards certain design considerations, so take things with a grain of salt there.