Created
October 14, 2014 22:31
-
-
Save ipoval/d91d3153af4d84db5c69 to your computer and use it in GitHub Desktop.
null object dp
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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