interfaces 0.0.2.pre → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +18 -28
- data/lib/interfaces/castable.rb +8 -0
- data/lib/interfaces/version.rb +1 -1
- data/spec/castable_spec.rb +4 -0
- metadata +5 -6
data/README.md
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# Interfaces
|
2
2
|
|
3
|
-
TODO: Write a gem description
|
4
|
-
|
5
3
|
## Installation
|
6
4
|
|
7
5
|
Add this line to your application's Gemfile:
|
@@ -29,9 +27,9 @@ Let's role play a development scenario. Let's say you are writing an applicatio
|
|
29
27
|
# some options specifically for sending mail
|
30
28
|
attr_accessor :email_server, :use_ssl?, :port, :use_html?
|
31
29
|
|
32
|
-
|
33
|
-
|
34
|
-
|
30
|
+
def email_sent_callback(mailer)
|
31
|
+
# do something after mail is sent
|
32
|
+
end
|
35
33
|
end
|
36
34
|
|
37
35
|
And we create a MailerService to send mail:
|
@@ -52,39 +50,26 @@ And we create a MailerService to send mail:
|
|
52
50
|
end
|
53
51
|
end
|
54
52
|
|
55
|
-
|
53
|
+
We can call the Mailer service and pass it a user:
|
56
54
|
|
57
|
-
|
58
|
-
attr_accessor :configuration, :to, :subject, :message
|
55
|
+
MailerService.new(user, 'bob@example.com', 'test message', 'hello world!').deliver
|
59
56
|
|
60
|
-
|
61
|
-
self.configuration = configuration
|
62
|
-
self.to = to
|
63
|
-
self.subject = subject
|
64
|
-
self.message = message
|
65
|
-
end
|
57
|
+
Looking at the line of code above, one might wonder why a user is being passed into the mailer service. One might also wonder what properties of 'user' the mailer service is actually using. Is the mailer only reading properties of my user or is it *changing* the user? What if another developer comes along, noticing that the mailer is receiving a User model, and inadvertantly tightly couples MailerService to the User model? That may not cause immediate problems, but down the road services and models can become more and more tightly coupled. When you find yourself needing to use your service somewhere else you might find the de-coupling refactor to be a daunting task...
|
66
58
|
|
67
|
-
|
68
|
-
# ... implement mail sending here
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
This also works fine. We can call the Mailer service and pass it a user:
|
73
|
-
|
74
|
-
MailerService.new(user, 'bob@example.com', 'test message', 'hello world!').deliver
|
75
|
-
|
76
|
-
The problem isn't that it doesn't work, it does. The problem is that the readability is low. Looking at the line of code above, one might wonder why a user is being passed into the mailer service. One might also wonder what properties of 'user' the mailer service is actually using. Is the mailer only reading properties of my user or is it *changing* the user? What if another developer comes along, noticing that the mailer is receiving a User model, and inadvertantly tightly couples MailerService to the User model? That may not cause immediate problems, but down the road services and models can become more and more tightly coupled. When you find yourself needing to use your service somewhere else you might find the de-coupling refactor to be a daunting task...
|
77
|
-
|
78
|
-
So let's re-write this code using Interfaces. First, let's define an interface:
|
59
|
+
The Mailer service does not really need a User, what it needs is configuration. So let's re-write this code using Interfaces. First, let's define an interface:
|
79
60
|
|
80
61
|
class MailerConfiguration < Interface
|
81
62
|
abstract :email_server, :use_ssl?, :port, :use_html?, :email_sent_callback
|
82
63
|
end
|
83
64
|
|
84
|
-
Then, when we call our mailer service
|
65
|
+
Then, when we call our mailer service it will cast whatever object is passed in to be a MailerConfiguration object:
|
85
66
|
|
86
|
-
|
67
|
+
class MailerService
|
68
|
+
attr_accessor :config, :to, :subject, :message
|
87
69
|
|
70
|
+
def initialize(config, to, subject, message)
|
71
|
+
self.config = config.as(MailerConfiguration)
|
72
|
+
...
|
88
73
|
|
89
74
|
Now it's clear that the user is being passed in because it contains configuration information. Further, there is enforcement taking place-- if User did not implement one of the four required methods, a clear exception would be fired at runtime. And if MailerService tries to call another method of User that is not defined in the MailerConfiguration interface, an exception will be thrown. Lastly, there's one place to look to determine what methods are needed by MailerConfiguration-- the code is self documenting.
|
90
75
|
|
@@ -132,6 +117,11 @@ An interface may contain optional methods. If they are defined by a class then t
|
|
132
117
|
TestInterface.new.field2
|
133
118
|
=> nil
|
134
119
|
|
120
|
+
## Checking conformance
|
121
|
+
|
122
|
+
User.conforms_to?(TestInterface)
|
123
|
+
=> true
|
124
|
+
|
135
125
|
## Typed accessors
|
136
126
|
|
137
127
|
The typed_attr_accessor and typed_attr_writer helpers make it easy to create attributes that always conform to an interface:
|
data/lib/interfaces/castable.rb
CHANGED
@@ -20,6 +20,14 @@ module Interfaces
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
+
# returns true if an object conforms to the given interface (implements the abstract methods)
|
24
|
+
def conforms_to?(interface)
|
25
|
+
# interface must be an Interface
|
26
|
+
raise InterfaceError, "#{interface} is not an Interface" unless interface < Interface
|
27
|
+
interface.abstract_methods.all? { |method| self.respond_to?(method) }
|
28
|
+
end
|
29
|
+
|
30
|
+
# casts an object to an instance of a particular Interface
|
23
31
|
def as(interface)
|
24
32
|
# interface must be a class
|
25
33
|
raise InterfaceError, "#{interface} is not a class" unless interface.is_a?(Class)
|
data/lib/interfaces/version.rb
CHANGED
data/spec/castable_spec.rb
CHANGED
@@ -31,6 +31,10 @@ describe Interfaces::Castable do
|
|
31
31
|
let(:instance) { ClassConformingToTestInterface.new }
|
32
32
|
let(:casted_instance) { instance.as(TestInterface) }
|
33
33
|
|
34
|
+
it 'instance should conform_to the TestInteface' do
|
35
|
+
instance.conforms_to?(TestInterface).should be_true
|
36
|
+
end
|
37
|
+
|
34
38
|
it 'casted_instance should be an instance of TestInterface' do
|
35
39
|
casted_instance.should be_a(TestInterface)
|
36
40
|
end
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: interfaces
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
5
|
-
prerelease:
|
4
|
+
version: 0.2.0
|
5
|
+
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Justin Schumacher
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2014-05-10 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
@@ -101,9 +101,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
101
101
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
102
|
none: false
|
103
103
|
requirements:
|
104
|
-
- - ! '
|
104
|
+
- - ! '>='
|
105
105
|
- !ruby/object:Gem::Version
|
106
|
-
version:
|
106
|
+
version: '0'
|
107
107
|
requirements: []
|
108
108
|
rubyforge_project:
|
109
109
|
rubygems_version: 1.8.23
|
@@ -116,4 +116,3 @@ test_files:
|
|
116
116
|
- spec/interface_spec.rb
|
117
117
|
- spec/spec_helper.rb
|
118
118
|
- spec/typed_accessor_spec.rb
|
119
|
-
has_rdoc:
|