of APIs Text Text • Why should we care about our interfaces? • What can we learn about APIs from a DSL? • How do we use APIs without thinking? 7 Monday, June 10, 13
• An Adaptive Domain Language Interpreter • Focus is shifted to the messages • Implementation changes based on a dialect • Messages are transformed 24 Monday, June 10, 13
• An Adaptive Domain Language Interpreter • Focus is shifted to the messages • Implementation changes based on a dialect • Messages are transformed Message-Driven Design 25 Monday, June 10, 13
work? • Language adaptation with Dependency Injection • Abstraction of external dependencies via proxy • Visitor pattern to apply commands Message-Driven Design 30 Monday, June 10, 13
class DefaultDialect < Dialect configure do |c| c.command_alias :select, "show" c.command_alias :create, "new" c.define :order, { desc: "most_recent" } c.define :business_objects, ["articles", "posts"] end end end Teach the TARDIS your dialect... 32 Monday, June 10, 13
class Dialect def self.configure &block @@config = Config.new yield @@config end def self.config @@config end end end Learn through configuration... 33 Monday, June 10, 13
class Tardis attr_reader :dialect_klass def initialize(dialect = "Tardis::DefaultDialect") self.dialect = dialect end def namespace Module.nesting.last end def dialect=(klass_string) if namespace.const_defined?(klass_string.split("::").last) @dialect_klass = klass_string.constantize end end def dialect @dialect_klass end def config dialect_klass.config end end end ...pass the knowledge in 34 Monday, June 10, 13
class Config ## Trimmed to fit def includes_command?(key) @commands.include?(key) end def includes_definition?(key) @dictionary.include?(key) end # Handle requests for configuration values by key def method_missing(meth, *args, &block) if includes_definition?(meth) return @dictionary[meth] elsif includes_command?(meth) return @commands[meth] else super end end end end ...and ask for a translation 35 Monday, June 10, 13
class Config ## Trimmed to fit def includes_command?(key) @commands.include?(key) end def includes_definition?(key) @dictionary.include?(key) end # Handle requests for configuration values by key def method_missing(meth, *args, &block) if includes_definition?(meth) return @dictionary[meth] elsif includes_command?(meth) return @commands[meth] else super end end end end > t = Tardis.new("Tardis::DefaultDialect") > puts t.config.select => "show" 36 Monday, June 10, 13
module Arel class Table attr_accessor :proxy_table def initialize(business_object) self.proxy_table = ::Arel::Table.new(business_object) end def method_missing(meth, *args, &block) if block_given? proxy_table.send(meth, args, block) else proxy_table.send(meth, args) end end def [](key) proxy_table.send(:[], key) end end end end Wrap the dependency and... 38 Monday, June 10, 13
do Arel::Table.engine = Arel::Sql::Engine.new(FakeConnection::Base.new) Given(:table) { Tardis::Arel::Table.new("articles") } context "initialize Table with business object" do Then { expect(table.proxy_table).to be_a(Arel::Table) } And { expect(table.proxy_table.name).to eq("articles") } describe "#order" do When(:query) { table.order(:created_date) } Then { query.to_sql.should =~ /^*ORDER BY 'created_date'$/ } end describe "#take" do When(:query) { table.take(3) } Then { query.to_sql.should =~ /^*LIMIT 3$/ } end end context "chain methods to build query" do When(:query) { table.order(:created_date).take(3) } Then { query.to_sql.should =~ /^*ORDER BY 'created_date' LIMIT 3$/ } end end Maintain API through the proxy 39 Monday, June 10, 13
class TableCommand attr_accessor :command, :config def initialize(command, config) @command = command @config = config end def accept(visitor) visitor.visit(self) end def method_missing(meth, *args, &block) if meth =~ /^is_(.+)$/ self.is_a?("Tardis::#{$1.gsub("?","").classify}".constantize) else super end end end end A command accepts visitors... 42 Monday, June 10, 13
class TableVisitor def self.visit(visitor) extract_table_from_command(visitor) do |table| visitor.table = ::Tardis::Arel::Table.new(table) end end def self.extract_table_from_command(visitor, &block) visitor.config.business_objects.detect do |table| if visitor.command =~ Regexp.new("(#{table})") && visitor.is_table_command? block_given? ? yield(table) : table end end end end end but only affected when needed 43 Monday, June 10, 13
## Trimmed to fit def apply_visitors_to(command) namespace.constants.each do |visitor| command_accepts_visitor(command, visitor_constant(visitor)) end end def visitor_constant(visitor) "Tardis::#{visitor}".constantize end def command_accepts_visitor(command, visitor) command.accept(visitor) if visitor.respond_to?(:visit) end def method_missing(meth, *args, &block) if meth =~ /#{config.commands[:select]}/ cmd = TableCommand.new(meth, config) apply_visitors_to(cmd) else super end end end The Implementation 44 Monday, June 10, 13
want to believe everything you see ActiveRecord is an intuitive interface • Business Objects are decoupled from data • It responds to a common dialect 50 Monday, June 10, 13