Skip to content

Instantly share code, notes, and snippets.

@ipoval
Created October 14, 2014 22:31
Show Gist options
  • Save ipoval/d91d3153af4d84db5c69 to your computer and use it in GitHub Desktop.
Save ipoval/d91d3153af4d84db5c69 to your computer and use it in GitHub Desktop.
null object dp
# encoding: utf-8
# Null-Object pattern.
# - has no state by definition and can be Singleton dp
# Prevent code from cluttered if condition checks to find that object is present and is not nil.
# Naught class also has a good examples of ruby conversion operators
class Georgian
def make_it_so(logger = nil)
# HAVING THIS PROBLEM:
# case for NullObject pattern
logger && logger.info('message')
logger && logger.warn('message')
end
end
# refactoring 1
class NullLogger
def debug(*); end
def info(*); end
def warn(*); end
def error(*); end
def fatal(*); end
end
class Georgian
def make_it_so(logger = NullLogger.new)
logger.info('message')
logger.warn('message')
end
end
# Generic Null Object
# Derive from BasicObject and not from Object
# since we want to catch also methods of Object like #to_s, #class, ...
class NullObject < BasicObject
def method_missing(*args, &block); nil; end
# normally we would define respond_to_missing? method, but since we inherit from BasicObject, it's ok it define respond_to?, since it does not have
# a default implementation from Object class
def respond_to?(*); true; end
# this is hook method for many utility methods in ruby that help to debug and print the object
def inspect; '<null>'; end
# should know its own class
klass = self
define_method(:class) { klass }
def nil?; true; end
end
describe NullObject do
subject(:null) { NullObject.new }
describe '#nil?' do
specify('true') { expect(null).to be_nil }
end
describe '#to_s handle Object methods' do
specify('return nil for Object methods by deriving from BasicObject') do
expect(null.to_s).to be_nil
end
end
describe 'responds to #random_method method and returns nil' do
specify 'returns nil for any message' do
expect(null.public_send(:random_method)).to be_nil
end
specify 'accepts any argument for any message and returns nil' do
expect(null.ranmod_method(1, ['args'])).to be_nil
end
describe '#respond_to? method returns true for any method call' do
specify 'responds true that it responds to any message' do
expect(null.respond_to?(:info)).to be true
end
end
end
describe '#inspect' do
specify('meaningful debugging information') { expect(null.inspect).to eq '<null>' }
end
describe '#class' do
specify('should know its own class') { expect(null.class).to eq NullObject }
end
end
# \Generic Null Object
#########################################################################################
# Generating NullObject classes
class Naught
def self.build(&block)
null_class_builder = NullClassBuilder.new
yield null_class_builder if block_given?
unless null_class_builder.interface_defined?
null_class_builder.respond_to_missing_with_nil
end
null_class_builder.generate_class
end
class NullClassBuilder
def initialize
@base_class = BasicObject
@interface_defined = false
@operations = []
@inspect_proc = -> { '<null>' }
end
def generate_class
null_class = Class.new(@base_class)
define_basic_methods
@operations.each { |op| op.call(null_class) }
null_class
end
def define_basic_methods
defer do |subject|
# make local var to be accesssible in module_eval block
inspect_proc = @inspect_proc
subject.module_eval do
define_method(:inspect, &inspect_proc)
klass = self
define_method(:class) { klass }
def nil?; true; end
end
end
end
def defer(&deferred_operation)
@operations << deferred_operation
end
def interface_defined?; @interface_defined; end
def respond_to_missing_with_nil
defer do |subject|
subject.module_eval do
def method_missing(*args, &block); nil; end
def respond_to?(*); true; end
end
end
@interface_defined = true
end
def define_explicit_conversions
defer do |subject|
subject.module_eval do
def to_s; ''; end
def to_i; 0; end
def to_f; 0.0; end
def to_a; []; end
def to_c; 0.to_c; end
def to_r; 0.to_r; end
def to_h; {}; end
end
end
end
def define_implicit_conversions
defer do |subject|
subject.module_eval do
# used in implicit type conversions in array-slat operations like a, b = [], so you can a, b = null_object
def to_ary; []; end
# used in implicit type coversions like eval(null_object)
def to_str; ''; end
end
end
end
def singleton
defer do |subject|
require 'singleton'
subject.module_eval do
include Singleton
end
end
end
def chaining
defer do |subject|
subject.class_eval do
def method_missing(*args, &block); self; end
def respond_to?(*); true; end
end
end
@interface_defined = true
end
# Mimicing the interface of another class
def mimic(class_to_mimic)
@base_class = root_class_of(class_to_mimic)
@inspect_proc = -> { Kernel.format '<null::%s>', class_to_mimic }
defer do |subject|
subject.module_eval do
# get only its own instance methods without instance methods of ancestor classes
class_to_mimic.instance_methods(false).each { |meth|
define_method(meth) { |*| nil; }
}
end
end
@interface_defined = true
end
def root_class_of(klass)
klass.ancestors.include?(Object) ? Object : BasicObject
end
end
end
describe Naught, 'null_object_fabric_class' do
subject(:null) { null_class.new }
let(:null_class) do
Naught.build { |null_class_builder|
null_class_builder.define_explicit_conversions
null_class_builder.define_implicit_conversions
}
end
describe Naught.method(:build) do
specify 'anonymous null class returned' do
expect(null.class).to be null_class
end
end
describe 'explicit conversion methods' do
specify('#to_s') { expect(null.to_s).to eq '' }
specify('#to_i') { expect(null.to_i).to eq 0 }
specify('#to_f') { expect(null.to_f).to eq 0.0 }
specify('#to_a') { expect(null.to_a).to eq [] }
specify('#to_c') { expect(null.to_c).to eq Complex(0) }
specify('#to_r') { expect(null.to_r).to eq Rational(0) }
specify('#to_h') { expect(null.to_h).to eq({}) }
end
describe 'implicit conversions' do
specify('#to_str') { expect(null.to_str).to eq '' }
specify('#to_ary') { expect(null.to_ary).to eq [] }
end
describe 'singleton' do
let(:null_class) do
Naught.build { |null_class_builder|
null_class_builder.singleton
}
end
specify 'does not respond to .new' do
expect { null_class.new }.to raise_error
end
specify 'has only one instance' do
expect(null_class.instance).to be null_class.instance
end
specify 'can be cloned and duplicated - important since Singleton module will disable those' do
expect(null_class.instance.dup).to be_nil
expect(null_class.instance.copy).to be_nil
end
end
describe 'chaining api DP' do
let(:null_class) {
Naught.build { |null_class_builder| null_class_builder.chaining }
}
specify 'returns self and acts as blackwhole receiver of the chained messages' do
expect(null << 'word' << 'word').to be null
end
end
describe 'null object mimicking a class' do
class LibraryPatron
def member?; true; end
end
subject(:mimic_null) { mimic_null_class.new }
let(:mimic_null_class) { Naught.build { |b| b.mimic LibraryPatron } }
specify 'responds to all methods defined on the target class' do
expect(mimic_null.member?).to be_nil
expect(mimic_null.respond_to?(:member?)).to eq true
end
specify 'does not respond to methods not defined on target class' do
expect { mimic_null.undefined_method }.to raise_error NoMethodError
end
specify '#inspect has informative format' do
expect(mimic_null.inspect).to eq '<null::LibraryPatron>'
end
end
end
# /Generating NullObject classes
#########################################################################################
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment