Using a Generic Domain

Using a Generic Domain

10.Oct.2021

Title: Using a domain

The generic domain is an interface. It's an abstract class which forces its subclasses to implement certain methods required for the model to work, but it also provides lots of utility functionality that can be used across multiple models. This means that throughout your application, you only have one implementation of this domain that needs to be tested and debugged, not many.

This article will walk you through how I implemented my own version of the generic domain. The reason why I built it this way was so I could share some common code across different parts of my application without having to worry about altering or breaking anything else in my application. You'll see what I mean later on when we write our first test case using the not-so-generic domain.

First things first: we need to create a directory and new file which will contain the generic domain: $ mkdir lib/model $ touch lib/model/generic_domain.rb

Next, let's build the base of our GenericDomain class, which is explained below:

require 'ostruct' require 'json' module Model::GenericDomain def self . build new ( name ) end class << self def initialize ( *args ) @instance ||= new ( *args ) end def [] ( klass ) @instance [ klass ] end def method_missing ( *args , &block ) if JSON . parse ( args . first ). has_key? "method" return super elsif block_given? return yield else super end end alias :method :send def valid_values ( *args ) validate ( *args ) unless args . first == 'validate' self end def self . extend class << self ; @extended = true ; attr_reader :extended; alias extended update; def initialize ( *name , &block ); super (); @name = name ; instance_eval (& block ); extended = true ; end func += method if extended && !alias.blank? func -= method if extended && !alias.blank? yield ensure super end private # Ensure the model(s) are loaded before working on them, to avoid issues # with circular dependencies or multiple loading of models. For example, # if the GenericDomain is loaded before a model that it depends on, # then that model will not be available in the current context. This could # result in errors when trying to work with dependent models. def ensure super if self . class . name == "GenericDomain" load_dependent_models end end def load_dependent_models @instance . load end end alias :extend extend protected :extend private :extend end end

The most complicated part is the initialize method, which takes a name argument and creates a new instance of itself by calling build passing this name parameter. Then initialize sets up some default values for our other object properties (i.e., valid_values , send , etc.) and calls extended = true so that the extend method can access these methods.

The reference to @name is used to set the internal variable name . It uses an instance_eval(&block) so that the block passed in will act as if it's running within our GenericDomain object, but without affecting any other objects (i.e., nothing gets changed outside of our GenericDomain object).

Next we define some required and optional properties for our generic domain:

- build_with - A class / module that implements our :build_with instance method; this will be responsible for actually creating instances of classes it has been given as a type. We have not created this yet, but we will do so shortly. - create_with - A class / module that implements our :create_with instance method; this will be responsible for creating instances of classes it has been given as a type. We have not created this yet, but we will do so shortly. - default_values - An array containing default values for each type of class our GenericDomain should be capable of building / creating. These are used by the valid_values method to return an array that contains all the possible parameter combinations that can be passed to our generic domain's constructor via :build or :create .

- validate - A Proc that takes an argument representing one potential value for each property (i.e., all properties defined in valid_values ) and returns true if the argument is acceptable, false otherwise. If the hash keys match up with valid_values , this means a potential value for a property within the hash has been passed in. We will discuss how these values are obtained shortly.

- default - An object containing all of the default values for our generic domain's properties. This is used by our model to set an instance variable equal to it, so that we can retrieve them later on via @name .

We also defined three methods: self.method_missing(), self+func() and self+=func(). I won't go into too much detail about these here, but essentially they allow us to use method names originating from one of our model classes (i.e., initialize , create_with , build_with , etc as if they were coming from our GenericDomain .

The final method we define is the ensure method, which allows us to determine whether or not all of our models have been fully loaded. This is done mainly so that we can ensure @name has been set properly before doing any other work on our object (i.e., sending messages, etc). The initialize() and build_with() methods would not be able to work if this was not the case.

Then we define a private method called load_dependent_models(). This simply calls ensure if self.class.name == "GenericModel" and then sets the instance variable @instance to whatever value it finds there (which should be an instance of a model class). Finally, we define some regular methods for adding, retrieving and removing items from the values hash.

The next part is the build_with() method, which first calls ensure on our object to ensure that valid @models have been passed in. Then it gets a reference to all of the types that are used by our models (i.e., :types) and iterates through them calling each type's new method with self as an argument. After this has finished, it sets self+instance to be an instance of self . Finally, if self+build_with has not been set yet, it sets this value to some code block that simply returns an instance of model(s) instantiated using some parameters passed into build_with().

We are social