Skip to content

Instantly share code, notes, and snippets.

@javierfernandes
Created November 21, 2018 12:40
Show Gist options
  • Select an option

  • Save javierfernandes/1f6bee63a2d224ab7f9cfa54fda4aa90 to your computer and use it in GitHub Desktop.

Select an option

Save javierfernandes/1f6bee63a2d224ab7f9cfa54fda4aa90 to your computer and use it in GitHub Desktop.
Metaprogramacion - Transacciones de objetos (Parte 2 & 4)
class Transactor
def self.perform(*args, &bloque)
t = Transaction.new(args)
begin
bloque.call(*args)
t.commit()
rescue Exception => e
t.undo()
raise e
end
t
end
end
# State related extensions to all objects
class Object
def takeState
ObjectState.new(
self,
instance_variables.collect do | varName |
StateVariable.new(self, varName, instance_variable_get(varName))
end
)
end
def restoreState(state)
state.variables.each { |var| instance_variable_set(var.name, var.value) }
end
end
#
# Transaction model
#
class Transaction
def initialize(objects)
@objects = objects
@copies = copyState(objects)
end
def copyState(objects)
objects.map &:takeState
end
def commit
@changes = @copies.flat_map &:changes
end
def undo
@copies.each_with_index { |state, i| @objects[i].restoreState(state) }
end
def changes
@changes
end
end
class ObjectState
attr_accessor :object
attr_accessor :variables
def initialize(object, variables)
@object = object
@variables = variables
end
def changes
variables.reduce([]) do |acc, variableState|
current = variableState.getFrom(@object)
initial = variableState.value
if (current != initial)
acc.push([@object.object_id, variableState.name, initial, current])
end
acc
end
end
end
class StateVariable
attr_accessor :object
attr_accessor :name
attr_accessor :value
def initialize(object, name, value)
self.object = object
self.name = name
self.value = value
end
def getFrom(object)
object.instance_variable_get(@name)
end
end
class Transactor2
def self.perform(&bloque)
startTransaction
begin
bloque.call
currentTransaction.commit
currentTransaction
rescue Exception => e
raise e
ensure
clearTransaction
end
end
def self.startTransaction
Thread.current[:transaction] = Transaction2.new
end
def self.currentTransaction
Thread.current[:transaction] || NoopTransaction
end
def self.clearTransaction
Thread.current[:transaction] = nil
end
end
# Usage API (open class)
class Object
def self.transactional
def self.attr_accessor(*names)
names.each do |name|
variableName = "@#{name}"
define_method name do
currentTransaction.read(self, variableName)
end
define_method "#{name}=" do |newValue|
currentTransaction.write(self, variableName, newValue)
end
end
end
define_method :currentTransaction do
Transactor2.currentTransaction
end
end
end
#
# Transaction Object Model
#
# La no-transaccion, delega en al variable de instancia del objeto
# (comportamiento default de ruby)
class NoopTransaction
def self.read(object, name)
object.instance_variable_get(name)
end
def self.write(object, name, value)
object.instance_variable_set(name, value)
end
def changes
[]
end
end
class Transaction2
def initialize
@states = Hash.new
@commited = false
end
def commit
if (not @commited)
applyValues(:after)
@commited = true
end
end
def undo
if (@commited)
applyValues(:before)
@commited = false
end
end
# Aplica el estado a los objetos
# Puede ser aplicar el valor original (:before) o el valor luego de la transacción (:after)
def applyValues(which)
@states.each_pair do |id, state|
state.applyValues(which)
end
end
# interceptors
def read(object, name)
getStateFor(object).read(name)
end
def write(object, name, value)
state = getStateFor(object)
state.write(name, value)
end
def getStateFor(object)
state = @states[object.object_id] || ObjectTransactionalState.new(object)
@states[object.object_id] = state
end
def changes
@states.keys.inject([]) do |acc, id|
@states[id].collectChanges(acc)
end
end
end
class ObjectTransactionalState
attr_accessor :object, :state
def initialize(object)
self.object = object
self.state = Hash.new
end
def write(name, value)
if (@state.has_key? name)
@state[name].after = value
else
previousValue = _readFromObject(name)
@state[name] = ObjectVariable.new(@object, name, previousValue, value)
end
end
def read(name)
if (@state[name])
@state[name].after
else
_readFromObject(name)
end
end
def _readFromObject(name)
@object.instance_variable_get(name)
end
def applyValues(which)
@state.each_value do |variable|
variable.applyValue(which)
end
end
def collectChanges(acc)
@state.values.each do |variable|
acc.push(variable.change)
end
acc
end
end
# Representa el estado de una variable de instancia en cierto objeto
# before = valor inicial / original
# after = valor actual en esta transaccion / final (si ya terminó)
class ObjectVariable
attr_accessor :object, :name, :before, :after
def initialize(object, name, before, after)
self.object = object
self.name = name
self.before = before
self.after = after
end
# Commitea uno de los valores en el objeto. Util para commitear / rollbackear
# which = :before | :after
def applyValue(which)
@object.instance_variable_set(@name, self.send(which))
end
def change
[@object.object_id, @name.to_sym, @before, @after]
end
end
require 'rspec'
require_relative '../src/transactor_parte2'
describe 'transactor' do
class Persona
transactional
attr_accessor :nombre, :edad
def initialize(nombre, edad)
self.nombre = nombre
self.edad = edad
end
def cumplirAnios
self.edad = self.edad + 1
end
end
it 'ejecuta el bloque exitosamente' do
persona = Persona.new("Lanata", 44)
Transactor2.perform do
persona.cumplirAnios()
end
expect(persona.edad).to eq(45)
end
it 'deshace cambios a variables de instancia si el bloque arroja exception' do
p = Persona.new("Lanata", 44)
expect {
Transactor2.perform do
p.cumplirAnios()
raise "Se pudrio todo, aca nadie cumple nada"
end
}.to raise_error("Se pudrio todo, aca nadie cumple nada")
expect(p.edad).to eq(44)
end
it "perform retorna un objeto transaccion que se puede deshacer" do
p = Persona.new("ponyo", 22)
transaction = Transactor2.perform do
p.cumplirAnios()
end
expect(p.edad).to eq 23 # anduvo !
transaction.undo()
expect(p.edad).to eq 22 # la deshizo !
end
it "captura los cambios en la transaccion" do
p = Persona.new("ponyo", 22)
transaction = Transactor2.perform {
p.cumplirAnios()
}
expect(transaction.changes()).to eq [[p.object_id, :@edad, 22, 23]]
end
end
require 'rspec'
require_relative '../src/transactor'
describe 'transactor' do
class Persona
attr_accessor :nombre
attr_accessor :edad
def initialize(nombre, edad)
self.nombre = nombre
self.edad = edad
end
def cumplirAnios
self.edad = self.edad + 1
end
end
it 'ejecuta el bloque con el parametro dado' do
persona = Persona.new("Lanata", 44)
Transactor.perform(persona) do |p|
p.cumplirAnios()
end
expect(persona.edad).to eq(45)
end
it 'deshace cambios a variables de instancia si el bloque arroja exception' do
persona = Persona.new("Lanata", 44)
expect {
Transactor.perform(persona) do |p|
p.cumplirAnios()
raise "Se pudrio todo, aca nadie cumple nada"
end
}.to raise_error("Se pudrio todo, aca nadie cumple nada")
expect(persona.edad).to eq(44)
end
it "perform retorna un objeto transaccion que se puede deshacer" do
p = Persona.new("ponyo", 22)
transaction = Transactor.perform(p) { |p|
p.cumplirAnios()
}
expect(p.edad).to eq 23 # anduvo !
transaction.undo()
expect(p.edad).to eq 22 # la deshizo !
end
it "captura los cambios en la transaccion" do
p = Persona.new("ponyo", 22)
transaction = Transactor.perform(p) { |p|
p.cumplirAnios()
}
expect(transaction.changes()).to eq [[p.object_id, :@edad, 22, 23]]
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment