Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up

Build maintainable Rails apps

License

NotificationsYou must be signed in to change notification settings

andypike/rectify

Repository files navigation

Code ClimateBuild StatusGem Version

Rectify is a gem that provides some lightweight classes that will make it easierto build Rails applications in a more maintainable way. It's built on top ofseveral other gems and adds improved APIs to make things easier.

Rectify is an extraction from a number of projects that use these techniques andproved to be successful.

Video

In June 2016, I spoke at RubyC about Rectify and how it can be used to improveareas of your application. The full video and slides can be found here:

Building maintainable Rails apps - RubyC 2016

Installation

To install, add it to yourGemfile:

gem"rectify"

Then use Bundler to install it:

bundle install

Overview

Currently, Rectify consists of the following concepts:

You can use these separately or together to improve the structure of your Railsapplications.

The main problem that Rectify tries to solve is where your logic should go.Commonly, business logic is either placed in the controller or the model and theviews are filled with too much logic. The opinion of Rectify is that theseplaces are incorrect and that your models in particular are doing too much.

Rectify's opinion is that controllers should just be concerned with HTTP relatedthings and models should just be concerned with data relationships. The problemthen becomes, how and where do you place validations, queries and other businesslogic?

Using Rectify, Form Objects contain validations and represent the data inputof your system. Commands then take a Form Object (as well as other data) andperform a single action which is invoked by a controller. Query objectsencapsulate a single database query (and any logic it needs). Presenters containthe presentation logic in a way that is easily testable and keeps your views asclean as possible.

Rectify is designed to be very lightweight and allows you to use some or all ofit's components. We also advise to use these components where they make sensenot just blindly everywhere. More on that later.

Here's an example controller that shows details about a user and also allows auser to register an account. This creates a user, sends some emails, does somespecial auditing and integrates with a third party system:

classUserController <ApplicationControllerincludeRectify::ControllerHelpersdefshowpresentUserDetailsPresenter.new(:user=>current_user)enddefnew@form=RegistrationForm.newenddefcreate@form=RegistrationForm.from_params(params)RegisterAccount.call(@form)doon(:ok){redirect_todashboard_path}on(:invalid){render:new}on(:already_registered){redirect_tologin_path}endendend

TheRegistrationForm Form Object encapsulates the relevant data that isrequired for the action and theRegisterAccount Command encapsulates thebusiness logic of registering a new account. The controller is clean andbusiness logic now has a natural home:

HTTP             => Controller   (redirecting, rendering, etc)Data Input       => Form Object  (validation, acceptable input)Business Logic   => Command      (logic for a specific use case)Data Persistence => Model        (relationships between models)Data Access      => Query Object (database queries)View Logic       => Presenter    (formatting data)

The next sections will give further details about using Form Objects, Commandsand Presenters.

Form Objects

The role of the Form Object is to manage the input data for a given action. Itvalidates data and only allows whitelisted attributes (replacing the need forStrong Parameters). This is a departure from "The Rails Way" where the modelcontains the validations. Form Objects help to reduce the weight of your modelsfor one, but also, in an app of reasonable complexity even simple things likevalidations become harder because context is important.

For example, you can add validation for aUser model but there are differentcontext where the validations change. When a user registers themselves you mighthave one set of validations, when an admin edits that user you might haveanother set, maybe even when a user edits themselves you may have a third. In"The Rails Way" you would have to have conditional validation in your model.With Rectify you can have a different Form Object per context and keep thingseasier to manage.

Form objects in Rectify are based onVirtusand make them compatible with Rails form builders, add ActiveModel validationsand all allow you to specify a model to mimic.

Here is how you define a form object:

classUserForm <Rectify::Formattribute:first_name,Stringattribute:last_name,Stringvalidates:first_name,:last_name,:presence=>trueend

You can then set that up in your controller instead of a normal ActiveRecordmodel:

classUsersController <ApplicationControllerdefnew@form=UserForm.newenddefcreate@form=UserForm.from_params(params)if@form.valid?# Do something interestingendendend

You can use the form object with form builders such assimple_form like this:

=simple_form_for@formdo |f|=f.input:first_name=f.input:last_name=f.submit

Mimicking models

When the form is generated it uses the name of the form class to infer what"model" it should mimic. In the example above, it will mimic theUser modelas it removes theForm suffix from the form class name by default.

The model being mimicked affects two things about the form:

  1. The route path helpers to use as the url to post to, for example:users_path.
  2. The parent key in the params hash that the controller receives, for exampleuser in this case:
params={"id"=>"1","user"=>{"first_name"=>"Andy","last_name"=>"Pike"}}

You might want to mimic something different and use a form object that is notnamed in a way where the correct model can be mimicked. For example:

classUserForm <Rectify::Formmimic:teacherattribute:first_name,Stringattribute:last_name,Stringvalidates:first_name,:last_name,:presence=>trueend

In this example we are using the sameUserForm class but am mimicking aTeacher model. The above form will then use the route path helpersteachers_path and the params key will beteacher rather thanusers_pathanduser respectively.

Attributes

You define your attributes for your form object just like you do inVirtus.

By default, Rectify forms include anid attribute for you so you don't need toadd that. We use thisid attribute to fulfill some of the requirements ofActiveModel so your forms will work with form builders. For example, your formobject has a#persisted? method. Your form object is never persisted sotechnically this should always returnfalse.

However, you are normally representing something that is persistable. So we usethe value ofid to workout if what this should return. Ifid is a numbergreater than zero then we assume it is persisted otherwise we assume it isn't.This is important as it affects where your form is posted (to the#create or#update action in your controller).

Populating attributes

There are a number of ways to populate attributes of a form object.

Constructor

You can use the constructor and pass it a hash of values:

form=UserForm.new(:first_name=>"Andy",:last_name=>"Pike")

Params hash

You can use the params hash that a Rails controller provides that contains allthe data in the request:

form=UserForm.from_params(params)

When populating from params we will populate the built inid attribute fromthe root of the params hash and populate the rest of the form attributes fromwithin the parent key. For example:

params={"id"=>"1","user"=>{"first_name"=>"Andy","last_name"=>"Pike"}}form=UserForm.from_params(params)form.id# => 1form.first_name# => "Andy"form.last_name# => "Pike"

The other thing to notice is that (thanks to Virtus), attribute values are castto the correct type. The params hash is actually all string based but when youget values from the form, they are returned as the correct type (seeidabove).

In addition to the params hash, you may want to add additional contextual data.This can be done by supplying a second hash to the.from_params method.Elements from this hash will be available to populate form attributes as if theywere under the params key:

form=UserForm.from_params(params,:ip_address=>"1.2.3.4")form.id# => 1form.first_name# => "Andy"form.last_name# => "Pike"form.ip_address# => "1.2.3.4"

Model

You can pass a Ruby object instance (which is normally an ActiveModelbut can be any PORO) to the form to populate it's attribute values. This is usefulwhen editing a model:

user=User.create(:first_name=>"Andy",:last_name=>"Pike")form=UserForm.from_model(user)form.id# => 1form.first_name# => "Andy"form.last_name# => "Pike"

This works by trying to match (deeply) the attributes of the form object with thepassed in object. If there is matching attribute or method in the model, thenwhatever it returns will be assigned to the form attribute.

This works great for most cases, but sometimes you need more control and need theability to do custom mapping from the model to the form. When this is required,you just need to implement the#map_model method in your form object:

classUserForm <Rectify::Formattribute:full_name,Stringdefmap_model(model)self.full_name="#{model.first_name}#{model.last_name}"endend

The#map_model method is called as part of.from_model after all the automaticattribute assignment is complete.

One important thing that is different about Rectify forms is that they are notbound to a model. You can use a model to populate the form's attributes but thatis all it will do. It does not keep a reference to the model or interact withit.

Rectify forms are designed to be lightweight representations of the data youwant to collect or show in your forms, not something that is linked to a model.This allows you to create any form that you like which doesn't need to match therepresentation of the data in the database.

JSON

You can also populate a form object from a JSON string. Just pass it in to the.from_json class method and the form will be created with the attributespopulated by matching names:

json=<<-JSON  {    "first_name": "Andy",    "age": 38  }JSONform=UserForm.from_json(json)form.first_name# => "Andy"form.age# => 38

Populating the form from JSON can be useful when dealing with API requests intoyour system. Which allows you to easily access data and perform validation ifrequired.

Validations

Rectify includesActiveModel::Validations for you so you can use all of theRails validations that you are used to within your models.

Your Form Object has a#valid? method that will validate the attributes ofyour form as well as any (deeply) nested form objects and array attributes thatcontain form objects. There is also an#invalid? method that returns theopposite of#valid?.

The#valid? and#invalid? methods also take a set of options. These options allowyou to not validate nested form objects or array attributes that contain form objects.For example:

classUserForm <Rectify::Formattribute:name,Stringattribute:address,AddressFormattribute:contacts,Array[ContactForm]validates:name,:presence=>trueendclassAddressForm <Rectify::Formattribute:street,Stringattribute:town,Stringattribute:city,Stringattribute:post_code,Stringvalidates:street,:post_code,:presence=>trueendclassContactForm <Rectify::Formattribute:name,Stringattribute:number,Stringvalidates:name,:presence=>trueendform=UserForm.from_params(params)form.valid?(:exclude_nested=>true,:exclude_arrays=>true)

In this case, theUserForm attributes will be validated (name in the example above)but theaddress andcontacts will not be validated.

Deep Context

It's sometimes useful to have some context within your form objects when performingvalidations or some other type of data manipulation of the input. For example, youmight want to check that the current user owns a particular resource as part of yourvalidations. You could add the current user as an additional contextual option asthe example shows above. However, sometimes you need this context to be availableat all levels within your form not just at the root form object. You might have nestedforms or arrays of form objects and they all might need access to this context. Asthere is no link up the chain from child to parent forms, we need a way to supplysome context and make it available to all child forms.

You can do that using the#with_context method.

form=UserForm.from_params(params).with_context(:user=>current_user)

This allows us to access#context in any form, and use the information withinit when we perform validations or other work:

classPostForm <Rectify::Formattribute:blog_id,Integerattribute:title,Stringattribute:body,Stringattribute:tags,Array[TagForm]validate:check_blog_ownershipdefcheck_blog_ownershipreturnifcontext.user.blogs.exists?(:id=>blog_id)errors.add(:blog_id,"not owned by this user")endendclassTagForm <Rectify::Formattribute:name,Stringattribute:category_id,Integervalidate:check_categorydefcheck_categoryreturnifcontext.user.categories.exists?(:id=>category_id)errors.add(:category_id,"not a category for this user")endend

The context is passed to all nested forms within a form object to make it easyto perform all the validations and data conversions you might need from withinthe form object without having to do this as part of the command.

Strong Parameters

Did you notice in the example above that there was no mention of StrongParameters. That's because with Form Objects you do not need strong parameters.You only specify attributes in your form that are allowed to be accepted. Allother data in your params hash is ignored.

Take a look atVirtus for more informationabout how to build a form object.

I18n

Regarding internationalization, the main affected classes when coercing areDate andTime classes. This is coercing Strings intoDate,DateTime andTime. Texts don't usually need to be coerced as they are simple String attributes with nothing special in them.

When coercing dates and times in a multi-language application, each locale will have its own date and time formats, and these formats should be taken into account when coercing strings (inputs entered by the user, or comming form external sources).

So forDate,DateTime andTime classes, Rectify does not support I18n by default. But there are some ways to achieve it indirectly.

Probably the best is to define customVirtus::Attributes for each kind of temporal class. For exmaple:

classLocalizedDate <Virtus::Attributedefcoerce(value)returnvalueunlessvalue.is_a?(String)Date.strptime(value,I18n.t("date.formats.short"))rescueArgumentErrornilendend

Commands

Commands (also known as Service Objects) are the home of your business logic.They allow you to simplify your models and controllers and allow them to focuson what they are responsible for. A Command should encapsulate a single usertask such as registering for a new account or placing an order. You of coursedon't need to put all code for this task within the Command, you can (andshould) create other classes that your Command uses to perform it's work.

With regard to naming, Rectify suggests using verbs rather than nouns forCommand class names, for exampleRegisterAccount,PlaceOrder orGenerateEndOfYearReport. Notice that we don't suffix commands withCommandorService or similar.

