brassbound-dci 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in brassbound-dci.gemspec
4
+ gemspec
data/README.rdoc ADDED
@@ -0,0 +1,78 @@
1
+ = Brassbound
2
+
3
+ Brassbound is a simple but strict implementation of the Data, Context, and Interaction (DCI) paradigm for Ruby.
4
+
5
+ = Example
6
+
7
+ The canonical DCI example is the transfer of funds between a money source
8
+ and money sink. In this example, source and sink are both roles
9
+ that are attached to data objects, and the transfer of funds is orchestrated
10
+ by the context. Here's how the example looks using Brassbound.
11
+
12
+ require 'brassbound'
13
+
14
+ # Domain/data objects are plain old Ruby objects.
15
+ class Account
16
+ # Pretend to lookup accounts in a database.
17
+ def self.find(account_id)
18
+ case account_id
19
+ when 1
20
+ Account.new(account_id, 2000)
21
+ when 2
22
+ Account.new(account_id, 1000)
23
+ end
24
+ end
25
+
26
+ attr_reader :id
27
+ attr_accessor :balance
28
+
29
+ def initialize(id, balance)
30
+ @id = id
31
+ @balance = balance
32
+ end
33
+ end
34
+
35
+ # Roles are plain old Ruby modules, and automatically have access to
36
+ # the invoking context.
37
+ module MoneySource
38
+ def transfer_out(amount)
39
+ puts("Transferring #{amount} from account #{self.id} to account #{context.money_sink.id}")
40
+ self.balance -= amount
41
+ puts("Source account new balance: #{self.balance}")
42
+ context.money_sink.transfer_in(amount)
43
+ end
44
+ end
45
+
46
+ module MoneySink
47
+ def transfer_in(amount)
48
+ self.balance += amount
49
+ puts("Destination account new balance: #{self.balance}")
50
+ end
51
+ end
52
+
53
+ # Contexts are Ruby classes that include the Brassbound::Context module.
54
+ # The common idiom is for the initialize method to create all of the
55
+ # necessary objects and declare how they are bound to roles.
56
+ # Then, within the scope of the execute method, the objects will have been
57
+ # bound to the declared roles, and can be accessed by the role name
58
+ # (convert to lower case with underscores by default).
59
+ class TransferFunds
60
+ include Brassbound::Context
61
+
62
+ def initialize(source_account_id, dest_account_id, amount)
63
+ @amount = amount
64
+
65
+ role MoneySource, Account.find(source_account_id)
66
+ role MoneySink, Account.find(dest_account_id)
67
+ end
68
+
69
+ def execute
70
+ # Here, money_source refers to the object bound to the MoneySource role
71
+ # in the initialize method.
72
+ money_source.transfer_out(@amount)
73
+ end
74
+ end
75
+
76
+ # Now let's create and execute our context.
77
+ TransferFunds.new(1, 2, 100).call
78
+
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ $:.push File.expand_path("../lib", __FILE__)
4
+ require 'brassbound/version'
5
+ require 'rake'
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "brassbound-dci"
9
+ s.version = Brassbound::VERSION
10
+ s.authors = ["Jason Voegele"]
11
+ s.email = ["jason@jvoegele.com"]
12
+ s.homepage = ""
13
+ s.summary = "Simple but strict DCI framework"
14
+ s.description = "Brassbound is a simple but strict implementation of the Data, Context, and Interaction (DCI) paradigm for Ruby."
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_development_dependency 'rspec', '~> 2.10.0'
22
+ end
@@ -0,0 +1,67 @@
1
+ require 'brassbound'
2
+
3
+ # Domain/data objects are plain old Ruby objects.
4
+ class Account
5
+ # Pretend to lookup accounts in a database.
6
+ def self.find(account_id)
7
+ case account_id
8
+ when 1
9
+ Account.new(account_id, 2000)
10
+ when 2
11
+ Account.new(account_id, 1000)
12
+ end
13
+ end
14
+
15
+ attr_reader :id
16
+ attr_accessor :balance
17
+
18
+ def initialize(id, balance)
19
+ @id = id
20
+ @balance = balance
21
+ end
22
+ end
23
+
24
+ # Roles are plain old Ruby modules, and automatically have access to
25
+ # the invoking context.
26
+ module MoneySource
27
+ def transfer_out(amount)
28
+ puts("Transferring #{amount} from account #{self.id} to account #{context.money_sink.id}")
29
+ self.balance -= amount
30
+ puts("Source account new balance: #{self.balance}")
31
+ context.money_sink.transfer_in(amount)
32
+ end
33
+ end
34
+
35
+ module MoneySink
36
+ def transfer_in(amount)
37
+ self.balance += amount
38
+ puts("Destination account new balance: #{self.balance}")
39
+ end
40
+ end
41
+
42
+ # Contexts are Ruby classes that include the Brassbound::Context module.
43
+ # The common idiom is for the initialize method to create all of the
44
+ # necessary objects and declare how they are bound to roles.
45
+ # Then, within the scope of the execute method, the objects will have been
46
+ # bound to the declared roles, and can be accessed by the role name
47
+ # (convert to lower case with underscores by default).
48
+ class TransferFunds
49
+ include Brassbound::Context
50
+
51
+ def initialize(source_account_id, dest_account_id, amount)
52
+ @amount = amount
53
+
54
+ role MoneySource, Account.find(source_account_id)
55
+ role MoneySink, Account.find(dest_account_id)
56
+ end
57
+
58
+ def execute
59
+ # Here, money_source refers to the object bound to the MoneySource role
60
+ # in the initialize method.
61
+ money_source.transfer_out(@amount)
62
+ end
63
+ end
64
+
65
+ # Now let's create and execute our context.
66
+ TransferFunds.new(1, 2, 100).call
67
+
@@ -0,0 +1,52 @@
1
+ require 'brassbound'
2
+
3
+ class Person
4
+ attr_accessor :first_name, :last_name
5
+
6
+ def initialize(first_name, last_name)
7
+ @first_name = first_name
8
+ @last_name = last_name
9
+ end
10
+ end
11
+
12
+ module WifeToBe
13
+ def marry(husband)
14
+ self.last_name = husband.last_name
15
+ end
16
+ end
17
+
18
+ module HusbandToBe
19
+ def marry(wife)
20
+ end
21
+ end
22
+
23
+ module Minister
24
+ def administer_marraige
25
+ context.husband.marry(context.wife)
26
+ context.wife.marry(context.husband)
27
+ puts("I, #{first_name} #{last_name}, now pronounce you Mr. and Mrs. #{context.wife.last_name}")
28
+ end
29
+ end
30
+
31
+ # In a traditional wedding, a man and a woman are married by a minister.
32
+ class TraditionalWedding
33
+ include Brassbound::Context
34
+
35
+ def initialize(man_first_name, man_last_name,
36
+ woman_first_name, woman_last_name)
37
+ @man = Person.new(man_first_name, man_last_name)
38
+ @woman = Person.new(woman_first_name, woman_last_name)
39
+ @mark = Person.new('Mark', 'Schlafman')
40
+
41
+ role :husband, HusbandToBe, @man
42
+ role :wife, WifeToBe, @woman
43
+ role Minister, @mark
44
+ end
45
+
46
+ def execute
47
+ minister.administer_marraige
48
+ end
49
+ end
50
+
51
+ TraditionalWedding.new('Jason', 'Voegele', 'Jennifer', 'Bollinger').call
52
+
@@ -0,0 +1,163 @@
1
+ # This module represents the notion of the Context in the DCI paradigm.
2
+ #
3
+ # According to http://en.wikipedia.org/wiki/Data,_context_and_interaction
4
+ #
5
+ # The Context is the class (or its instance) whose code includes the roles
6
+ # for a given algorithm, scenario, or use case, as well as the code to map
7
+ # these roles into objects at run time and to enact the use case. Each role
8
+ # is bound to exactly one object during any given use case enactment; however,
9
+ # a single object may simultaneously play several roles. A context is
10
+ # instantiated at the beginning of the enactment of an algorithm, scenario,
11
+ # or use case. In summary, a Context comprises use cases and algorithms in
12
+ # which data objects are used through specific Roles.
13
+ #
14
+ # In Brassbound, context implementations are classes that include this module.
15
+ # Context implementations can use the #role method to bind roles to objects,
16
+ # and must provide an #execute method that enacts the use case behavior.
17
+ #
18
+ # Brassbound enforces the fact that each role is bound to exactly one object,
19
+ # since each role must have a unique name. However, any given object may
20
+ # simultaneously play several roles.
21
+ #
22
+ # Note that no special support is required for the role module or data object
23
+ # implementations; they are normal Ruby modules and objects, respectively.
24
+ # The context manages the necessary plumbing to bind roles to objects and to
25
+ # provide access to the context for the roles. In other words, role modules do
26
+ # not have to include any other modules or adhere to any particular conventions,
27
+ # and neither do the objects to which the roles are bound need to inherit from
28
+ # any other class or include any other modules.
29
+ module Brassbound::Context
30
+
31
+ require 'brassbound/util'
32
+
33
+ def self.current
34
+ Thread.current[:brassbound_context]
35
+ end
36
+
37
+ RoleMapping = Struct.new(:role_module, :data_object)
38
+
39
+ # A Hash containing all of the declared role mappings for this context.
40
+ # The keys of the Hash are the declared role names, while the values
41
+ # are the associated RoleMapping objects.
42
+ #
43
+ # Role mappings are declared using the #role method, which see for
44
+ # further details.
45
+ def roles
46
+ @declared_roles ||= Hash.new
47
+ end
48
+
49
+ # Declare a role mapping.
50
+ #
51
+ # This method accepts either two or three arguments. In the three argument
52
+ # form, the arguments are as follows:
53
+ #
54
+ # role_name:: The name of the role in this context.
55
+ # role_module:: The module that serves as the "methodful role".
56
+ # obj:: The data object to which the role_module will be attached.
57
+ #
58
+ # In the two argument form, the role_name is omitted and the role_name
59
+ # is derived from the name of the role_module by converting from
60
+ # CamelCaseName to underscore_name. For instance, if the role_name
61
+ # is not specified, then a role_module called MoneySource would be
62
+ # named money_source.
63
+ #
64
+ # This method adds the role mapping to the Hash returned by the #roles
65
+ # method. If the context is already executing when this method is
66
+ # invoked, it will call #apply_role to reify the role mapping
67
+ # immediately. Otherwise, all role mappings are applied when the
68
+ # context begins execution.
69
+ def role(*args)
70
+ case args.size
71
+ when 2
72
+ role_module = args[0]
73
+ obj = args[1]
74
+ role_name = Util.underscore(role_module.name).to_sym
75
+ when 3
76
+ role_name = args[0]
77
+ role_module = args[1]
78
+ obj = args[2]
79
+ else
80
+ raise ArgumentError
81
+ end
82
+
83
+ self.roles[role_name] = RoleMapping.new(role_module, obj)
84
+
85
+ if ::Brassbound::Context.current.equal?(self)
86
+ # If we are already in the execute method, apply the role mapping.
87
+ apply_role(role_name)
88
+ end
89
+ end
90
+
91
+ # Apply the role identified by role_name to the associated object.
92
+ # This has several effects. First, the object is extended with the
93
+ # role module (i.e. the "methodful role" in DCI terminology), thus
94
+ # adding all of the role module's methods to the object. Second,
95
+ # the object has a :context method added to it, which provides
96
+ # the role implementation access to this context. Finally, a new
97
+ # method is added to this context object, which is named after the
98
+ # role_name and which returns the object which is mapped to that role.
99
+ def apply_role(role_name)
100
+ role_mapping = self.roles[role_name]
101
+ role_module, obj = role_mapping.role_module, role_mapping.data_object
102
+ obj.extend(role_module)
103
+ obj.instance_variable_set(:@__brassbound_context, self)
104
+ class << obj
105
+ def context
106
+ @__brassbound_context
107
+ end
108
+ end
109
+ self.singleton_class.send(:define_method, role_name) do
110
+ obj
111
+ end
112
+ end
113
+
114
+ # Undo (most of) the effects of #apply_role. This method will remove all of
115
+ # the methods that were added to the mapped data object by #apply role, but
116
+ # the fact that the object has been extended with the role module cannot be
117
+ # undone. Therefore, <tt>obj.kind_of?(role_module)</tt> will still be true,
118
+ # even though the methods defined in role_module will have been removed from
119
+ # the object.
120
+ def unapply_role(role_name)
121
+ role_mapping = self.roles[role_name]
122
+ role_module, obj = role_mapping.role_module, role_mapping.data_object
123
+ obj.instance_variable_set(:@__brassbound_context, nil)
124
+ class << obj
125
+ remove_method(:context)
126
+ end
127
+ role_module.instance_methods.each do |m|
128
+ obj.singleton_class.send(:undef_method, m)
129
+ end
130
+ self.singleton_class.send(:undef_method, role_name)
131
+ end
132
+
133
+ def undef_role(role_module, obj)
134
+ obj.instance_variable_set(:@__brassbound_context, nil)
135
+ class << obj
136
+ remove_method(:context)
137
+ end
138
+ role_module.instance_methods.each do |m|
139
+ obj.singleton_class.send(:undef_method, m)
140
+ end
141
+ end
142
+
143
+ # Begin execution of this context. Note that context implementations must
144
+ # provide an #execute method, and should not override this #call method.
145
+ # This method will first set Context.current to self, apply all of the
146
+ # role mappings that have been declared with the #role method, and then
147
+ # call the #execute method.
148
+ #
149
+ # After the #execute method has returned, it will unapply all role mappings
150
+ # and set the current context back to its previous value.
151
+ def call(*args)
152
+ old_context = ::Brassbound::Context.current
153
+ Thread.current[:brassbound_context] = self
154
+
155
+ roles.each_key(&method(:apply_role))
156
+ begin
157
+ self.execute(*args)
158
+ ensure
159
+ roles.each_key(&method(:unapply_role))
160
+ Thread.current[:brassbound_context] = old_context
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,9 @@
1
+ module Util
2
+ def self.underscore(str)
3
+ str.gsub(/::/, '/').
4
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
5
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
6
+ tr("-", "_").
7
+ downcase
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module Brassbound
2
+ VERSION = "0.5.0"
3
+ end
data/lib/brassbound.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'brassbound/version'
2
+
3
+ module Brassbound
4
+ end
5
+
6
+ require 'brassbound/context'
7
+
@@ -0,0 +1,90 @@
1
+ require 'spec_helper'
2
+ include Brassbound
3
+
4
+ class Account
5
+ def self.find(account_id)
6
+ case account_id
7
+ when 1
8
+ Account.new(200)
9
+ when 2
10
+ Account.new(100)
11
+ end
12
+ end
13
+
14
+ attr_accessor :balance
15
+
16
+ def initialize(balance)
17
+ @balance = balance
18
+ end
19
+ end
20
+
21
+ module MoneySource
22
+ def transfer_out(amount)
23
+ self.balance -= amount
24
+ context.dest_account.transfer_in(amount)
25
+ end
26
+ end
27
+
28
+ module MoneySink
29
+ def transfer_in(amount)
30
+ self.balance += amount
31
+ end
32
+ end
33
+
34
+ class TransferFunds
35
+ include Context
36
+
37
+ def initialize(source_account_id, dest_account_id, amount)
38
+ @source_account_id = source_account_id
39
+ @dest_account_id = dest_account_id
40
+ @amount = amount
41
+
42
+ role :source_account, MoneySource, Account.find(@source_account_id)
43
+ role :dest_account, MoneySink, Account.find(@dest_account_id)
44
+ end
45
+
46
+ def execute
47
+ source_account.transfer_out(@amount)
48
+ # Allow spec to access the executing context for testing
49
+ yield self if block_given?
50
+ end
51
+ end
52
+
53
+ describe Context do
54
+ include Context
55
+
56
+ let(:source_account_id) { 1 }
57
+ let(:dest_account_id) { 2 }
58
+ let(:source_account) { Account.find(source_account_id) }
59
+ let(:dest_account) { Account.find(dest_account_id) }
60
+
61
+ let(:transfer_funds_context) {
62
+ TransferFunds.new(source_account_id, dest_account_id, 50)
63
+ }
64
+
65
+ context "#role" do
66
+ it "declares a role mapping" do
67
+ role MoneySource, source_account
68
+ mapping = roles[:money_source]
69
+ mapping.role_module.should == MoneySource
70
+ mapping.data_object.should == source_account
71
+
72
+ role :money_sink, MoneySink, dest_account
73
+ mapping = roles[:money_sink]
74
+ mapping.role_module.should == MoneySink
75
+ mapping.data_object.should == dest_account
76
+ end
77
+ end
78
+
79
+ context "#call" do
80
+ it "binds all roles to their mapped objects" do
81
+ transfer_funds_context.call do |ctx|
82
+ ctx.source_account.should == source_account
83
+ ctx.source_account.should be_kind_of(MoneySource)
84
+ ctx.dest_account.should == dest_account
85
+ ctx.dest_account.should be_kind_of(MoneySink)
86
+ end
87
+ end
88
+ end
89
+ end
90
+
@@ -0,0 +1,2 @@
1
+ require 'brassbound'
2
+
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: brassbound-dci
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jason Voegele
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-05-05 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: &70190658979080 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 2.10.0
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70190658979080
25
+ description: Brassbound is a simple but strict implementation of the Data, Context,
26
+ and Interaction (DCI) paradigm for Ruby.
27
+ email:
28
+ - jason@jvoegele.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - .gitignore
34
+ - Gemfile
35
+ - README.rdoc
36
+ - Rakefile
37
+ - brassbound-dci.gemspec
38
+ - examples/money_transfer.rb
39
+ - examples/wedding.rb
40
+ - lib/brassbound.rb
41
+ - lib/brassbound/context.rb
42
+ - lib/brassbound/util.rb
43
+ - lib/brassbound/version.rb
44
+ - spec/context_spec.rb
45
+ - spec/spec_helper.rb
46
+ homepage: ''
47
+ licenses: []
48
+ post_install_message:
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ! '>='
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ none: false
60
+ requirements:
61
+ - - ! '>='
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements: []
65
+ rubyforge_project:
66
+ rubygems_version: 1.8.15
67
+ signing_key:
68
+ specification_version: 3
69
+ summary: Simple but strict DCI framework
70
+ test_files:
71
+ - spec/context_spec.rb
72
+ - spec/spec_helper.rb