Home > Articles

This chapter is from the book

2.3 Handling Dependencies within Components

With Sportsball now having the ability to store teams and games, we can turn to the question of how to predict the outcome of games based on past performance. To this end, we would like to add a page that will allow us to pick two teams. Click a button labeled something like “Predict the winner!”, and see the application’s prediction of who is more likely to win as a result.

2.3.1 Using path Blocks for Specifying CBRA Dependencies

When we added our component, we used the following format for stating this dependency in the app’s Gemfile as follows:

Sample Gemfile reference using path option

1 gem 'app_component', path: 'components/app_component'

There is another way of stating this dependency using a block syntax, like so:

Same Gemfile reference using path block

1 path "components" do
2   gem "app_component"
3 end

The first visible difference is that there will be less code to write when the list of dependencies grows. That is, of course, only if we put future components into the same components folder. Additional components are simply added to the block:

Gemfile reference with multiple gems in path block

1 path "components" do
2   gem "app_component"
3   gem "component_a"
4   gem "component_b"
5 end

There is another difference between the path option and the block syntax. As Enrico Teotti reports (http://teotti.com/gemfiles-hierarchy-in-ruby-on-rails-component-based-architecture/), the block syntax will use a feature in bundler that ensures that transitive dependencies of AppComponent are looked up in the stated path folder. That means that it will not be necessary to state every transitive CBRA dependency explicitly in the Gemfile. Instead, only the direct dependencies need to be listed.

For example, imagine that in the previous Gemfile, component_a depends on component_c. Without path block syntax, we would need to add gem "component_c", path: "components/component_c" to our Gemfile. With path block syntax, we don’t have to. We get this for free since we already stated that the direct dependency component_a is listed.

Because of this, when using cobradeps to generate component diagrams, it is no longer necessary to specify a special group for direct dependencies; cobradeps simply assumes that all stated dependencies are direct dependencies.

2.3.2 Adding a Regular Gem: slim—Different Templating

Before we get to the part where we calculate a likely outcome, we need to add the new page. I find ERB unnecessarily verbose and avoid it when possible. Luckily, there are plenty of alternatives out there, and we can add the first dependency to our component that is not Rails.

I like slim (http://slim-lang.com/), as it greatly reduces the amount of code I have to write in comparison to ERB. Particularly, the number of chevrons (the “<” and “>” symbols so common in HTML) is greatly reduced, which I like a lot. Instead of adding the slim (https://rubygems.org/gems/slim) gem, we will add slim-rails (https://rubygems.org/gems/slim-rails), which in turn will require slim, but in addition adds Rails generators that can create views in slim syntax.

./components/app_component/app_component.gemspec - Add slim dependency

1 s.add_dependency "slim-rails"

This line should be added to AppComponent’s gemspec file to require slim-rails. Running bundle in the main app’s root folder, we should see slim-rails and slim being installed. Take a note of the exact version of slim-rails that is installed. At the time of writing, it is 3.1.3.

To make use of our new gem, let us use the current welcome page as an example and translate it into slim. In fact, the current welcome page still contains that default auto-generated text. We will use the opportunity to give the page a bit more meaningful content. So, let’s delete ./components/app_component/app/views/app_component/welcome/index.html.erb and create an index.html.slim in the same folder instead. The new page links to the admin pages of Team and Game.

./components/app_component/app/views/app_component/welcome/index.html.slim

1 h1 Welcome to Sportsball!
2 p Predicting the outcome of matches since 2015.
3
4 = link_to "Manage Teams", teams_path
5 | &nbsp;|&nbsp;
6 = link_to "Manage Games", games_path

When we fire up the server, however, and try to load the new homepage of our app, instead of a page, we get this “Template is missing” error depicted in Figure 2.5.

Figure 2.5

Figure 2.5. “Template missing” error after switching to slim

The reason for this is that, unlike Rails applications, which automatically require all the gems they are directly dependent upon, Rails engines do not. Check out Jonathan Rochkind’s blog post on the issue (https://bibwild.wordpress.com/2013/02/27/gem-depends-on-rails-engine-gem-gotcha-need-explicit-require/). We never require slim in our engine and it shows, because Rails reports only the template handlers that come standard: :handlers=>[:erb, :builder, :raw, :ruby], but not :slim as we would expect.

To fix the issue, we must explicitly require slim-rails in our AppComponent component, as follows. Note that I moved require "app_component/engine" into the scope of the AppComponent module. There is no programmatic need for that, but I like for gems to indicate this way which requires are local (i.e., within the gem) versus external (i.e., external gem dependencies).

./components/app_component/lib/app_component.rb - Require slim

1 require "slim-rails"
2 
3 module AppComponent
4   require "app_component/engine"
5 end

We restart Rails to make it pick up the newly required gem and when we reload the homepage, we get the desired outcome, shown in Figure 2.6.

Figure 2.6

Figure 2.6. New welcome page written in slim

2.3.3 Locking Down Gem Versions

Let us take another closer look at the runtime dependencies now present in our AppComponent gemspec.

./components/app_component/app_component.gemspec - Production dependencies

1  s.add_dependency "rails", "~> 5.1.4"
2  s.add_dependency "slim-rails"

The Rails dependency was generated as ~> 5.1.4, allowing all versions of Rails 5.1.* (where * is 4 or greater).1 We added slim-rails without any version restrictions.

Commonly, when developing gems, authors strive to keep the range of acceptable versions of needed gems as broad as possible. This is to exclude the fewest number of developers who might be in different situations and on different update paths from using a gem. Only for incompatible differences, which would prevent the gem from working properly, would a restriction typically be added.

Contrary to this, in Rails applications, Gemfile.lock is added to source control to lock down the versions of all dependencies. This ensures that when code is run in different environments or by different people, it will behave the same.

So. We are building an app, but are using gems. Which strategy should we take? Should we have a loose or a tight version policy? Well, I lock down all runtime dependencies in components to exact versions, like the following:

./components/app_component/app_component.gemspec - Production dependencies locked down

1 s.add_dependency "rails", "5.1.4"
2 s.add_dependency "slim-rails", "3.1.3"

The reason for the version lockdown has to do with the testing of the component and is based on a couple of assumptions. I assume that you write:

  • Automated tests for your code

  • Different kinds of tests, like unit, functional, integration, and feature

  • Tests at the lowest possible level

If these assumptions are true for you, you will attempt to verify all of the internals of the component within the component itself. That also means you will not be testing the internals outside of the component, that is, in the context of the completed Rails application. What would happen if, in this situation, the versions of dependencies of the component somehow drifted from the ones used in the Rails app? That would be an untested dependency in production: The functioning of the component would be verified against a version of its dependencies that are not used in production. The version lockdown enforces that all components bound together by the Rails app run with and are tested against the same version of a dependency. Testing components is the topic of the next section, Section 3.1.

For this section, suffice it to say that there is merit to keeping the versions of dependencies in sync among all parts of the app. In the running app, only one version of every dependency is going to be loaded; we might as well try not to be surprised by how it works.

2.3.4 Adding the Development Version of a Gem: Trueskill—A Rating Calculation Library

We can now turn to the prediction of the outcome of future games. If we do not want to be in the business of figuring out how to do that, we better find a gem that will do such a calculation for us. Luckily, there is a lot of theory we could potentially draw from, such as ranking algorithms, rating algorithms, or Bayesian networks (https://www.cs.ubc.ca/~murphyk/Bayes/bnintro.html). I started my search for a fitting gem with the FIFA World Rankings page on Wikipedia, which, while not explaining how the official rankings are calculated, mentions an alternative, the Elo rating system (The rating of chess players, past and present by Aspad Elo, 1978). Elo was created for use in chess but is now used in many competitor-versus-competitor games. An improvement to Elo is the Glicko rating system (http://www.glicko.net/glicko.html), which in turn was extended by Microsoft to TrueSkill (http://trueskill.org/), a system that works for multiplayer games. For all of these—Elo, Glicko, and Trueskill—we can find corresponding gems on rubygems (https://rubygems.org). For the following, we are going to work with the trueskill gem (https://rubygems.org/gems/trueskill). Not only does the idea of assessing a team’s strength while taking into account the players’ strengths sound appealing, but the gem also poses a nice little problem: It is totally outdated. At the time of writing, the last version of the gem was published in 2011. However, code has been contributed to forks of the original project until late 2014.

The version of the code we would like to use for trueskill is commit e404f45af5 (https://github.com/benjaminleesmith/trueskill/tree/e404f45af5b3fb86982881ce064a9c764cc6a901) on the benjaminleesmith fork.2 The problem is that we can only specify gems to depend on published versions of other gems. There is no way for us to set a restriction based on a commit SHA. For gems that are intended to be published, this makes sense: They should not depend on code that was not also published and distributed as a gem.

To work around this problem, we have to employ the gem’s gemspec and its Gemfile at the same time.

./components/app_component/Gemfile

1 source "https://rubygems.org"
2 
3 gemspec
4
5 gem "trueskill",
6      git: "https://github.com/benjaminleesmith/trueskill",
7      ref: "e404f45af5b3fb86982881ce064a9c764cc6a901"

./components/app_component/app_component.gemspec – Dependencies

1 s.add_dependency "rails", "5.1.4"
2 s.add_dependency "slim-rails", "3.1.3"
3 s.add_dependency "trueskill"

The Gemfile in a gem’s directory is used during development of the gem, just like the Gemfile in a Rails app. When bundle is called in this directory, it will install all the gem dependencies listed there. The special line gemspec tells bundler to look for a gemspec file in the current directory and add all dependencies specified there to the current bundle. In our case, the gemspec states that AppComponent has a runtime dependency on trueskill and the Gemfile restricts this to be from the specified git URL at the given SHA.

Bundle AppComponent with trueskill (some results omitted). Execute in ./components/app_component

$ bundle
The latest bundler is 1.16.0.pre.3, but you are currently running     1.15.4.
To update, run `gem install bundler --pre`
Fetching https://github.com/benjaminleesmith/trueskill
Fetching gem metadata from https://rubygems.org/.......... 
Fetching version metadata from https://rubygems.org/.. 
Fetching dependency metadata from https://rubygems.org/. 
Resolving dependencies...
 
 . . .
 
Using trueskill 1.0.0 from https://github.com/benjaminleesmith/\
    trueskill (at e404f45@e404f45)
 
 . . .
 
Bundle complete! 3 Gemfile dependencies, 46 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

When bundling the component, we see git checking out the repository specified and using the correct SHA. However, bundling the main app will reveal that it does not take into account the restriction posed by the Gemfile. That is because the Gemfile of any gem is ignored by other gems or apps depending on it (again, due to fact that the common expectation is for a gem to be published).

To work around this, there is no other way than to ensure that the version of the dependency is enforced by the app itself. That leads to an exact duplicate of the trueskill line from AppComponent’s Gemfile in the main app’s Gemfile.

New lines in ./Gemfile

1 gem "trueskill",
2      git: "https://github.com/benjaminleesmith/trueskill",
3      ref: "e404f45af5b3fb86982881ce064a9c764cc6a901"

And just like with slim-rails, we need to explicitly require the trueskill gem in AppComponent to make sure it is loaded.

./components/app_component/lib/app_component.rb - Requiring the trueskill dependency

1 require "slim-rails"
2 require "saulabs/trueskill"
3
4 module AppComponent
5   require "app_component/engine"
6 end

2.3.5 Adding Predictions to the App

With models, scaffolds for administration, and the rating calculation library in place, we can turn to implementing the first iteration of game prediction.

Let’s create a cursory sketch of how our models might interact to generate a prediction. A predictor object might get a collection of all the games it should consider. As we are using an external library, we don’t really know what is going on. The best way we can describe it is that the predictor learns (about the teams or the games). Because of this, we will make learn the first method of the public interface of the class.

After the predictor has learned the strengths of teams it can, given two teams, predict the outcome of their next match. predict becomes the second method of the public interface.

./components/app_component/app/models/app_component/predictor.rb

 1 module AppComponent
 2   class Predictor
 3     def initialize(teams)
 4       @teams_lookup = teams.inject({}) do |memo, team|
 5         memo[team.id] = {
 6             team: team,
 7             rating: [Saulabs::TrueSkill::Rating.new(
 8                           1500.0, 1000.0, 1.0)]
 9         }
10         memo
11     end
12   end
13
14   def learn(games)
15     games.each do |game|
16       first_team_rating = 
17           @teams_lookup[game.first_team_id][:rating]
18       second_team_rating = 
19           @teams_lookup[game.second_team_id][:rating]
20       game_result = game.winning_team == 1 ?
21            [first_team_rating, second_team_rating] :
22            [second_team_rating, first_team_rating]
23       Saulabs::TrueSkill::FactorGraph.new(
24           game_result, [1, 2]).update_skills
25     end
26   end
27
28   def predict(first_team, second_team)
29     team1 = @teams_lookup[first_team.id][:team]
30     team2 = @teams_lookup[second_team.id][:team]
31     winner = higher_mean_team(first_team, second_team) ?
32         team1 : team2
33     AppComponent::Prediction.new(team1, team2, winner)
34   end
35
36   def higher_mean_team(first_team, second_team)
37     @teams_lookup[first_team.id][:rating].first.mean >
38         @teams_lookup[second_team.id][:rating].first.mean
39   end
40  end
41 end

To start, initialize creates a lookup hash from all the teams it is handed that allows the Predictor class to efficiently access teams and their ratings by a team’s id.

Inside of learn, the predictor loops over all the games that were given. It looks up the ratings of the two teams playing each game. The teams’ ratings are passed into an object from trueskill called FactorGraph in the order “winner first, loser second” so that the update_skills method can update the ratings of both teams.

predict simply compares the mean rating values of the two teams and “predicts” that the stronger team will win. It returns a Prediction object, which we will look at next.

There is not much going on in the Prediction class. It is simply a data object that holds on to the teams participating in the prediction, as well as the winning team.

./components/app_component/app/models/app_component/prediction.rb

 1 module AppComponent
 2   class Prediction
 3     attr_reader :first_team, :second_team, :winner
 4
 5     def initialize(first_team, second_team, winner)
 6       @first_team = first_team
 7       @second_team = second_team
 8       @winner = winner
 9     end
10   end
11 end

The PredictionsController has two actions: new and create. The first, new, loads all teams so they are available for the selection of the game to be predicted. create creates a new Predictor and then calls learn and predict in sequence to generate a prediction.

./components/app_component/app/controllers/app_component/predictions_controller.rb

 1 require_dependency "app_component/application_controller"
 2 module AppComponent
 3   class PredictionsController < ApplicationController
 4     def new
 5       @teams = AppComponent::Team.all
 6     end
 7
 8     def create
 9       predictor = Predictor.new(AppComponent::Team.all)
10       predictor.learn(AppComponent::Game.all)
11       @prediction = predictor.predict(
12           AppComponent::Team.find(params["first_team"]["id"]),
13           AppComponent::Team.find(params["second_team"]["id"]))
14     end
15   end
16 end

For completeness, we list the two views of the prediction interface as well as a helper that is used to generate the prediction result that will be displayed as a result.

./components/app_component/app/views/app_component/predictions/ new.html.slim

 1 h1 Predictions
 2 
 3 = form_tag prediction_path, method: "post" do |f|
 4   .field
 5     = label_tag :first_team_id
 6     = collection_select(:first_team, :id, @teams, :id, :name)
 7
 8   .field
 9    = label_tag :second_team_id
10    = collection_select(:second_team, :id, @teams, :id, :name)
11   .actions = submit_tag "What is it going to be?", class: "button"

./components/app_component/app/views/app_component/predictions/create.html.slim

1 h1 Prediction
2
3 =prediction_text @prediction.first_team, @prediction.second_team,
      @prediction.winner
4
5 .actions
6   = link_to "Try again!", new_prediction_path, class: "button"

./components/app_component/app/helpers/app_component/predictions_helper.rb

1 module AppComponent
2   module PredictionsHelper
3     def prediction_text(team1, team2, winner)
4       "In the game between #{team1.name} and #{team2.name} " +
5           "the winner will be #{winner.name}"
6     end
7   end
8 end

Finally, we can add a link to the prediction to the homepage to complete this feature.

./components/app_component/app/views/app_component/welcome/ index.html.slim

1 h1 Welcome to Sportsball!
2 p Predicting the outcome of matches since 2015.
3
4 = link_to "Manage Teams", teams_path
5 | &nbsp;|&nbsp;
6 = link_to "Manage Games", games_path
7 | &nbsp;|&nbsp;
8 = link_to "Predict an outcome!", new_prediction_path

With the changes from this section in place, we can navigate to http://localhost:3000/ to see a new homepage (see Figure 2.7) from which we can navigate to our new prediction section. Figure 2.8 shows how we can request a new prediction. Finally, in Figure 2.9, we see the result of a successful prediction.

Figure 2.7

Figure 2.7. Sportsball homepage with link to predictions

Figure 2.8

Figure 2.8. Requesting the prediction of a game

Figure 2.9

Figure 2.9. Showing the prediction result

With the conclusion of this chapter, Sportsball is fully functional! Well, if you can call it functional at this point. In any case, the current state of the application will allow us to discuss and analyze many aspects of componentization. We will start in Chapter 3 by testing our component and application.

InformIT Promotional Mailings & Special Offers

I would like to receive exclusive offers and hear about products from InformIT and its family of brands. I can unsubscribe at any time.

Overview


Pearson Education, Inc., 221 River Street, Hoboken, New Jersey 07030, (Pearson) presents this site to provide information about products and services that can be purchased through this site.

This privacy notice provides an overview of our commitment to privacy and describes how we collect, protect, use and share personal information collected through this site. Please note that other Pearson websites and online products and services have their own separate privacy policies.

Collection and Use of Information


To conduct business and deliver products and services, Pearson collects and uses personal information in several ways in connection with this site, including:

Questions and Inquiries

For inquiries and questions, we collect the inquiry or question, together with name, contact details (email address, phone number and mailing address) and any other additional information voluntarily submitted to us through a Contact Us form or an email. We use this information to address the inquiry and respond to the question.

Online Store

For orders and purchases placed through our online store on this site, we collect order details, name, institution name and address (if applicable), email address, phone number, shipping and billing addresses, credit/debit card information, shipping options and any instructions. We use this information to complete transactions, fulfill orders, communicate with individuals placing orders or visiting the online store, and for related purposes.

Surveys

Pearson may offer opportunities to provide feedback or participate in surveys, including surveys evaluating Pearson products, services or sites. Participation is voluntary. Pearson collects information requested in the survey questions and uses the information to evaluate, support, maintain and improve products, services or sites, develop new products and services, conduct educational research and for other purposes specified in the survey.

Contests and Drawings

Occasionally, we may sponsor a contest or drawing. Participation is optional. Pearson collects name, contact information and other information specified on the entry form for the contest or drawing to conduct the contest or drawing. Pearson may collect additional personal information from the winners of a contest or drawing in order to award the prize and for tax reporting purposes, as required by law.

Newsletters

If you have elected to receive email newsletters or promotional mailings and special offers but want to unsubscribe, simply email information@informit.com.

Service Announcements

On rare occasions it is necessary to send out a strictly service related announcement. For instance, if our service is temporarily suspended for maintenance we might send users an email. Generally, users may not opt-out of these communications, though they can deactivate their account information. However, these communications are not promotional in nature.

Customer Service

We communicate with users on a regular basis to provide requested services and in regard to issues relating to their account we reply via email or phone in accordance with the users' wishes when a user submits their information through our Contact Us form.

Other Collection and Use of Information


Application and System Logs

Pearson automatically collects log data to help ensure the delivery, availability and security of this site. Log data may include technical information about how a user or visitor connected to this site, such as browser type, type of computer/device, operating system, internet service provider and IP address. We use this information for support purposes and to monitor the health of the site, identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents and appropriately scale computing resources.

Web Analytics

Pearson may use third party web trend analytical services, including Google Analytics, to collect visitor information, such as IP addresses, browser types, referring pages, pages visited and time spent on a particular site. While these analytical services collect and report information on an anonymous basis, they may use cookies to gather web trend information. The information gathered may enable Pearson (but not the third party web trend services) to link information with application and system log data. Pearson uses this information for system administration and to identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents, appropriately scale computing resources and otherwise support and deliver this site and its services.

Cookies and Related Technologies

This site uses cookies and similar technologies to personalize content, measure traffic patterns, control security, track use and access of information on this site, and provide interest-based messages and advertising. Users can manage and block the use of cookies through their browser. Disabling or blocking certain cookies may limit the functionality of this site.

Do Not Track

This site currently does not respond to Do Not Track signals.

Security


Pearson uses appropriate physical, administrative and technical security measures to protect personal information from unauthorized access, use and disclosure.

Children


This site is not directed to children under the age of 13.

Marketing


Pearson may send or direct marketing communications to users, provided that

  • Pearson will not use personal information collected or processed as a K-12 school service provider for the purpose of directed or targeted advertising.
  • Such marketing is consistent with applicable law and Pearson's legal obligations.
  • Pearson will not knowingly direct or send marketing communications to an individual who has expressed a preference not to receive marketing.
  • Where required by applicable law, express or implied consent to marketing exists and has not been withdrawn.

Pearson may provide personal information to a third party service provider on a restricted basis to provide marketing solely on behalf of Pearson or an affiliate or customer for whom Pearson is a service provider. Marketing preferences may be changed at any time.

Correcting/Updating Personal Information


If a user's personally identifiable information changes (such as your postal address or email address), we provide a way to correct or update that user's personal data provided to us. This can be done on the Account page. If a user no longer desires our service and desires to delete his or her account, please contact us at customer-service@informit.com and we will process the deletion of a user's account.

Choice/Opt-out


Users can always make an informed choice as to whether they should proceed with certain services offered by InformIT. If you choose to remove yourself from our mailing list(s) simply visit the following page and uncheck any communication you no longer want to receive: www.informit.com/u.aspx.

Sale of Personal Information


Pearson does not rent or sell personal information in exchange for any payment of money.

While Pearson does not sell personal information, as defined in Nevada law, Nevada residents may email a request for no sale of their personal information to NevadaDesignatedRequest@pearson.com.

Supplemental Privacy Statement for California Residents


California residents should read our Supplemental privacy statement for California residents in conjunction with this Privacy Notice. The Supplemental privacy statement for California residents explains Pearson's commitment to comply with California law and applies to personal information of California residents collected in connection with this site and the Services.

Sharing and Disclosure


Pearson may disclose personal information, as follows:

  • As required by law.
  • With the consent of the individual (or their parent, if the individual is a minor)
  • In response to a subpoena, court order or legal process, to the extent permitted or required by law
  • To protect the security and safety of individuals, data, assets and systems, consistent with applicable law
  • In connection the sale, joint venture or other transfer of some or all of its company or assets, subject to the provisions of this Privacy Notice
  • To investigate or address actual or suspected fraud or other illegal activities
  • To exercise its legal rights, including enforcement of the Terms of Use for this site or another contract
  • To affiliated Pearson companies and other companies and organizations who perform work for Pearson and are obligated to protect the privacy of personal information consistent with this Privacy Notice
  • To a school, organization, company or government agency, where Pearson collects or processes the personal information in a school setting or on behalf of such organization, company or government agency.

Links


This web site contains links to other sites. Please be aware that we are not responsible for the privacy practices of such other sites. We encourage our users to be aware when they leave our site and to read the privacy statements of each and every web site that collects Personal Information. This privacy statement applies solely to information collected by this web site.

Requests and Contact


Please contact us about this Privacy Notice or if you have any requests or questions relating to the privacy of your personal information.

Changes to this Privacy Notice


We may revise this Privacy Notice through an updated posting. We will identify the effective date of the revision in the posting. Often, updates are made to provide greater clarity or to comply with changes in regulatory requirements. If the updates involve material changes to the collection, protection, use or disclosure of Personal Information, Pearson will provide notice of the change through a conspicuous notice on this site or other appropriate way. Continued use of the site after the effective date of a posted revision evidences acceptance. Please contact us if you have questions or concerns about the Privacy Notice or any objection to any revisions.

Last Update: November 17, 2020