Commands in Rectify are based onWisperwhich allows classes to broadcast events for publish/subscribe capabilities.Rectify::Command is a lightweight class that gives an alternate API and addssome helper methods to improve Command logic.

The reason for using the pub/sub model rather than returning a result means thatwe can reduce the number of conditionals in our code as the outcome of a Commandmight be more complex than just success or failure.

Here is an example Command with the structure Rectify suggests (as seen in theoverview above):

classRegisterAccount <Rectify::Commanddefinitialize(form)@form=formenddefcallreturnbroadcast(:invalid)ifform.invalid?transactiondocreate_usernotify_adminsaudit_eventsend_user_details_to_crmendbroadcast(:ok)endprivateattr_reader:formdefcreate_user# ...enddefnotify_admins# ...enddefaudit_event# ...enddefsend_user_details_to_crm# ...endend

To invoke this Command, you would do the following:

defcreate@form=RegistrationForm.from_params(params)RegisterAccount.call(@form)doon(:ok){redirect_todashboard_path}on(:invalid){render:new}on(:already_registered){redirect_tologin_path}endend

What happens inside a Command?

When you call the.call class method, Rectify will instantiate a new instanceof the command and will pass the parameters to it's constructor, it will thencall the instance method#call on the newly created command object. The.call method also allows you to supply a block where you can handle the eventsthat may have been broadcast from the command.

The events that your Command broadcasts can be anything, Rectify suggests:okfor success and:invalid if the form data is not valid, but it's totally up toyou.

From here you can choose to implement your Command how you see fit. ARectify::Command only has to have the instance method#call.

Writing Commands

As your application grows and Commands get more complex we recommend using thestructure above. Within the#call method you first check that the input datais valid. If it is you then perform the various tasks that need to be completed.We recommend using private methods for each step that are well named which makesit very easy for anyone reading the code to workout what it does.

Feel free to use other classes and objects where appropriate to keep your codewell organized and maintainable.

Events

Just as inWisper, you fire events usingthebroadcast method. You can use any event name you like. You can also passparameters to the handling block:

# within the command:classRegisterAccount <Rectify::Commanddefcall# ...broadcast(:ok,user)endend# within the controller:defcreateRegisterAccount.call(@form)doon(:ok){ |user|logger.info("#{user.first_name} created")}endend

When an event is handled, the appropriate block is called in the context of thecontroller. Basically, any method call within the block is delegated back to thecontroller.

As well as capturing events in a block, the command will also return a hash of thebroadcast events together with any parameters that were passed. For example:

events=RegisterAccount.call(form)events# => { :ok => user }

There will be a key for each event broadcast and its value will be the parameterspassed. If there is a single parameter it will be the value. If there are noparameters or many, the hash value for the event key will be an array of the parameters:

events=RegisterAccount.call(form)events# => {#      :ok       => user,#      :messages => ["User registered", "Email sent", "Account ready"],#      :next     => []#    }

You may occasionally want to expose a value within a handler block to the view.You do this via theexpose method within the handler block. If you want touseexpose then you must include theRectify::ControllerHelpers module inyour controller. You pass a hash of the variables you wish to expose to the viewand they will then be available. If you have set a Presenter for the view thenexpose will try to set an attribute on that presenter. If there is noPresenter or the Presenter doesn't have a matching attribute thenexpose willset an instance variable of the same name. See below for more details aboutPresenters.

# within the controller:includeRectify::ControllerHelpersdefcreatepresentHomePresenter.new(:name=>"Guest")RegisterAccount.call(@form)doon(:ok){ |user|expose(:name=>user.name,:greeting=>"Hello")}endend
<!-- within the view: --><p><%=@greeting%><%=presenter.name%></p># =><p>Hello Andy</p>

Take a look atWisper for moreinformation around how to do publish/subscribe.

Presenters

A Presenter is a class that contains the presentational logic for your views.These are also known as an "exhibit", "view model", "view object" or just a"view" (Rails views are actually templates, but anyway). To avoid confusionRectify calls these classes Presenters.

It's often the case that you need some logic that is just for the UI. The samequestion comes up, where should this logic go? You could put it directly in theview, add it to the model or create a helper. Rectify's opinion is that all ofthese are incorrect. Instead, create a Presenter for the view (or component ofthe view) and place your logic here. These classes are easily testable andprovide a more object oriented approach to the problem.

