ohm 1.3.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +14 -6
- data/.gems +4 -0
- data/.gitignore +4 -0
- data/CHANGELOG +52 -0
- data/benchmarks/common.rb +33 -0
- data/benchmarks/create.rb +21 -0
- data/benchmarks/delete.rb +13 -0
- data/examples/activity-feed.rb +162 -0
- data/examples/chaining.rb +203 -0
- data/examples/json-hash.rb +102 -0
- data/examples/one-to-many.rb +124 -0
- data/examples/philosophy.rb +149 -0
- data/examples/redis-logging.txt +179 -0
- data/examples/slug.rb +149 -0
- data/examples/tagging.rb +234 -0
- data/lib/ohm.rb +38 -4
- data/ohm.gemspec +18 -0
- data/test/model.rb +2 -2
- metadata +30 -15
- data/test/ranks.rb +0 -21
- data/test/setup.rb +0 -48
checksums.yaml
CHANGED
@@ -1,7 +1,15 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
ZDVlZWJhMjlkNWUzM2Y2NzY3ODVmN2E3YmIxYjQ3ZmU3OGQ3N2JiOA==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
ZGMyODFlYTAwYzU0MzE4MTQ0Y2FhZTNlZTdhODU0YzgwYTgyZjA4Mw==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
NGY2YmU4YTE0NWQ3N2E4Y2QyYWViZDBiOWUwM2NmMTFhNjY5YjNmMTIxNmE3
|
10
|
+
OTRhNmJiMjQ2ZjcwNDQwMjU4YTQ3ZWRjZmJjMmZlOTIxMTY3OTA5ZGJkNDYy
|
11
|
+
Yjg0ZDllODdkNjM4NGFmNWM4YzY5MTQ0ODBlYTNkYjhkODhmOGE=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
ZTJiYWNjNGUxZTk1ZTUxNTQ5Nzk2NTZmOGQ1YTAzY2QzY2Y2NDk4MGE5MDU3
|
14
|
+
Zjk1ODY3MThlYTdmOGZhZWFiY2QyNzg1MTg2YjA5NjFlODE4NDNjNmE4N2Mx
|
15
|
+
MmJlNjg3ZjViYzc4NmU5ZGNlNmU1MmEwMmNkMDBhOTg1ZDUyZjk=
|
data/.gems
ADDED
data/.gitignore
ADDED
data/CHANGELOG
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
1.3.1
|
2
|
+
|
3
|
+
- Improve memory consumption when indexing persisted attributes.
|
4
|
+
|
5
|
+
No migration is needed and old indices will be cleaned up as you save
|
6
|
+
instances.
|
7
|
+
|
8
|
+
1.3.0
|
9
|
+
|
10
|
+
- Add Model.attributes.
|
11
|
+
|
12
|
+
1.2.0
|
13
|
+
|
14
|
+
- Enumerable fix.
|
15
|
+
- Merge Ohm::PipelinedFetch into Ohm::Collection.
|
16
|
+
- Fix Set, MultiSet, and List enumerable behavior.
|
17
|
+
- Change dependencies to use latest cutest.
|
18
|
+
|
19
|
+
1.1.0
|
20
|
+
|
21
|
+
- Compatible with redis-rb 3.
|
22
|
+
|
23
|
+
1.0.0
|
24
|
+
|
25
|
+
- Fetching a batch of objects is now done through one pipeline, effectively
|
26
|
+
reducing the IO to just 2 operations (one for SMEMBERS / LRANGE, one for
|
27
|
+
the actual HGET of all the individual HASHes.)
|
28
|
+
- write_remote / read_remote have been replaced with set / get respectively.
|
29
|
+
- Ohm::Model.unique has been added.
|
30
|
+
- Ohm::Model::Set has been renamed to Ohm::Set
|
31
|
+
- Ohm::Model::List has been renamed to Ohm::List
|
32
|
+
- Ohm::Model::Collection is gone.
|
33
|
+
- Ohm::Validations is gone. Ohm now uses Scrivener::Validations.
|
34
|
+
- Ohm::Key is gone. Ohm now uses Nest directly.
|
35
|
+
- No more concept of volatile keys.
|
36
|
+
- Ohm::Model::Wrapper is gone.
|
37
|
+
- Use Symbols for constants instead of relying on Ohm::Model.const_missing.
|
38
|
+
- #sort / #sort_by now uses `limit` as it's used in redis-rb, e.g. you
|
39
|
+
have to pass in an array like so: sort(limit: [0, 1]).
|
40
|
+
- Set / List have been trimmed to contain only the minimum number
|
41
|
+
of necessary methods.
|
42
|
+
- You can no longer mutate a collection / set as before, e.g. doing
|
43
|
+
User.find(...).add(User[1]) will throw an error.
|
44
|
+
- The #union operation has been added. You can now chain it with your filters.
|
45
|
+
- Temporary keys when doing finds are now automatically cleaned up.
|
46
|
+
- Counters are now stored in their own key instead, i.e. in
|
47
|
+
User:<id>:counters.
|
48
|
+
- JSON support has to be explicitly required by doing `require
|
49
|
+
"ohm/json"`.
|
50
|
+
- All save / delete / update operations are now done using
|
51
|
+
transactions (see http://redis.io/topics/transactions).
|
52
|
+
- All indices are now stored without converting the values to base64.
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "bench"
|
2
|
+
require_relative "../lib/ohm"
|
3
|
+
|
4
|
+
Ohm.connect(:port => 6379, :db => 15)
|
5
|
+
Ohm.flush
|
6
|
+
|
7
|
+
class Event < Ohm::Model
|
8
|
+
attribute :name
|
9
|
+
attribute :location
|
10
|
+
|
11
|
+
index :name
|
12
|
+
index :location
|
13
|
+
|
14
|
+
def validate
|
15
|
+
assert_present :name
|
16
|
+
assert_present :location
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Sequence
|
21
|
+
def initialize
|
22
|
+
@value = 0
|
23
|
+
end
|
24
|
+
|
25
|
+
def succ!
|
26
|
+
Thread.exclusive { @value += 1 }
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.[](name)
|
30
|
+
@@sequences ||= Hash.new { |hash, key| hash[key] = Sequence.new }
|
31
|
+
@@sequences[name]
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require_relative "common"
|
2
|
+
|
3
|
+
benchmark "Create Events" do
|
4
|
+
i = Sequence[:events].succ!
|
5
|
+
|
6
|
+
Event.create(:name => "Redis Meetup #{i}", :location => "London #{i}")
|
7
|
+
end
|
8
|
+
|
9
|
+
benchmark "Find by indexed attribute" do
|
10
|
+
Event.find(:name => "Redis Meetup 1").first
|
11
|
+
end
|
12
|
+
|
13
|
+
benchmark "Mass update" do
|
14
|
+
Event[1].update(:name => "Redis Meetup II")
|
15
|
+
end
|
16
|
+
|
17
|
+
benchmark "Load events" do
|
18
|
+
Event[1].name
|
19
|
+
end
|
20
|
+
|
21
|
+
run 5000
|
@@ -0,0 +1,162 @@
|
|
1
|
+
### Building an activity feed
|
2
|
+
|
3
|
+
#### Common solutions using a relational design
|
4
|
+
|
5
|
+
# When faced with this application requirement, the most common approach by
|
6
|
+
# far have been to create an *activities* table, and rows in this table would
|
7
|
+
# reference a *user*. Activities would typically be generated for each
|
8
|
+
# follower (or friend) when a certain user performs an action, like posting a
|
9
|
+
# new status update.
|
10
|
+
|
11
|
+
#### Problems
|
12
|
+
|
13
|
+
# The biggest issue with this design, is that the *activities* table will
|
14
|
+
# quickly get very huge, at which point you would need to shard it on
|
15
|
+
# *user_id*. Also, inserting thousands of entries per second would quickly
|
16
|
+
# bring your database to its knees.
|
17
|
+
|
18
|
+
#### Ohm Solution
|
19
|
+
|
20
|
+
# As always we need to require `Ohm`.
|
21
|
+
require "ohm"
|
22
|
+
|
23
|
+
# We create a `User` class, with a `set` for all the other users he
|
24
|
+
# would be `following`, and another `set` for all his `followers`.
|
25
|
+
class User < Ohm::Model
|
26
|
+
set :followers, User
|
27
|
+
set :following, User
|
28
|
+
|
29
|
+
# Because a `User` literally has a `list` of activities, using a Redis
|
30
|
+
# `list` to model the activities would be a good choice. We default to
|
31
|
+
# getting the first 100 activities, and use
|
32
|
+
# [lrange](http://code.google.com/p/redis/wiki/LrangeCommand) directly.
|
33
|
+
def activities(start = 0, limit = 100)
|
34
|
+
key[:activities].lrange(start, start + limit)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Broadcasting a message to all the `followers` of a user would simply
|
38
|
+
# be prepending the message for each if his `followers`. We also use
|
39
|
+
# the Redis command
|
40
|
+
# [lpush](http://code.google.com/p/redis/wiki/RpushCommand) directly.
|
41
|
+
def broadcast(str)
|
42
|
+
followers.each do |user|
|
43
|
+
user.key[:activities].lpush(str)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Given that *Jane* wants to follow *John*, we simply do the following
|
48
|
+
# steps:
|
49
|
+
#
|
50
|
+
# 1. *John* is added to *Jane*'s `following` list.
|
51
|
+
# 2. *Jane* is added to *John*'s `followers` list.
|
52
|
+
def follow(other)
|
53
|
+
following << other
|
54
|
+
other.followers << self
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
#### Testing
|
60
|
+
|
61
|
+
# We'll use cutest for our testing framework.
|
62
|
+
require "cutest"
|
63
|
+
|
64
|
+
# The database is flushed before each test.
|
65
|
+
prepare { Ohm.flush }
|
66
|
+
|
67
|
+
# We define two users, `john` and `jane`, and yield them so all
|
68
|
+
# other tests are given access to these 2 users.
|
69
|
+
setup do
|
70
|
+
john = User.create
|
71
|
+
jane = User.create
|
72
|
+
|
73
|
+
[john, jane]
|
74
|
+
end
|
75
|
+
|
76
|
+
# Let's verify our model for `follow`. When `jane` follows `john`,
|
77
|
+
# the following conditions should hold:
|
78
|
+
#
|
79
|
+
# 1. The followers list of `john` is comprised *only* of `jane`.
|
80
|
+
# 2. The list of users `jane` is following is comprised *only* of `john`.
|
81
|
+
test "jane following john" do |john, jane|
|
82
|
+
jane.follow(john)
|
83
|
+
|
84
|
+
assert [john] == jane.following.to_a
|
85
|
+
assert [jane] == john.followers.to_a
|
86
|
+
end
|
87
|
+
|
88
|
+
# Broadcasting a message should simply notify all the followers of the
|
89
|
+
# `broadcaster`.
|
90
|
+
test "john broadcasting a message" do |john, jane|
|
91
|
+
jane.follow(john)
|
92
|
+
john.broadcast("Learning about Redis and Ohm")
|
93
|
+
|
94
|
+
assert jane.activities.include?("Learning about Redis and Ohm")
|
95
|
+
end
|
96
|
+
|
97
|
+
#### Total Denormalization: Adding HTML
|
98
|
+
|
99
|
+
# This may be a real edge case design decision, but for some scenarios this
|
100
|
+
# may work. The beauty of this solution is that you only have to generate the
|
101
|
+
# output once, and successive refreshes of the end user will help you save
|
102
|
+
# some CPU cycles.
|
103
|
+
#
|
104
|
+
# This example of course assumes that the code that generates this does all
|
105
|
+
# the conditional checks (possibly changing the point of view like *Me:*
|
106
|
+
# instead of *John says:*).
|
107
|
+
test "broadcasting the html directly" do |john, jane|
|
108
|
+
jane.follow(john)
|
109
|
+
|
110
|
+
snippet = '<a href="/1">John</a> says: How\'s it going ' +
|
111
|
+
'<a href="/user/2">jane</a>?'
|
112
|
+
|
113
|
+
john.broadcast(snippet)
|
114
|
+
|
115
|
+
assert jane.activities.include?(snippet)
|
116
|
+
end
|
117
|
+
|
118
|
+
#### Saving Space
|
119
|
+
|
120
|
+
# In most cases, users don't really care about keeping their entire activity
|
121
|
+
# history. This application requirement would be fairly trivial to implement.
|
122
|
+
|
123
|
+
# Let's reopen our `User` class and define a new broadcast method.
|
124
|
+
class User
|
125
|
+
# We define a constant where we set the maximum number of activity entries.
|
126
|
+
MAX = 10
|
127
|
+
|
128
|
+
# Using `MAX` as the reference, we check if the number of activities exceeds
|
129
|
+
# `MAX`, and use
|
130
|
+
# [ltrim](http://code.google.com/p/redis/wiki/LtrimCommand) to truncate
|
131
|
+
# the activities.
|
132
|
+
def broadcast(str)
|
133
|
+
followers.each do |user|
|
134
|
+
user.key[:activities].lpush(str)
|
135
|
+
|
136
|
+
if user.key[:activities].llen > MAX
|
137
|
+
user.key[:activities].ltrim(0, MAX - 1)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Now let's verify that this new behavior is enforced.
|
144
|
+
test "pushing 11 activities maintains the list to 10" do |john, jane|
|
145
|
+
jane.follow(john)
|
146
|
+
|
147
|
+
11.times { john.broadcast("Flooding your feed!") }
|
148
|
+
|
149
|
+
assert 10 == jane.activities.size
|
150
|
+
end
|
151
|
+
|
152
|
+
|
153
|
+
#### Conclusion
|
154
|
+
|
155
|
+
# As you can see, choosing a more straightforward approach (in this case,
|
156
|
+
# actually having a list per user, instead of maintaining a separate
|
157
|
+
# `activities` table) will greatly simplify the design of your system.
|
158
|
+
#
|
159
|
+
# As a final note, keep in mind that the Ohm solution would still need
|
160
|
+
# sharding for large datasets, but that would be again trivial to implement
|
161
|
+
# using [redis-rb](http://github.com/ezmobius/redis-rb)'s distributed support
|
162
|
+
# and sharding it against the *user_id*.
|
@@ -0,0 +1,203 @@
|
|
1
|
+
### Chaining Ohm Sets
|
2
|
+
|
3
|
+
#### Doing the straight forward approach
|
4
|
+
|
5
|
+
# Let's design our example around the following requirements:
|
6
|
+
#
|
7
|
+
# 1. a `User` has many orders.
|
8
|
+
# 2. an `Order` can be pending, authorized or captured.
|
9
|
+
# 3. a `Product` is referenced by an `Order`.
|
10
|
+
|
11
|
+
#### Doing it the normal way
|
12
|
+
|
13
|
+
# Let's first require `Ohm`.
|
14
|
+
require "ohm"
|
15
|
+
|
16
|
+
# A `User` has a `collection` of *orders*. Note that a collection
|
17
|
+
# is actually just a convenience, which implemented simply will look like:
|
18
|
+
#
|
19
|
+
# def orders
|
20
|
+
# Order.find(:user_id => self.id)
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
class User < Ohm::Model
|
24
|
+
collection :orders, Order
|
25
|
+
end
|
26
|
+
|
27
|
+
# The product for our purposes will only contain a name.
|
28
|
+
class Product < Ohm::Model
|
29
|
+
attribute :name
|
30
|
+
end
|
31
|
+
|
32
|
+
# We define an `Order` with just a single `attribute` called state, and
|
33
|
+
# also add an `index` so we can search an order given its state.
|
34
|
+
#
|
35
|
+
# The `reference` to the `User` is actually required for the `collection`
|
36
|
+
# of *orders* in the `User` declared above, because the `reference` defines
|
37
|
+
# an index called `:user_id`.
|
38
|
+
#
|
39
|
+
# We also define a `reference` to a `Product`.
|
40
|
+
class Order < Ohm::Model
|
41
|
+
attribute :state
|
42
|
+
index :state
|
43
|
+
|
44
|
+
reference :user, User
|
45
|
+
reference :product, Product
|
46
|
+
end
|
47
|
+
|
48
|
+
##### Testing what we have so far.
|
49
|
+
|
50
|
+
# For the purposes of this tutorial, we'll use cutest for our test framework.
|
51
|
+
require "cutest"
|
52
|
+
|
53
|
+
# Make sure that every run of our test suite has a clean Redis instance.
|
54
|
+
prepare { Ohm.flush }
|
55
|
+
|
56
|
+
# Let's create a *user*, a *pending*, *authorized* and a captured order.
|
57
|
+
# We also create two products named *iPod* and *iPad*.
|
58
|
+
setup do
|
59
|
+
@user = User.create
|
60
|
+
|
61
|
+
@ipod = Product.create(:name => "iPod")
|
62
|
+
@ipad = Product.create(:name => "iPad")
|
63
|
+
|
64
|
+
@pending = Order.create(:user => @user, :state => "pending",
|
65
|
+
:product => @ipod)
|
66
|
+
@authorized = Order.create(:user => @user, :state => "authorized",
|
67
|
+
:product => @ipad)
|
68
|
+
@captured = Order.create(:user => @user, :state => "captured",
|
69
|
+
:product => @ipad)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Now let's try and grab all pending orders, and also pending
|
73
|
+
# *iPad* and *iPod* ones.
|
74
|
+
test "finding pending orders" do
|
75
|
+
assert @user.orders.find(state: "pending").include?(@pending)
|
76
|
+
|
77
|
+
assert @user.orders.find(:state => "pending",
|
78
|
+
:product_id => @ipod.id).include?(@pending)
|
79
|
+
|
80
|
+
assert @user.orders.find(:state => "pending",
|
81
|
+
:product_id => @ipad.id).empty?
|
82
|
+
end
|
83
|
+
|
84
|
+
# Now we try and find captured and authorized orders. The tricky part
|
85
|
+
# is trying to find an order that is either *captured* or *authorized*,
|
86
|
+
# since `Ohm` as of this writing doesn't support unions in its
|
87
|
+
# finder syntax.
|
88
|
+
test "finding authorized and/or captured orders" do
|
89
|
+
assert @user.orders.find(:state => "authorized").include?(@authorized)
|
90
|
+
assert @user.orders.find(:state => "captured").include?(@captured)
|
91
|
+
|
92
|
+
assert @user.orders.find(:state => ["authorized", "captured"]).empty?
|
93
|
+
|
94
|
+
auth_or_capt = @user.orders.key.volatile[:auth_or_capt]
|
95
|
+
auth_or_capt.sunionstore(
|
96
|
+
@user.orders.find(:state => "authorized").key,
|
97
|
+
@user.orders.find(:state => "captured").key
|
98
|
+
)
|
99
|
+
|
100
|
+
assert auth_or_capt.smembers.include?(@authorized.id)
|
101
|
+
assert auth_or_capt.smembers.include?(@captured.id)
|
102
|
+
end
|
103
|
+
|
104
|
+
#### Creating shortcuts
|
105
|
+
|
106
|
+
# You can of course define methods to make that code more readable.
|
107
|
+
class User < Ohm::Model
|
108
|
+
def authorized_orders
|
109
|
+
orders.find(:state => "authorized")
|
110
|
+
end
|
111
|
+
|
112
|
+
def captured_orders
|
113
|
+
orders.find(:state => "captured")
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# And we can now test these new methods.
|
118
|
+
test "finding authorized and/or captured orders" do
|
119
|
+
assert @user.authorized_orders.include?(@authorized)
|
120
|
+
assert @user.captured_orders.include?(@captured)
|
121
|
+
end
|
122
|
+
|
123
|
+
# In most cases this is fine, but if you want to have a little fun,
|
124
|
+
# then we can play around with some chainability.
|
125
|
+
|
126
|
+
#### Chaining Kung-Fu
|
127
|
+
|
128
|
+
# The `Ohm::Model::Set` takes a *Redis* key and a *class monad*
|
129
|
+
# for its arguments.
|
130
|
+
#
|
131
|
+
# We can simply subclass it and define the monad to always be an
|
132
|
+
# `Order` so we don't have to manually set it everytime.
|
133
|
+
class UserOrders < Ohm::Model::Set
|
134
|
+
def initialize(key)
|
135
|
+
super key, Ohm::Model::Wrapper.wrap(Order)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Here is the crux of the chaining pattern. Instead of
|
139
|
+
# just doing a straight up `find(:state => "pending")`, we return
|
140
|
+
# `UserOrders` again.
|
141
|
+
def pending
|
142
|
+
self.class.new(model.index_key_for(:state, "pending"))
|
143
|
+
end
|
144
|
+
|
145
|
+
def authorized
|
146
|
+
self.class.new(model.index_key_for(:state, "authorized"))
|
147
|
+
end
|
148
|
+
|
149
|
+
def captured
|
150
|
+
self.class.new(model.index_key_for(:state, "captured"))
|
151
|
+
end
|
152
|
+
|
153
|
+
# Now we wrap the implementation of doing an `SUNIONSTORE` and also
|
154
|
+
# make it return a `UserOrders` object.
|
155
|
+
#
|
156
|
+
# NOTE: `volatile` just returns the key prepended with a `~:`, so in
|
157
|
+
# this case it would be `~:Order:accepted`.
|
158
|
+
def accepted
|
159
|
+
model.key.volatile[:accepted].sunionstore(
|
160
|
+
authorized.key, captured.key
|
161
|
+
)
|
162
|
+
|
163
|
+
self.class.new(model.key.volatile[:accepted])
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Now let's re-open the `User` class and add a customized `orders` method.
|
168
|
+
class User < Ohm::Model
|
169
|
+
def orders
|
170
|
+
UserOrders.new(Order.index_key_for(:user_id, id))
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# Ok! Let's put all of that chaining code to good use.
|
175
|
+
test "finding pending orders using a chainable style" do
|
176
|
+
assert @user.orders.pending.include?(@pending)
|
177
|
+
assert @user.orders.pending.find(:product_id => @ipod.id).include?(@pending)
|
178
|
+
|
179
|
+
assert @user.orders.pending.find(:product_id => @ipad.id).empty?
|
180
|
+
end
|
181
|
+
|
182
|
+
test "finding authorized and/or captured orders using a chainable style" do
|
183
|
+
assert @user.orders.authorized.include?(@authorized)
|
184
|
+
assert @user.orders.captured.include?(@captured)
|
185
|
+
|
186
|
+
assert @user.orders.accepted.include?(@authorized)
|
187
|
+
assert @user.orders.accepted.include?(@captured)
|
188
|
+
|
189
|
+
accepted = @user.orders.accepted
|
190
|
+
|
191
|
+
assert accepted.find(:product_id => @ipad.id).include?(@authorized)
|
192
|
+
assert accepted.find(:product_id => @ipad.id).include?(@captured)
|
193
|
+
end
|
194
|
+
|
195
|
+
#### Conclusion
|
196
|
+
|
197
|
+
# This design pattern is something that really depends upon the situation. In
|
198
|
+
# the example above, you can add more complicated querying on the `UserOrders`
|
199
|
+
# class.
|
200
|
+
#
|
201
|
+
# The most important takeaway here is the ease of which we can weild the
|
202
|
+
# different components of Ohm, and mold it accordingly to our preferences,
|
203
|
+
# without having to monkey-patch anything.
|