├── VERSION ├── demo ├── applique │ └── dci.rb ├── context.md └── account_example.md ├── .yardopts ├── .gitignore ├── .travis.yml ├── Gemfile ├── MANIFEST ├── HISTORY.md ├── Assembly ├── Profile ├── README.md ├── .ruby ├── lib ├── dci.rb └── dci │ ├── object.rb │ ├── role.rb │ └── context.rb ├── try └── example.rb ├── work └── reference │ ├── example.rb │ └── my-example.rb ├── COPYING.md └── .gemspec /VERSION: -------------------------------------------------------------------------------- 1 | 0.2.0 2 | -------------------------------------------------------------------------------- /demo/applique/dci.rb: -------------------------------------------------------------------------------- 1 | require 'dci' 2 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --title "DCI" 2 | --readme README.md 3 | --protected 4 | --private 5 | lib 6 | - 7 | [A-Z]*.* 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .reap/digest 2 | .yardoc 3 | doc 4 | log 5 | pkg 6 | site 7 | tmp 8 | web 9 | wiki 10 | work/trash 11 | DEMO.md 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | script: "bundle exec qed" 3 | rvm: 4 | - 1.8.7 5 | - 1.9.2 6 | - 1.9.3 7 | - rbx-19mode 8 | - jruby-19mode 9 | 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | #source :rubygems 4 | #gemspec 5 | 6 | gem 'detroit', :groups => [:development, :build] 7 | gem 'qed', :groups => [:development, :test] 8 | 9 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | #!mast .ruby .yardopts bin demo lib test [A-Z]*.* 2 | .ruby 3 | .yardopts 4 | demo/account_example.md 5 | demo/applique/dci.rb 6 | lib/dci/context.rb 7 | lib/dci/object.rb 8 | lib/dci/role.rb 9 | lib/dci.rb 10 | COPYING.md 11 | HISTORY.md 12 | README.md 13 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # HISTORY 2 | 3 | ## 0.2.0 | 2012-02-08 4 | 5 | This release wraps the code in the DCI namepace and improved role 6 | lookup in Context class. 7 | 8 | Changes: 9 | 10 | * Use DCI namespace. 11 | * Improve context role lookup. 12 | 13 | 14 | ## 0.1.0 | 2012-02-07 15 | 16 | This is the initial working version of DCI. 17 | 18 | Changes: 19 | 20 | * Happy Birthday! 21 | 22 | -------------------------------------------------------------------------------- /Assembly: -------------------------------------------------------------------------------- 1 | --- 2 | github: 3 | gh_pages: web 4 | 5 | gem: 6 | active: true 7 | 8 | dnote: 9 | title: Source Notes 10 | output: log/notes.html 11 | 12 | vclog: 13 | output: 14 | - log/history.html 15 | - log/changes.html 16 | 17 | email: 18 | mailto: 19 | - ruby-talk@ruby-lang.org 20 | - rubyworks-mailinglist@googlegroups.com 21 | 22 | qed: 23 | files: demo/ 24 | 25 | qedoc: 26 | files: demo/ 27 | output: 28 | - DEMO.md 29 | - web/demo.html 30 | 31 | -------------------------------------------------------------------------------- /Profile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | version File.read('VERSION').strip 4 | 5 | name "dci" 6 | title "DCI" 7 | summary "DCI for Ruby" 8 | created '2011-03-04' 9 | 10 | description "Faithful DCI framework for Ruby application development." 11 | 12 | authors [ 13 | 'Thomas Sawyer ' 14 | ] 15 | 16 | #requirements [ 17 | # 'detroit (build)', 18 | # 'qed (test)' 19 | #] 20 | 21 | resources( 22 | 'home' => 'http://rubyworks.github.com/dci', 23 | 'code' => 'http://github.com/rubyworks/dci', 24 | 'mail' => 'http://groups.google.com/groups/rubyworks-mailinglist' 25 | ) 26 | 27 | repositories( 28 | 'upstream' => 'git@github.com:rubyworks/dci.git' 29 | ) 30 | 31 | copyrights [ 32 | '2012 Thomas Sawyer (BSD-2-Clause)' 33 | ] 34 | 35 | organization 'Rubyworks' 36 | 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DCI for Ruby 2 | 3 | [Home](http://rubyworks.github.com/dci) / 4 | [Code](http://github.com/rubyworks/dci) / 5 | [Bugs](http://github.com/rubyworks/dci/issues) / 6 | [Mail](http://groups.google.com/groups/rubyworks-mailinglist) 7 | 8 | 9 | ## Description 10 | 11 | The DCI library for Ruby is a fairly faithful implementation of the DCI 12 | concepts developed by Trygve Reenskaug, Reenskaug, and James O. Coplien. 13 | 14 | It defines two reusable base classes, the Role and Context. The best way 15 | to understand their usage is to look at the QED documentation provided 16 | ([for example](https://github.com/rubyworks/dci/blob/master/demo/account_example.md)). 17 | 18 | 19 | ## Installation 20 | 21 | The usual RubyGems install procedure: 22 | 23 | $ gem install dci 24 | 25 | 26 | ## Copyrights 27 | 28 | Copyright (c) 2012 Rubyworks. All rights reserved. 29 | 30 | DCI for Ruby is distributable under the terms of **BSD-2-Clause** license. 31 | 32 | See COPYING.md file for license details. 33 | -------------------------------------------------------------------------------- /demo/context.md: -------------------------------------------------------------------------------- 1 | # Context 2 | 3 | Let's setup a very simple example to run some tests. 4 | 5 | class ExampleObject 6 | def say_anything 7 | "anything" 8 | end 9 | end 10 | 11 | class ExampleRole < DCI::Role 12 | def say_anything 13 | @self.say_anything 14 | end 15 | end 16 | 17 | class ExampleContext < DCI::Context 18 | role :player => ExampleRole 19 | 20 | def initialize(player) 21 | self.player = player 22 | end 23 | 24 | def say_anything 25 | roles.each{ |role| role.say_anything } 26 | end 27 | end 28 | 29 | So lets make sure the roles are listed. 30 | 31 | ExampleContext.roles #=> [:player] 32 | 33 | Now if subclass the ExampleContext and added a new roll, 34 | 35 | class AlternateContext < ExampleContext 36 | role :fanboy => ExampleRole 37 | end 38 | 39 | Then both roles should be in the the list. 40 | 41 | AlternateContext.roles #=> [:fanboy, :player] 42 | 43 | 44 | -------------------------------------------------------------------------------- /.ruby: -------------------------------------------------------------------------------- 1 | --- 2 | source: 3 | - Profile 4 | authors: 5 | - name: Thomas Sawyer 6 | email: transfire@gmail.com 7 | copyrights: 8 | - holder: Thomas Sawyer 9 | year: '2012' 10 | license: BSD-2-Clause 11 | replacements: [] 12 | alternatives: [] 13 | requirements: 14 | - name: detroit 15 | version: ! '>= 0' 16 | groups: 17 | - :development 18 | - :build 19 | - name: qed 20 | version: ! '>= 0' 21 | groups: 22 | - :test 23 | - :development 24 | dependencies: [] 25 | conflicts: [] 26 | repositories: 27 | - uri: git@github.com:rubyworks/dci.git 28 | scm: git 29 | name: upstream 30 | resources: 31 | home: http://rubyworks.github.com/dci 32 | code: http://github.com/rubyworks/dci 33 | mail: http://groups.google.com/groups/rubyworks-mailinglist 34 | extra: {} 35 | load_path: 36 | - lib 37 | revision: 0 38 | version: 0.2.0 39 | name: dci 40 | title: DCI 41 | summary: DCI for Ruby 42 | created: '2011-03-04' 43 | description: Faithful DCI framework for Ruby application development. 44 | organization: Rubyworks 45 | date: '2012-02-19' 46 | -------------------------------------------------------------------------------- /lib/dci.rb: -------------------------------------------------------------------------------- 1 | # Data, Context and Interaction (DCI) is a paradigm used in computer software to 2 | # program systems of communicating objects. Its goals are: 3 | # 4 | # * To improve the readability of object-oriented code by giving system behavior 5 | # first-class status; 6 | # 7 | # * To cleanly separate code for rapidly changing system behavior (what the system does) 8 | # from code for slowly changing domain knowledge (what the system is), instead 9 | # of combining both in one class interface; 10 | # 11 | # * To help software developers reason about system-level state and behavior 12 | # instead of only object state and behavior; 13 | # 14 | # * To support an object style of thinking that is close to peoples' mental 15 | # models, rather than the class style of thinking that overshadowed object 16 | # thinking early in the history of object-oriented programming languages. 17 | # 18 | # The paradigm separates the domain model (data) from use cases (context) and roles 19 | # that objects play (interaction). DCI is complementary to model–view–controller (MVC). 20 | # MVC as a pattern language is still used to separate the data and its processing from 21 | # presentation. 22 | # 23 | # DCI was invented by Trygve Reenskaug, also the inventor of MVC. The current formulation 24 | # of DCI is mostly the work of Reenskaug and James O. Coplien. 25 | # 26 | # acct1 = Account.new(10500) 27 | # acct2 = Account.new(10010) 28 | # 29 | # Balance::Transfer.new(acct1, acct2).transfer(50) 30 | # 31 | module DCI 32 | end 33 | 34 | require 'dci/object' # data 35 | require 'dci/context' # context 36 | require 'dci/role' # interation 37 | 38 | 39 | -------------------------------------------------------------------------------- /try/example.rb: -------------------------------------------------------------------------------- 1 | require 'dci' 2 | 3 | # Mixins are fixed rolls. 4 | module Balance 5 | def initialize(account_id) 6 | @account_id = account_id 7 | @balance = 0 8 | end 9 | def account_id 10 | @account_id 11 | end 12 | def available_balance 13 | @balance 14 | end 15 | def increase_balance(amount) 16 | @balance += amount 17 | end 18 | def decrease_balance(amount) 19 | @balance -= amount 20 | end 21 | end 22 | 23 | # 24 | class Balance::TransferSource < Role 25 | def transfer(amount) 26 | decrease_balance(amount) 27 | puts "Tranfered $#{amount} from account ##{account_id}." 28 | end 29 | end 30 | 31 | # 32 | class Balance::TransferDestination < Role 33 | def transfer(amount) 34 | increase_balance(amount) 35 | puts "Tranfered $#{amount} into account ##{account_id}." 36 | end 37 | end 38 | 39 | # We can think of a context as setting a scene. 40 | class Balance::Transfer < Context 41 | role :source_account => Balance::TransferSource 42 | role :destination_account => Balance::TransferDestination 43 | 44 | def initialize(source_account, destination_account) 45 | self.source_account = source_account 46 | self.destination_account = destination_account 47 | end 48 | 49 | def transfer(amount) 50 | begin 51 | puts "Begin transfer." 52 | roles.each{ |role| role.transfer(amount) } 53 | end 54 | puts "Transfer complete." 55 | end 56 | end 57 | 58 | class Account 59 | # An account by definition has a balance. 60 | include Balance 61 | end 62 | 63 | acct1 = Account.new(000100) 64 | acct2 = Account.new(000200) 65 | 66 | Balance::Transfer.new(acct1, acct2).transfer(50) 67 | 68 | 69 | -------------------------------------------------------------------------------- /work/reference/example.rb: -------------------------------------------------------------------------------- 1 | class Account 2 | def self.find(accountID) 3 | @@accunts[accountID] 4 | end 5 | 6 | attr :accountID 7 | attr :balance 8 | 9 | def initialize(accountID, initialBalance) 10 | @@accounts[accountID] = self 11 | 12 | @accountID = accountID 13 | @balance = initialBalance 14 | end 15 | end 16 | 17 | class SavingsAccount < Account 18 | def initialize(accountID, initialBalance) 19 | super(accountID, initialBalance) 20 | end 21 | 22 | def availableBalance; @balance; end 23 | def decreaseBalance(amount); @balance -= amount; end 24 | def increaseBalance(amount); @balance += amount; end 25 | 26 | def updateLog(message, time, amount) 27 | end 28 | end 29 | 30 | class TransferMoneyContext 31 | attr :source_account 32 | attr :destination_account 33 | attr :amount 34 | def initialize(amt, sourceID, destID) 35 | @source_account = Account.find(sourceID) 36 | @source_account.extend TransferMoneySource 37 | @destination_account = Account.find(destID) 38 | @amount = amt 39 | end 40 | def execute 41 | execute_in_context do 42 | source_account.transferTo 43 | end 44 | end 45 | def self.execute(amt, sourceID, destID) 46 | TransferMoneyContext.new(amt, sourceID, destID).execute 47 | end 48 | end 49 | 50 | module TransferMoneySource 51 | include ContextAccessor 52 | def transferTo 53 | raise "Insufficiant Funds" if balance < context.amount 54 | withdraw context.amount 55 | context.source_account.deposit context.amount 56 | updateLog "Transfer Out", Time.now, context.amount 57 | context.source_account.updateLog "Transfer In", Time.now, context.amount 58 | end 59 | end 60 | 61 | -------------------------------------------------------------------------------- /COPYING.md: -------------------------------------------------------------------------------- 1 | # COPYRIGHT 2 | 3 | ## NOTICES 4 | 5 | ### Assay 6 | 7 | | Project | Assay | 8 | |-----------|-----------------------------------| 9 | | Copyright | (c) 2012 Rubyworks | 10 | | License | (r) BSD-2-Clause | 11 | | Website | http://rubyworks.github.com/assay | 12 | 13 | ## LICENSES 14 | 15 | ### BSD-2-Clause License 16 | 17 | Redistribution and use in source and binary forms, with or without 18 | modification, are permitted provided that the following conditions are met: 19 | 20 | 1. Redistributions of source code must retain the above copyright notice, 21 | this list of conditions and the following disclaimer. 22 | 23 | 2. Redistributions in binary form must reproduce the above copyright 24 | notice, this list of conditions and the following disclaimer in the 25 | documentation and/or other materials provided with the distribution. 26 | 27 | THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, 28 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 29 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 30 | COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 31 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 32 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 33 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 34 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 35 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 36 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 37 | -------------------------------------------------------------------------------- /work/reference/my-example.rb: -------------------------------------------------------------------------------- 1 | # 2 | class Account 3 | def self.accounts 4 | @@accounts ||= {} 5 | end 6 | 7 | def self.find(accountID) 8 | accounts[accountID] 9 | end 10 | 11 | attr :accountID 12 | attr :balance 13 | 14 | def initialize(accountID, initialBalance) 15 | Account.accounts[accountID] = self 16 | 17 | @accountID = accountID 18 | @balance = initialBalance 19 | end 20 | end 21 | 22 | # 23 | class SavingsAccount < Account 24 | def initialize(accountID, initialBalance) 25 | super(accountID, initialBalance) 26 | end 27 | 28 | def availableBalance; @balance; end 29 | def decreaseBalance(amount); @balance -= amount; end 30 | def increaseBalance(amount); @balance += amount; end 31 | 32 | def updateLog(message, time, amount) 33 | puts "%s %s #%s $%.2f" % [message, time, accountID, amount.to_f] 34 | end 35 | end 36 | 37 | # Use Case (Context) 38 | class MoneyTransfer 39 | attr :amount 40 | attr :source_account 41 | attr :destination_account 42 | 43 | def initialize(amt, sourceID, destID) 44 | @amount = amt 45 | @source_account = Account.find(sourceID) 46 | @destination_account = Account.find(destID) 47 | end 48 | 49 | def execute 50 | source_account.extend TransferSource 51 | destination_account.extend TransferDestination 52 | 53 | source_account.withdraw(amount) 54 | destination_account.deposit(amount) 55 | 56 | #source_account.unextend TransferSource 57 | #destination_account.unextend TransferDestination 58 | end 59 | end 60 | 61 | # Account Role 62 | module TransferSource 63 | def withdraw(amount) 64 | raise "Insufficiant Funds" if balance < amount 65 | decreaseBalance(amount) 66 | updateLog "Transfer Out", Time.now, amount 67 | end 68 | end 69 | 70 | # Account Role 71 | module TransferDestination 72 | def deposit(amount) 73 | increaseBalance(amount) 74 | updateLog "Transfer In", Time.now, amount 75 | end 76 | end 77 | 78 | SavingsAccount.new(1, 500) 79 | SavingsAccount.new(2, 100) 80 | 81 | transfer_case = MoneyTransfer.new(50, 1, 2) 82 | transfer_case.execute 83 | 84 | -------------------------------------------------------------------------------- /demo/account_example.md: -------------------------------------------------------------------------------- 1 | # Account Balance Transfer 2 | 3 | The Account Balance Transfre is the classic example of using DCI. 4 | 5 | First we need our Data model. In the example that is the Account class. 6 | To keep our example simple we will initialize new accounts with 7 | a balance of $100. (We're generous like that.) 8 | 9 | class Account 10 | def initialize(account_id) 11 | @account_id = account_id 12 | @balance = 100 13 | end 14 | def account_id 15 | @account_id 16 | end 17 | def available_balance 18 | @balance 19 | end 20 | def increase_balance(amount) 21 | @balance += amount 22 | end 23 | def decrease_balance(amount) 24 | @balance -= amount 25 | end 26 | end 27 | 28 | We set up two Roles, one role for withdrawing money from an account, 29 | and one for depositing money into an account. 30 | 31 | class Account::TransferWithdraw < DCI::Role 32 | def transfer(amount) 33 | decrease_balance(amount) 34 | #log "Tranfered $#{amount} from account ##{account_id}." 35 | end 36 | end 37 | 38 | class Account::TransferDeposit < DCI::Role 39 | def transfer(amount) 40 | increase_balance(amount) 41 | #log "Tranfered $#{amount} into account ##{account_id}." 42 | end 43 | end 44 | 45 | Now we create a Context which will assign accounts to the roles 46 | and used to perform the transfer. 47 | 48 | class Account::Transfer < DCI::Context 49 | role :source_account => Account::TransferWithdraw 50 | role :destination_account => Account::TransferDeposit 51 | 52 | def initialize(source_account, destination_account) 53 | self.source_account = source_account 54 | self.destination_account = destination_account 55 | end 56 | 57 | def transfer(amount) 58 | #log "Begin transfer." 59 | roles.each{ |role| role.transfer(amount) } 60 | #log "Transfer complete." 61 | end 62 | end 63 | 64 | Let's give it a try. 65 | 66 | acct1 = Account.new(000100) 67 | acct2 = Account.new(000200) 68 | 69 | Account::Transfer.new(acct1, acct2).transfer(50) 70 | 71 | acct1.available_balance #=> 50 72 | acct2.available_balance #=> 150 73 | 74 | -------------------------------------------------------------------------------- /lib/dci/object.rb: -------------------------------------------------------------------------------- 1 | # Data - The data are "what the system is." The data part of the DCI architecture 2 | # is its (relatively) static data model with relations. The data design is usually 3 | # coded up as conventional classes that represent the basic domain structure of 4 | # the system. These classes are barely smart data, and they explicitly lack the 5 | # functionality that is peculiar to support of any particular use case. These 6 | # classes commonly encapsulate the physical storage of the data. These data 7 | # implement an information structure that comes from the mental model of end 8 | # users, domain experts, programmers, and other people in the system. They may 9 | # correspond closely to the model objects of MVC. 10 | # 11 | # An example of a data object could be a bank account. Its interface would have 12 | # basic operations for increasing and decreasing the balance and for inquiring 13 | # about the current balance. The interface would likely not offer operations that 14 | # involve transactions, or which in any way involve other objects or any user 15 | # interaction. So, for example, while a bank account may offer a primitive for 16 | # increasing the balance, it would have no method called deposit. Such operations 17 | # belong instead in the interaction part of DCI. 18 | # 19 | # Data objects are instances of classes that might come from domain-driven design, 20 | # and such classes might use subtyping relationships to organize domain data. 21 | # Though it reduces to classes in the end, DCI reflects a computational model 22 | # dominated by object thinking rather than class thinking. Therefore, when 23 | # thinking "data" in DCI, it means thinking more about the instances at run time 24 | # than about the classes from which they were instantiated. 25 | # 26 | # class Account 27 | # def initialize(accountId) 28 | # @account_id = accountId 29 | # @balance = 0 30 | # end 31 | # def account_id 32 | # @account_id 33 | # end 34 | # def available_balance 35 | # @balance 36 | # end 37 | # def increase_balance(amount) 38 | # @balance += amount 39 | # end 40 | # def decrease_balance(amount) 41 | # @balance -= amount 42 | # end 43 | # end 44 | # 45 | # @todo Should objects track the roles in which they are presently particiapting? 46 | class Object 47 | end 48 | -------------------------------------------------------------------------------- /lib/dci/role.rb: -------------------------------------------------------------------------------- 1 | module DCI 2 | 3 | # Interaction - The Interaction is "what the system does." The Interaction 4 | # is implemented as Roles which are played by objects at run time. These objects 5 | # combine the state and methods of a Data (domain) object with methods (but no 6 | # state, as Roles are stateless) from one or more Roles. In good DCI style, 7 | # a Role addresses another object only in terms of its (methodless) Role. There 8 | # is a special Role called `@self` which binds to the object playing the current 9 | # Role. Code within a Role method may invoke a method on `@self` and thereby 10 | # invoke a method of the Data part of the current object. One curious aspect 11 | # of DCI is that these bindings are guaranteed to be in place only at run time 12 | # (using a variety of approaches and conventions; C++ templates can be used to 13 | # guarantee that the bindings will succeed). This means that Interactions—the 14 | # Role methods—are generic. In fact, some DCI implementations use generics or 15 | # templates for Roles. 16 | # 17 | # A Role is a stateless programming construct that corresponds to the end user's 18 | # mental model of some entity in the system. A Role represents a collection of 19 | # responsibilities. Whereas vernacular object-oriented programming speaks of 20 | # objects or classes as the loci of responsibilities, DCI ascribes them to Roles. 21 | # An object participating in a use case has responsibilities: those that it takes 22 | # on as a result of playing a particular Role. 23 | # 24 | # In the money transfer use case, for example, the role methods in the 25 | # SourceAccount and DestinationAccount enact the actual transfer. 26 | # 27 | # class Account::TransferWithdraw < Role 28 | # def transfer(amount) 29 | # decrease_balance(amount) 30 | # log "Tranfered from account #{account_id} $#{amount}" 31 | # end 32 | # end 33 | # 34 | # class Account::TransferDepoit < Role 35 | # def transfer(amount) 36 | # increase_balance(amount) 37 | # log "Tranfered into account #{account_id} $#{amount}" 38 | # end 39 | # end 40 | # 41 | class Role 42 | 43 | # 44 | def initialize(player) 45 | @self = player 46 | end 47 | 48 | # 49 | # @todo Should use #public_send? 50 | def method_missing(s, *a, &b) 51 | @self.__send__(s, *a, &b) 52 | end 53 | 54 | end 55 | 56 | end 57 | -------------------------------------------------------------------------------- /lib/dci/context.rb: -------------------------------------------------------------------------------- 1 | module DCI 2 | 3 | # Context - The Context is the class (or its instance) whose code includes the roles 4 | # for a given algorithm, scenario, or use case, as well as the code to map these 5 | # roles into objects at run time and to enact the use case. Each role is bound 6 | # to exactly one object during any given use case enactment; however, a single 7 | # object may simultaneously play several roles. A context is instantiated at the 8 | # beginning of the enactment of an algorithm, scenario, or use case. In summary, 9 | # a Context comprises use cases and algorithms in which data objects are used 10 | # through specific Roles. 11 | # 12 | # Each context represents one or more use cases. A context object is instantiated 13 | # for each enactment of a use case for which it is responsible. Its main job 14 | # is to identify the objects that will participate in the use case and to 15 | # assign them to play the Roles which carry out the use case through their 16 | # responsibilities. A role may comprise methods, and each method is some small 17 | # part of the logic of an algorithm implementing a use case. Role methods run 18 | # in the context of an object that is selected by the context to play that role 19 | # for the current use case enactment. The role-to-object bindings that take place 20 | # in a context can be contrasted with the polymorphism of vernacular object-oriented 21 | # programming. The overall business functionality is the sum of complex, dynamic 22 | # networks of methods decentralized in multiple contexts and their roles. 23 | # 24 | # Each context is a scope that includes identifiers that correspond to its roles. 25 | # Any role executing within that context can refer to the other roles in that 26 | # context through these identifiers. These identifiers have come to be called 27 | # methodless roles. At use case enactment time, each and every one of these 28 | # identifiers becomes bound to an object playing the corresponding Role for 29 | # this Context. 30 | # 31 | # An example of a context could be a wire transfer between two accounts, 32 | # where data models (the banking accounts) are used through roles named 33 | # SourceAccount and # DestinationAccount. 34 | # 35 | # class Account::Transfer < Context 36 | # role :source_account => Account::TransferWithdraw 37 | # role :destination_account => Account::TransferDeposit 38 | # 39 | # def initialize(source_account, destination_account) 40 | # self.source_account = source_account 41 | # self.destination_account = destination_account 42 | # end 43 | # 44 | # def transfer(amount) 45 | # roles.each{ |role| role.transfer(amount) } 46 | # end 47 | # end 48 | # 49 | class Context 50 | 51 | # Define a role given the name the role will use in this context, 52 | # and the role class that is to be played. 53 | # 54 | def self.role(name_to_role) 55 | @roles = nil # reset 56 | 57 | name_to_role.each do |name, role| 58 | define_method("role_#{name}"){ role } 59 | 60 | module_eval %{ 61 | def #{name}=(data) 62 | @#{name} = role_#{name}.new(data) 63 | end 64 | 65 | def #{name} 66 | @#{name} 67 | end 68 | } 69 | end 70 | end 71 | 72 | # Return a list of the names of defined roles. 73 | # 74 | # TODO: Consider private vs public roles. 75 | def self.roles 76 | @roles ||= ( 77 | list = [] 78 | instance_methods.each do |name| 79 | next unless name.to_s.start_with?('role_') 80 | list << name.to_s[5..-1].to_sym 81 | end 82 | list 83 | ) 84 | end 85 | 86 | # The default contructor can be used to assign roles via 87 | # a settings hash. 88 | # 89 | def initialize(settings={}) 90 | settings.each do |k,v| 91 | __send__("#{k}=", v) 92 | end 93 | end 94 | 95 | # Return Array of all role instances in the context. 96 | # 97 | def roles 98 | self.class.roles.map do |name| 99 | __send__(name) 100 | end 101 | end 102 | 103 | end 104 | 105 | end 106 | -------------------------------------------------------------------------------- /.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'yaml' 4 | 5 | module DotRuby 6 | 7 | # 8 | class GemSpec 9 | 10 | # For which revision of .ruby is this gemspec intended? 11 | REVISION = 0 unless defined?(REVISION) 12 | 13 | # 14 | PATTERNS = { 15 | :bin_files => 'bin/*', 16 | :lib_files => 'lib/{**/}*.rb', 17 | :ext_files => 'ext/{**/}extconf.rb', 18 | :doc_files => '*.{txt,rdoc,md,markdown,tt,textile}', 19 | :test_files => '{test/{**/}*_test.rb,spec/{**/}*_spec.rb}' 20 | } unless defined?(PATTERNS) 21 | 22 | # 23 | def self.instance 24 | new.to_gemspec 25 | end 26 | 27 | attr :metadata 28 | 29 | attr :manifest 30 | 31 | # 32 | def initialize 33 | @metadata = YAML.load_file('.ruby') 34 | @manifest = Dir.glob('manifest{,.txt}', File::FNM_CASEFOLD).first 35 | 36 | if @metadata['revision'].to_i != REVISION 37 | warn "You have the wrong revision. Trying anyway..." 38 | end 39 | end 40 | 41 | # 42 | def scm 43 | @scm ||= \ 44 | case 45 | when File.directory?('.git') 46 | :git 47 | end 48 | end 49 | 50 | # 51 | def files 52 | @files ||= \ 53 | #glob_files[patterns[:files]] 54 | case 55 | when manifest 56 | File.readlines(manifest). 57 | map{ |line| line.strip }. 58 | reject{ |line| line.empty? || line[0,1] == '#' } 59 | when scm == :git 60 | `git ls-files -z`.split("\0") 61 | else 62 | Dir.glob('{**/}{.*,*}') # TODO: be more specific using standard locations ? 63 | end.select{ |path| File.file?(path) } 64 | end 65 | 66 | # 67 | def glob_files(pattern) 68 | Dir.glob(pattern).select { |path| 69 | File.file?(path) && files.include?(path) 70 | } 71 | end 72 | 73 | # 74 | def patterns 75 | PATTERNS 76 | end 77 | 78 | # 79 | def executables 80 | @executables ||= \ 81 | glob_files(patterns[:bin_files]).map do |path| 82 | File.basename(path) 83 | end 84 | end 85 | 86 | def extensions 87 | @extensions ||= \ 88 | glob_files(patterns[:ext_files]).map do |path| 89 | File.basename(path) 90 | end 91 | end 92 | 93 | # 94 | def name 95 | metadata['name'] || metadata['title'].downcase.gsub(/\W+/,'_') 96 | end 97 | 98 | # 99 | def to_gemspec 100 | Gem::Specification.new do |gemspec| 101 | gemspec.name = name 102 | gemspec.version = metadata['version'] 103 | gemspec.summary = metadata['summary'] 104 | gemspec.description = metadata['description'] 105 | 106 | metadata['authors'].each do |author| 107 | gemspec.authors << author['name'] 108 | 109 | if author.has_key?('email') 110 | if gemspec.email 111 | gemspec.email << author['email'] 112 | else 113 | gemspec.email = [author['email']] 114 | end 115 | end 116 | end 117 | 118 | gemspec.licenses = metadata['copyrights'].map{ |c| c['license'] }.compact 119 | 120 | metadata['requirements'].each do |req| 121 | name = req['name'] 122 | version = req['version'] 123 | groups = req['groups'] || [] 124 | 125 | case version 126 | when /^(.*?)\+$/ 127 | version = ">= #{$1}" 128 | when /^(.*?)\-$/ 129 | version = "< #{$1}" 130 | when /^(.*?)\~$/ 131 | version = "~> #{$1}" 132 | end 133 | 134 | if groups.empty? or groups.include?('runtime') 135 | # populate runtime dependencies 136 | if gemspec.respond_to?(:add_runtime_dependency) 137 | gemspec.add_runtime_dependency(name,*version) 138 | else 139 | gemspec.add_dependency(name,*version) 140 | end 141 | else 142 | # populate development dependencies 143 | if gemspec.respond_to?(:add_development_dependency) 144 | gemspec.add_development_dependency(name,*version) 145 | else 146 | gemspec.add_dependency(name,*version) 147 | end 148 | end 149 | end 150 | 151 | # convert external dependencies into a requirements 152 | if metadata['external_dependencies'] 153 | ##gemspec.requirements = [] unless metadata['external_dependencies'].empty? 154 | metadata['external_dependencies'].each do |req| 155 | gemspec.requirements << req.to_s 156 | end 157 | end 158 | 159 | # determine homepage from resources 160 | homepage = metadata['resources'].find{ |key, url| key =~ /^home/ } 161 | gemspec.homepage = homepage.last if homepage 162 | 163 | gemspec.require_paths = metadata['load_path'] || ['lib'] 164 | gemspec.post_install_message = metadata['install_message'] 165 | 166 | # RubyGems specific metadata 167 | gemspec.files = files 168 | gemspec.extensions = extensions 169 | gemspec.executables = executables 170 | 171 | if Gem::VERSION < '1.7.' 172 | gemspec.default_executable = gemspec.executables.first 173 | end 174 | 175 | gemspec.test_files = glob_files(patterns[:test_files]) 176 | 177 | unless gemspec.files.include?('.document') 178 | gemspec.extra_rdoc_files = glob_files(patterns[:doc_files]) 179 | end 180 | end 181 | end 182 | 183 | end #class GemSpec 184 | 185 | end 186 | 187 | DotRuby::GemSpec.instance 188 | --------------------------------------------------------------------------------