To create a Presenter just derive off ofRectify::Presenter, add attributes asyou do for Form Objects usingVirtusattribute declaration. Inside a Presenter you have access to all view helpermethods so it's easy to move the presentation logic here:

classUserDetailsPresenter <Rectify::Presenterattribute:user,Userdefedit_linkreturn""unlessuser.admin?link_to"Edit#{user.name}",edit_user_path(user)endend

Once you have a Presenter, you typically create it in your controller and makeit accessible to your views. There are two ways to do that. The first way is tojust treat it as a normal class:

classUsersController <ApplicationControllerdefshowuser=User.find(params[:id])@presenter=UserDetailsPresenter.new(:user=>user).attach_controller(self)endend

You need to call#attach_controller and pass it a controller instance which willallow it access to the view helpers. You can then use the Presenter in yourviews as you would expect:

<p><%=@presenter.edit_link%></p>

The second way is a little cleaner as we have supplied a few helper methods toclean up remove some of the boilerplate. You need to include theRectify::ControllerHelpers module and then use thepresent helper:

classUsersController <ApplicationControllerincludeRectify::ControllerHelpersdefshowuser=User.find(params[:id])presentUserDetailsPresenter.new(:user=>user)endend

In your view, you can access this presenter using thepresenter helper method:

<p><%=presenter.edit_link%></p>

We recommend having a single Presenter per view but you may want to have morethan one presenter. You can use a Presenter to to hold the presentation logicof your layout or for a component view. To do this, you can either use the firstmethod above or use thepresent method and add afor option with any key:

classApplicationController <ActionController::BaseincludeRectify::ControllerHelpersbefore_action{presentLayoutPresenter.new(:user=>user),:for=>:layout}end

To access this Presenter in the view, just pass the Presenter key to thepresenter method like so:

<p><%=presenter(:layout).login_link%></p>

Updating values of a Presenter

After a presenter has been instantiated you can update it's values by justsetting their attributes:

classUsersController <ApplicationControllerincludeRectify::ControllerHelpersdefshowuser=User.find(params[:id])presentUserDetailsPresenter.new(:user=>user)presenter.user=User.firstend# or...defother_actionuser=User.find(params[:id])@presenter=UserDetailsPresenter.new(:user=>user).attach_controller(self)@presenter.user=User.firstendend

As mentioned above in the Commands section, you can use theexpose method (ifyou includeRectify::ControllerHelpers). You can use this anywhere in thecontroller action including the Command handler block. If you have set aPresenter for the view thenexpose will try to set an attribute on thatpresenter. If there is no Presenter or the Presenter doesn't have a matchingattribute thenexpose will set an instance variable of the same name:

classUsersController <ApplicationControllerincludeRectify::ControllerHelpersdefshowuser=User.find(params[:id])presentUserDetailsPresenter.new(:user=>user)expose(:user=>User.first,:message=>"Hello there!")# presenter.user == User.first# @message == "Hello there!"endend

Decorators

Another option for containing your UI logic is to use a Decorator. Rectifydoesn't ship with a built in way to create a decorator but we recommend eitherusingDraper or you can roll your ownusingSimpleDelegator:

classUserDecorator <SimpleDelegatordeffull_name"#{first_name}#{last_name}"endenduser=User.new(:first_name=>"Andy",:last_name=>"Pike")decorator=UserDecorator.new(user)decorator.full_name# => "Andy Pike"

If you want to decorate a collection of objects you can do that by adding thefor_collection method:

classUserDecorator <SimpleDelegator# ...defself.for_collection(users)users.map{ |u|new(u)}endendusers=UserDecorator.for_collection(User.all)user.eachdo |u|u.full_name# => Works for each user :o)end

Query Objects

The final main component to Rectify is the Query Object. It's role is toencapsulate a single database query and any logic that it query needs tooperate. It still uses ActiveRecord but adds some very light sugar on the top tomake this style of architecture easier. This helps to keep your model classeslean and gives a natural home to this code.

To create a query object, you create a new class and derive off ofRectify::Query. The only thing you need to do is to implement the#query method and return anActiveRecord::Relation object from it:

classActiveUsers <Rectify::QuerydefqueryUser.where(:active=>true)endend

To use this object, you just instantiate it and then use one of the followingmethods to make use of it:

ActiveUsers.new.count# => Returns the number of recordsActiveUsers.new.first# => Returns the first recordActiveUsers.new.exists?# => Returns true if there are any records, else falseActiveUsers.new.none?# => Returns true if there are no records, else falseActiveUsers.new.to_a# => Execute the query and returns the resulting objectsActiveUsers.new.eachdo |user|# => Iterates over each resultputsuser.nameendActiveUsers.new.map(&:age)# => All Enumerable methods

Passing data to query objects

Passing data that your queries need to operate is best done via the constructor:

classUsersOlderThan <Rectify::Querydefinitialize(age)@age=ageenddefqueryUser.where("age > ?",@age)endendUsersOlderThan.new(25).count# => Returns the number of users over 25 years old

Sometimes your queries will need to do a little work with the provided databefore they can use it. Having your query encapsulated in an object makes thiseasy and maintainable (here's a trivial example):

classUsersWithBlacklistedEmail <Rectify::Querydefinitialize(blacklist)@blacklist=blacklistenddefqueryUser.where(:email=>blacklisted_emails)endprivatedefblacklisted_emails@blacklist.map{ |b|b.email.strip.downcase}endend

Composition

One of this great features of ActiveRecord is the ability to easily composequeries together in a simple way which helps reusability. Rectify Query Objectscan also be combined to created composed queries using the| operator as weuse in Ruby for Set Union. Here's how it looks:

active_users_over_20=ActiveUsers.new |UsersOlderThan.new(20)active_users_over_20.count# => Returns number of active users over 20 years old

You can union many queries in this manner which will result in anotherRectify::Query object that you can use just like any other. This results in asingle database query.

As an alternative you can also use the#merge method which is simply an aliasof the| operator:

active_users_over_20=ActiveUsers.new.merge(UsersOlderThan.new(20))active_users_over_20.count# => Returns number of active users over 20 years old

The.merge class method ofRectify::Query accepts multipleRectify::Query objects to union together. This is the same as using the| operator on multipleRectify::Query objects.

active_users_over_20=Rectify::Query.merge(ActiveUsers.new,UsersOlderThan.new(20))active_users_over_20.count# => Returns number of active users over 20 years old

You can also pass aRectify::Query object into the constructor of anotherRectify::Query object to set it as the base scope.

classUsersOlderThan <Rectify::Querydefinitialize(age,scope=AllUsers.new)@age=age@scope=scopeenddefquery@scope.query.where("age > ?",@age)endendUsersOlderThan.new(20,ActiveUsers.new).count

Leveraging your database

UsingActiveRecord::Relation is a great way to construct your database queriesbut sometimes you need to to use features of your database that aren't supportedby ActiveRecord directly. These are usually database specific and can greatlyimprove your query efficiency. When that happens, you will need to write someraw SQL. Rectify Query Objects allow for this. In addition to your#querymethod returning anActiveRecord::Relation you can also return an array ofobjects. This means you can run raw SQL usingActiveRecord::Querying#find_by_sql:

classUsersOverUsingSql <Rectify::Querydefinitialize(age)@age=ageenddefqueryUser.find_by_sql(["SELECT * FROM users WHERE age > :age ORDER BY age ASC",{:age=>@age}])endend

When you do this, the normalRectify::Query methods are available but theyoperate on the returned array rather than on theActiveRecord::Relation. Thisincludes composition using the| operator but you can't compose anActiveRecord::Relation query object with one that returns an array of objectsfrom its#query method. You can compose two queries where both return arraysbut be aware that this will query the database for each query object and thenperform a Ruby array set union on the results. This might not be the mostefficient way to get the results so only use this when you are sure it's theright thing to do.

The above example is fine for short SQL statements but if you are using raw SQL,they will probably be much longer than a single line. Rectify provides a smallmodule that you can include to makes your query objects cleaner:

classUsersOverUsingSql <Rectify::QueryincludeRectify::SqlQuerydefinitialize(age)@age=ageenddefmodelUserenddefsql<<-SQL.strip_heredoc      SELECT *      FROM users      WHERE age > :age      ORDER BY age ASC    SQLenddefparams{:age=>@age}endend

Just includeRectify::SqlQuery in your query object and then supply the amodel method that returns the model of the returned objects. Aparams method that returns a hash containing named parameters that the SQLstatement requires. Lastly, you must supply asql method that returns the rawSQL. We recommend using a heredoc which makes the SQL much cleaner and easierto read. Parameters use the ActiveRecord standard symbol notation as shown abovewith the:age parameter.

Stubbing Query Objects in tests

Now that you have your queries nicely encapsulated, it's now easier with a cleardivision of responsibility to improve how you use the database within yourtests. You should unit test your Query Objects to ensure they return the correctdata from a know database state.

What you can now do it stub out these database calls when you use them in otherclasses. This improves your test code in a couple of ways:

  1. You need less database setup code within your tests. Normally you might usesomething like factory_girl to create records in your database and then whenyour tests run they query this set of data. Stubbing the queries within yourtests can reduce this complexity.
  2. Fewer database queries running and less factory usage means that your tests
  3. are doing less work and therefore will run a bit faster.

In Rectify, we provide the RSpec helper methodstub_query that will makestubbing Query Objects easy:

# inside spec/rails_helper.rbrequire"rectify/rspec"RSpec.configuredo |config|# snip ...config.includeRectify::RSpec::Helpersend# within a spec:it"returns the number of users"dostub_query(UsersOlderThan,:results=>[User.new,User.new])expect(subject.awesome_method).toeq(2)end

As a convenience:results accepts either an array of objects or a singleinstance:

stub_query(UsersOlderThan,:results=>[User.new,User.new])stub_query(UsersOlderThan,:results=>User.new)

Where do I put my files?

The next inevitable question is "Where do I put my Forms, Commands, Queries andPresenters?". You could createforms,commands,queries andpresentersfolders and follow the Rails Way. Rectify suggests grouping your classes byfeature rather than by pattern. For example, create a folder calledcore (thiscan be anything) and within that, create a folder for each broad feature of yourapplication. Something like the following:

.└── app    ├── controllers    ├── core    │   ├── billing    │   ├── fulfillment    │   ├── ordering    │   ├── reporting    │   └── security    ├── models    └── views

Then you would place your classes in the appropriate feature folder. If youfollow this pattern remember to namespace your classes with a matching modulewhich will allow Rails to load them:

# in app/core/billing/send_invoice.rbmoduleBillingclassSendInvoice <Rectify::Command# ...endend

You don't need to alter your load path as everything in theapp folder isloaded automatically.

As stated above, if you prefer not to use this method of organizing your codethen that is totally fine. Just create folders underapp for the things inRectify that you use:

.└── app    ├── commands    ├── controllers    ├── forms    ├── models    ├── presenters    ├── queries    └── views

You don't need to make any configuration changes for your preferred folderstructure, just use whichever you feel most comfortable with.

Trade offs

This style of Rails architecture is not a silver bullet for all projects. Ifyour app is pretty much just basic CRUD then you are unlikely to get muchbenefit from this. However, if your app is more than just CRUD then you shouldsee an improvement in code structure and maintainability.

The downside to this approach is that there will be many more classes and filesto deal with. This can be tricky as the application gets bigger to hold thewhole system in your head. Personally I would prefer that as maintaining it willbe easier as all code around a specific user task is on one place.

Before you use these methods in your project, consider the trade off and usethese strategies where they make sense for you and your project. It maybe mostpragmatic to use a mixture of the classic Rails Way and the Rectify approachdepending on the complexity of different areas of your application.

Developing Rectify

Some tests (specifically for Query objects) we need access to a database thatActiveRecord can connect to. We use SQLite for this at present. When you run thespecs withbundle exec rspec, the database will be created for you.

There are some Rake tasks to help with the management of this test databaseusing normal(ish) commands from Rails:

rake db:migrate# => Migrates the test databaserake db:schema# => Dumps database schemarake g:migration# => Create a new migration file (use snake_case name)

Releasing a new version

Bump the version inlib/rectify/version.rb then do the following:

bundlegem build rectify.gemspecgem push rectify-0.0.0.gem

[8]ページ先頭

©2009-2025 Movatter.jp