We’ll work model by model, adding soft programmatic enhancement to theWork, Customer, and Composer models.. To give you an overview, here’s a list ofall the questions that we’ll ask and
Trang 1these particular models, and they’ve been chosen to represent a broad range ofpossibilities But the point is that the door is open As you become increasinglyfamiliar and comfortable with Ruby, writing this kind of code will come more andmore naturally
We’ll work model by model, adding soft programmatic enhancement to theWork, Customer, and Composer models To give you an overview, here’s a list ofall the questions that we’ll ask and answer by writing methods in this section:
■ The Work model
● Which publishers have published editions of this work?
● What country is this work from?
● What key is this work in?
■ The Customer model
● Which customers have ordered this work?
● What open orders does this customer have?
● What editions does this customer have on order?
● What editions has this customer ever ordered?
● What works does this customer have on order?
● What works has this customer ever ordered?
■ The Composer model
● What editions of this composer’s works exist?
● What publishers have editions of this composer’s works?
As you’ll see, all of these methods involve giving the models a boost—a way tounearth and collect existing information that isn’t available already in the form
we want it through an existing method
NOTE PLAIN ARRAYS VS MAGIC COLLECTIONS You should keep one important
point in mind as you look at, and eventually write, soft enhancements toyour models When you gather together, say, an array of Edition objects
by traversing a collection of Work objects and accumulating their editions,
you end up with a plain Ruby array You don’t end up with a magic
ActiveRecord collection You’ll recall that in discussing arrays inchapter 11, we looked at ActiveRecord collections as an example of some-thing array-like that is also endowed with methods and behaviors that gobeyond those of the normal array Those extra powers aren’t added toarrays that you create, even if they contain ActiveRecord objects
Trang 215.2.1 Honing the Work model through soft enhancements
The soft programmatic extensions to the Work model involve mining the modeland its associated entities for a next level of information This information, inturn, might be used for in-house reports, richer on-screen information displays,
or sales profiling
All these Work enhancements belong in the work model file, work.rb
Which publishers have published editions of this work?
This method uses the same basic approach as the editions method that served as
an introductory example in section 15.1 In fact, it builds on that method: It callseditions and uses map on the resulting array to extract the publisher of each edi-tion It then performs a uniq operation, resulting in a nonduplicative list of allpublishers who have published this work:
def publishers
editions.map {|e| e.publisher}.uniq
end
This technique skims the publishers from the editions, producing a list of the latter
What country is this work from?
A case could be made for assigning the work either to the native country of thecomposer or to the country of first publication Because we’re dealing with asheet-music store and not a library, we don’t necessarily know what the first publi-cation was That means if we want to assign a country to a work, it’s best to echothe composer’s country This is an easy soft enhancement to the Work model:def country
composer.country
end
The enhancement qualifies as soft because it’s passive: It reaches out one level, fromthe work to its composer, and gathers information, which it returns unchanged
Which customers have ordered this work?
A method like this could conceivably be of interest in calculating sales figures andtrends Once again, we use the editions method as a point of entry for gatheringfurther information In this case, we map all the existing orders for all editions ofthis work—and from that mapping, we harvest the customers of the orders:def ordered_by
editions.orders.map {|o| o.customer }.uniq
end
Trang 3We then make the resulting array unique, in case any customer has purchasedtwo different editions of the work or purchased one edition twice For some pur-poses, you may want to keep such duplicates—for example, if you’re trying todetermine a work’s popularity (in which case someone who bought every edition
of it might legitimately be counted multiple times) But assuming that you’reinterested simply in a list of customers who have bought this work, there’s nopoint saving the duplicates
What key is this work in?
You might not have been expecting this to be one of the enhancements, because the
work’s key is already stored directly in the database But remember: key is a reserved
word in SQL, so we named the field containing the key kee The enhancement we
need is one that will let us use key as a method name to get the key of a work It’s simple:
15.2.2 Modeling the customer’s business
In the case of the customer, we want to know a number of things Some of thesemethods are layered on, or embedded into, others Some will be of direct use atthe controller/view stage
What open orders does this customer have?
We’ll write open_orders to return an ActiveRecord collection:
What editions does this customer have on order?
Here, we use standard Ruby array methods to grab all the editions this customerhas ordered:
Trang 4What editions has this customer ever ordered?
This method is a superset of editions_on_order, returning a list of all the editionsthis customer has ever ordered This information will be useful in calculating acustomer’s favorites (favorite composers and/or instruments):
to generate a list of every work the customer has ever ordered
What works does this customer have on order?
Here, we start with editions_on_order and then dig into the contents of the tion (its list of works):
so we flatten it and then run it through uniq to get rid of duplicate works
What works has this customer ever ordered?
This method is like the previous one, but it gathers works from all editions, notjust those on order:
Trang 5enhancement; they involve calculating a new value from existing data We’ll leavethose for the next section, and turn briefly to the composer.
15.2.3 Fleshing out the Composer
Composers are a fairly inactive element in the universe of our domain They don’tchange much, and most of them have stopped composing, so there’s not as muchneed to provide them with a data-manipulation toolset as there is with some of theother models We’ll define only two composer instance methods; they go in theComposer class, in composer.rb
What editions of this composer’s works exist?
This is the method that served as the preliminary example of a soft modelenhancement:
embed-What publishers have editions of this composer’s works?
This method may possibly be of use only for internal accounting purposes—butwe’ll throw it in for good measure and as a lesson in how easy it is to expand yourapplication’s repertoire of methods:
First, by way of final reflection on soft enhancements, a few words are in orderabout the relationship between Ruby code and SQL—or, more accurately, the pro-cess of choosing between Ruby and SQL—in the writing of soft enhancements
15.2.4 Ruby vs SQL in the development of soft enhancements
When you write code whose main purpose is to pull records out of a relational base, the most efficient, fastest code you can write is SQL code As you probably
Trang 6data-know, much of what ActiveRecord does for you under the hood is to translate yourRuby code into SQL statements and then query your application’s databases withthose statements
In the interest of increasing execution speed, ActiveRecord lets you feed itpure SQL almost whenever you want You lose the nice Ruby-wrapped look-and-feel, but you gain efficiency
As a study in Ruby/SQL contrast, take the Composer#editions method fromsection 15.2.3:
a well-stocked music store
Here’s an alternative editions method, written using SQL instead of Ruby tonarrow the selection of Edition objects:
def editions
ddddEdition.find_by_sql("SELECT edition_id from editions_works
ddddLEFT JOIN works ON editions_works.work_id = works.id
ddddLEFT JOIN composers ON works.composer_id = composers.id
ddddWHERE (composers.id = #{id})")
end
This method asks the database engine to do the work By the time the single call
to find_by_sql is finished, we have all the editions we need; no further Ruby mands are required
Database engines such as MySQL tend to be efficient (at least, ideally) Askingfor the right records in the first place, rather than asking for more records thanyou need and then pruning them in Ruby, is faster and more efficient
But it also means you have to write everything in SQL—which is not necessarily
a hardship from the point of view of programming but does destroy your gram’s consistent look and feel Nor is this issue entirely cosmetic The consistent
pro-“Rubyness” of a Rails application makes for a consistent development experience:It’s easier to think in Ruby the whole time than it is to switch back and forth (You
Trang 7have to do some switching anyway, if you’re writing the database; but the ideal is
to keep that process as separate as possible from the higher-level coding.)
Because it involves hard-coding table and field names into your Ruby methods,doing soft enhancements in SQL has the potential to make the application codeharder to maintain later on True, you can’t write a Rails application withoutknowing the table and field names; but having them physically present in yourmodel code takes the coupling of database and code a step further But it willmake your application faster, as well as giving you “magic” ActiveRecord collec-tions rather than standard Ruby arrays as containers for your objects
What’s the right choice? Not surprisingly, it all depends Luckily, you don’thave to make an all-or-nothing, winner-take-all choice between Ruby and SQL asmodel enhancement languages Rails is designed in full knowledge of the prosand cons of SQL versus pure Ruby The existence of the find_by_sql methodattests to this fact; so does the use of SQL fragments to specify record order (as in:order => "created_at, ASC", an SQL hint used in the customer’s has_many:orders association) The reality of relational database programming is that youshould know some SQL if you’re going to do it, even at one level of remove—andRails facilitates your using SQL when you want to
The philosophy of this book is that it’s good to use Ruby to enhance the tionality of models until you hit a performance wall and have to use raw SQL Therelationship between Ruby and SQL, in this context, isn’t unlike the relationshipbetween Ruby and C in the general Ruby-programming context: Ruby program-
func-mers write in Ruby, knowing that it isn’t a terribly fast language; when they hit
seri-ous performance bottlenecks, they write parts of their programs as C extensions,
so that those parts will speed up and the whole program will run faster
SQL can play a similar role for you in your Rails application development.Think of Rails applications as Ruby programs, first and foremost But by all meanstake advantage of the options that ActiveRecord gives you, by way of using SQL,when you spot something you’ve written in Ruby that seems to be seriously slow-ing your program
This meditation on SQL and Ruby truly brings us to the end of our soft grammatic enhancement discussion and to our next major topic: hard program-matic enhancements of ActiveRecord models
Trang 8pro-15.3 Hard programmatic enhancement
of model functionality
In this section, we’re going to pull out the Ruby stops and show how you can addnew functionality to your models that may not have any direct relation to themodels’ basic properties and capabilities Basically, you can define any method foryour models to respond to The idea isn’t to create chaos, but to come up withthings you might want to know
The examples here are clustered by type of example rather than in a and-answer format This reflects the fact that hard enhancements tend to have apurpose other than straightforward querying of an object for information; theyentail the creation of a new object or data structure rather than a culling of exist-ing objects
In the sections that follow, we’ll develop hard programmatic enhancements ofseveral of the R4RMusic models The enhancements fall into three categories:
■ Prettification of string properties
■ Calculating a work’s period
■ Providing the customer with more functionality
Your Ruby skills will get a workout here, and you’ll learn a few new techniquesalong the way
15.3.1 Prettification of string properties
A common use for hard model enhancements is the prettification of string
proper-ties—the generation of a new string in which existing string information isembedded and which looks better, for presentation, than the raw string data avail-able through the object would look
We’ve already seen one example of prettification of strings: the poser#whole_name method defined for the purpose of easily displaying all thecomponents of a composer’s name together This kind of thing can come inhandy frequently and can involve greater complexity and planning than juststringing strings together We’ll look at some examples here
Com-Formatting the names of the work’s instruments
The Work model is a good candidate for some pretty-formatting operations It has
a title, an opus number, and a list of instruments, all of which are stored in rawform and are in need of massaging on the way to public viewing
Trang 9We’ll begin with the instruments, because the resulting list will be of use inthe title.
Let’s start with the nice_instruments method, an instance method of the Workclass in work.rb, like this:
def nice_instruments
instrs = instruments.map {|inst| inst.name }
This map operation skims the name values from the list of instrument objects and
stores them in a new array, which we save to the variable instrs
The next step (almost) is to format these names into a nice string There’s oneintermediate tweak, though It has to do with the order of instruments: cello andpiano, or piano and cello?
We’ll handle this in the following way First, we create an array of instrumentnames in what we consider the canonical (or at least likely to be correct almostevery time) order Incidentally, you’ll encounter a new technique in this line ofcode: the %wf{…} construct, which generates an array whose elements are the indi-vidual words inside the curly braces
ordered = %w{ flute oboe violin viola cello piano orchestra }
Next, we sort instrs according to where in this array (at what numbered index)each instrument occurs Because it’s possible that we’ll encounter an instrumentthat isn’t on this list, if no index is found we return 0, which in a sorting context
means equal to:
instrs = instrs.sort_by {|i| ordered.index(i) || 0 }
We can also put the list of ordered instruments in a constant at the top of themodel file and then refer to that constant in the method That would probablymake the list easier to maintain It still has the disadvantage of having to beupdated manually, but in a production environment you could ensure that everytime a new instrument was introduced into the universe, a decision would have to
be made about where it fitted into the list (You could also start with a much ger list, of course.)
We now have a list of instrument names sorted according to conventionalinstrument-listing semantics What we now do with those names, for purposes ofinserting them into the nice title of the work, depends on how many there are:
■ If there are none, we want nil (not an empty string, for reasons that willbecome apparent when we put together the whole title)
■ If there’s just one, we want it by itself (Partita for Violin).
Trang 10■ If there are two, we want them joined by the word and (Sonata for Violin and
Piano).
■ If there are three or more, we want to join them with commas—except the last
two, which are additionally joined by and (Trio for Violin, Cello, and Piano).
It will be a matter of testing the size of instrs and proceeding accordingly Wecan do this with a case statement, with separate branches for each of the fourpossibilities:
The last case—more than two names—is worth examining up close It uses the
trick of grabbing all elements of the array except the last:
ddinstrs = instruments.map {|inst| inst.name }
ddordered = %w{ flute oboe violin viola cello piano orchestra }
ddinstrs = instrs.sort_by {|i| ordered.index(i) || 0 }
Trang 11ddddinstrs[0 -1].join(", ") + ", and " + instrs[-1]
ddend
end
That should give us a reasonably well-formatted, descriptive string for later tion into the nice title
inser-Formatting a work’s opus number
Let’s prettify the opus number next As you’ll recall, the opus field in the database
holds a string Due to the vagaries of indexing systems, several formats are ble for entries in this field:
possi-■ Plain opus number (“129”)
■ Opus number plus number designation (“129 no.4”)
■ Special catalogue designation, plus number (“K.84”, “BWV1005”, and so on)Plain opus numbers, and those with a number designation, should be rendered asthey are but with “op.” in front of them The more specialized designationsshould be rendered exactly as we find them
To accomplish this, we have to know whether the string in the opus field beginswith a series of digits If it does, we can assume that it’s in one of the first two cate-gories If it doesn’t, we can assume that it’s a specialized index like K or BWV
We can use a simple regular-expression match operation to test for a digit at thebeginning of the opus string and determine the correct return string accordingly:def nice_opus
The work’s prettified title
Creating a nice title is a matter of putting the nice components in place, with acouple of connector words The format is represented by this example:
Sonata in F Major, op 99, for cello and piano
B C D
B
Trang 12More is going on here than retrieving the parts We’re also connecting them with
a mixture of commas, spaces, and the word for The elements of the title are as
follows:
■ Title
■ If there’s a key, then the phrase “in ” + key
■ If there’s an opus number, then the sequence “, ” + nice_opus
■ If there are instruments, then the sequence “, for ” + nice_instruments The main thing is that if no key is indicated, we don’t want the word in, and like-
wise for the connecting strings for opus and instruments We therefore have to
put the nice title string together conditionally.
We do this as follows:
def nice_title
ddt,k,o,i = title, key, nice_opus, nice_instruments
dd"#{t} #{"in #{k}" if k}#{", #{o}" if o}#{", for #{i}" if i}"
end
First, the four pieces of information are retrieved and saved to variables with shortnames There are two reasons to do this First, it saves us from calling methodsmore than once If we used expressions like
" in #{k}" if k
This expression returns, for example, “in F major” if there’s a key If there’s no key(if k is nil) then the expression returns nil (the return value of a failed if state-ment) That nil, in turn, is interpolated in the outer string The string representa-tion of nil is “”, the empty string Therefore, if there’s no key, the whole "in#{k}"
ifk expression is rendered as an empty string and has no impact on the final string (The need to test truth-value with if, by the way, is why we havenice_instruments return nil rather than an empty string if the work has noinstruments Empty strings evaluate to true in a Boolean context; nil evaluates tofalse It’s possible to test a string for emptiness with empty?, but using nil allowsfor a quick Boolean check.)
Trang 13All told, you can get quite a bit of prettification mileage out of a decent edge of how to manipulate, test, and combine strings in Ruby.
knowl-A nice title for the Edition model
When it comes to titles of editions, there are two possibilities Some editions’ titlesare the titles of the one work they contain Other editions—collections of works,such as a volume of piano sonatas or string quartets—have one title encompassing
the whole collection: The Late String Quartets, for example, or Suites for
Unaccompa-nied Cello.
The steps we’ll take to define a nice title for an edition are as follows:
■ If the edition object has its own title, use that
■ If not, use the nice title of its first work (which is probably its only work—but for anomalous cases, this is a reasonable fallback)
■ In either case, add the publisher and year, in parentheses, after the title.We’ll do an “or” operation, using the Boolean operator ||, to handle the first twosteps This operator returns the value of the expression to its left if that expressionhas a Boolean value of true (like, for example, a non-nil title); otherwise, itreturns the value of the expression to its right We’ll then use a string additionoperation to handle the third step:
15.3.2 Calculating a work’s period
Let’s look at a more involved example: getting musical works to know what periodthey come from—not just by date, but by name or description
Teaching a work what its century is
One fairly easy way to do this is by century Here’s a method you can add towork.rb that causes each work to report what century it was written in:
def century
ddc = (year - 1).to_s[0,2].succ
ddc += case c
dddddddwhen "21" then "st"
Trang 14ddc + " century"
end
This method first determines a two-digit century equivalent of the year To do this,
it subtracts 1 from the year (so that the zero years, like 2000, land in the right tury) It then converts the year to a string and grabs the first two characters of thestring It increments the string with a succ (successor) so that 19, for example,ends up as 20, which is the correct century designation
Next comes an algorithm for adding the correct suffix to the century This
algorithm only works as far back as the fourth century; it won’t hand the rd suffix
of 3rd correctly Because the music we’re selling tends to date from a lot later than
the third century, that shouldn’t be a problem
Centuries are fine, although they’re easy to glean by looking at the year Youcan also get musical works to give you descriptive information about their period
A more descriptive periodization of a work
Like painting, literature, architecture, and other arts, musical works are oftendescribed not just by year or century but by terms referring descriptively to a period:
baroque, classical, romantic, and so forth With a little ingenuity, it’s possible to get
musical works to tell you what period they’re from and to do so programmatically The first step is to make a set of decisions about the period descriptions It’s pos-sible to associate a given time period with a description However, and in spite ofthe fact that it involves a bit more work up front, a more scalable approach is to
define each period as a combination of time and place For example, we might want British music of the nineteenth century (at least, most of it) to be described as Vic-
torian, whereas that term wouldn’t make sense for music from Italy or France.
We’re looking for a Ruby data structure that lets us make connections amongtime spans, countries, and descriptive period names
There are a couple of tools we can reach for One possibility is to create a newclass, encapsulating periods, along these lines:
Trang 15We could then write the time and country specifications for, say, the Baroqueperiod Music historians might argue one way or the other about the details, butwe’ll go ahead and define it like this:
If you put this code in a file in the lib subdirectory of the music application, it will
be visible from the model files at runtime You could then write a method thatculled all the existing periods and searched them on certain criteria
Nothing is wrong with this code in principle, and it would be feasible in tice But there’s another valid approach to the problem: storing the period infor-mation in a hash This hash can live inside work.rb or in a separately loaded file inthe lib directory We’ll take the former approach here
A period hash can be constructed in any of several ways One way or another,you must include a range of dates, a list of countries, and a descriptive tag (like
“Baroque”) Something has to be the key, and something has to be the value, foreach entry
Because we have three pieces of information to record for each period, andhashes are fundamentally based on pairs rather than triples, we need to combinetwo of the items into one object—presumably an array The most logical choice isfor each hash entry to have an array containing the time span of the period alongwith the countries Such an array looks like this:
[1650 1750, %w{ EN DE FR IT ES NL }]
This array contains two elements:
■ A range, bracketing the years covered by the period
■ An inner array of country designations (England, Germany, France, Italy,
Spain, the Netherlands)
NOTE RANGE OBJECTS A range is an object with a starting point and an ending
point and the ability to be queried as to whether it does or doesn’tinclude a particular value The range 1650 1750, for example, includes
1697 but doesn’t include 1811 The two numbers with two dots between
them are a range literal If you use three dots, the range excludes its own
endpoint; with two, it includes the endpoint Some ranges, but not all,can also be iterated through, like arrays For purposes of dating music,we’re only interested in being able to determine whether a given yearfalls inside the range
Trang 16The primary remaining task is to link this array (and several others like it) todescriptive period tags—“Baroque”, in the case of the example
But should the arrays serve as hash keys, or hash values? In other words, do wewant a typical pair in the period hash to look like this
search-of the period is the key Because we’re going from the match criteria to the name,
the left-to-right orientation makes a good visual fit
We’ll put the period criteria in a hash Let’s make it a constant in the Work class:PERIODS = { [1650 1750, %w{ EN DE FR IT ES NL}] => "Baroque",
We now need to write an instance method for the Work class that searches thishash and finds a match based on the work’s year and country In the event that nomatch is found for a given work, we’ll fall back on the default of providing thework’s century
Here is the period method:
def period
ddpkey = PERIODS.keys.find do |yrange,countries|
ddddyrange.include?(year) && countries.include?(country)
Trang 17The && operator tests the expression to its left for truth-value, returning the value
of the expression on the right if the expression on the left has a Boolean value of
true If the expression on the right isn’t true—if it evaluates to false or nil—thewhole expression returns false or nil
Starting at the end of the method dd, you can see that it uses an or test to return
either a value from the PERIODS hash or the work’s century If PERIODS[pkey]returns something true, which it will if pkey is an existing key of PERIODS (rememberthat strings like “Classical” are true in the Boolean sense), then the method returnsthat value If not (in other words, if pkey isn’t an existing key, and specifically if pkey
is nil), the method returns the work’s century
pkey is calculated by iterating through the keys of the PERIODS hash dd Each key,
as you’ll recall, is an array consisting of a range of years (assigned to yrange) and anarray of countries (assigned to countries) If there’s a hash key whose year-rangeincludes year and whose country-array includes country, that hash key is assigned
to pkey The and test is performed with the && operator If no key is found thatpasses the double test, pkey is nil and, subsequently, PERIODS[pkey] is also nil If
a key passes the tests, you get the corresponding value when you ask for it dd
We now have a programmatic way to get a work to report its artistic period
We also have a good example of a case where doing something programmaticallyhas distinct advantages over just putting data in a database Yes, we could just cre-
ate a field in the works table that contained the period But by calculating the
period dynamically, we’ve made it a lot easier to make additions and changes Anentire chart of periods is available at a glance and can be modified and aug-mented as needed
On the other side of the convenience equation, if you were migrating the base to another application, you’d have to reconstruct a way to get at the periodinformation, since it wouldn’t be in the database—or you’d have to redesign thedatabase and write a script that determined each work’s period and put it in adatabase field after all And in making real-world decisions about data storage ver-sus programmatic calculation of values, you do have to weigh considerations ofthat kind
We’ll settle on doing periods programmatically, on the theory that the musicstore application will be stable and fairly permanent
Now we’ll return to a strong candidate for a considerable amount of hardmodel enhancement: the Customer
C
B
B
Trang 1815.3.3 The remaining business of the Customer
The Customer model can be enhanced in a number of ways, and we’re going to
do several We’ll start by developing code to determine various rankings—the
cus-tomer’s favorites in various categories We’ll then move from rankings to businesscalculations, including the customer’s order history and outstanding balance.Finally, we’ll teach the customer how to check out (complete all pending orders)
Rankings per customer
It’s popular for online shopping sites to put links and special offers on the screenbased on a customer’s known favorite items Some of this information may bestored on the server or on the customer’s computer in the form of cookies Some
of it (if there isn’t too much to do reasonably quickly) can be calculated in realtime based on the customer’s searching and/or ordering history
We’ll perform a couple of calculations of this type: determining this tomer’s favorite composers and instruments (Both of these methods are instancemethods of the Customer class and therefore belong in the customer.rb modelfile; but they’ll undergo some revision before they’re final.)
The two methods work in similar ways First, they create an array of the item’shistory (composer or instrument) by traversing either the edition history or thework history This array is in chronological order; the most recently ordered com-
posers or instruments are last (This happens automatically, because of the way
we’ve specified the ordering of order objects That order propagates toedition_history and work_history.)
Next, we run uniq on the array, because we only want to rank each item once Therank is based on how many times the item occurs in the complete array Finally, wereverse the result Because the number of occurrences is higher for the favorites,they’re at the end of the array—so we reverse it, to put them at the beginning:def composer_rankings
ddhistory = edition_history.map {|ed| ed.composers }.flatten
Trang 19These two methods will work, but even a glance at them glaringly reveals the factthat they’re almost identical You can trim them down a lot by extracting theircommon code into a separate method and calling that method where it’s needed:def rank(list)
Let’s say someone orders:
■ A work for cello and piano
■ A work for cello and orchestra
■ A work for orchestra
That means our pre-flattened instrument history is
[["cello", "piano"], ["cello", "orchestra"], ["orchestra"]]
and the flattened version is
["cello","piano","cello","orchestra","orchestra"]
We then send this array to the rank method Going through these one at a time,and never repeating an item (thanks to uniq), rank sorts them by how often theyoccur in the non-uniqued list The statistics are as follows:
Trang 20The almost-final order, then, is piano, cello, orchestra Because we want the list in
descending order of favoriteness (most favorite first in the array; least favorite
last), we reverse it: orchestra, cello, piano That gives us a reasonable representation
of this customer’s most- and/or most-recently ordered instruments
NOTE ALGORITHM GRANULARITY The algorithm we’re using to determine
favorites is reasonably fine-grained It’s slightly vulnerable, however, to
the ordering of instruments within a work or composers within an tion If works for cello and piano are listed with piano first, and cello and
edi-piano are tied, edi-piano will come out ahead If they’re listed the other way
around, cello will (You can try this in irb Paste the rank method directly
into the irb session—it can operate as a standalone, top-level methodinside irb—and then look at the difference between rank(%w{cpcp})and rank(%w{p c p c}).) It would be possible to store items in a hashthat kept closer track of ties, but it’s questionable whether the effort to
do this would pay off After all, when it comes to displaying favoriteitems on the screen, you’d probably end up choosing among the tieditems anyway Moreover, if you wanted to be more nuanced, you could
do something along the lines of the instrument-ordering we did for thenice_instruments method—perhaps write a weighted_instrumentsmethod and then call that instead of instruments It would still be nomore than a calculated guess With instruments, this kind of pre-rankweighting would be hard to justify; with composers, impossible Deter-mining customer favorites is a fuzzy process (as anyone knows who hasseen his or her own favorites page on a shopping site populated withsuggestions based on items ordered as gifts for other people)
We now have a way to determine customer favorites—and we’ll come back to itand complete the picture, when we get to controllers and views in the next chap-ter Meanwhile, let’s turn now to the business end of the customer: the methodswe’ll need as a foundation for accepting orders and calculating costs
Calculating the number of copies ordered
We need a way to know how many copies of a given edition a customer hasordered It would be possible, and plausible, to design the application and thedatabase so that this number was stored in the database and incremented whenthe customer changed the number of copies of an edition or ordered anothercopy However, order counting is also a good example of a case where you mightcalculate a value on the fly, programmatically; and that’s how we’ll do it here The following method, an instance method of Customer, tells us how many cop-ies of a given edition the customer has ordered:
Trang 21ID field with the edition_id field in the database records for the customer’s orders.
(Note the use of SQL, and in particular the single equal-sign for comparison whereyou would use == in Ruby.) Finally, we take the size of this subset of the customer’sorders; this tells us how many copies of this edition the customer has ordered
Remaining unpaid balance
At some point, we’ll need to be able to calculate how much the customer owes forunpaid orders (their unpaid balance) We already have a method that returns anarray of all the customer’s open orders All we need to do is add up the prices ofthe items in that array
Let’s look at two ways to do this: one by hand and one using the Array#injectmethod The first version of the method looks like this:
This kind of operation—iterating through a collection and accumulating theresults of some calculation incrementally—can be done automatically with theinject method This method initializes an accumulator (in this case, zero) andthen iterates through the array On the first iteration, inject yields two values tothe block: the accumulator object and the first element of the array On the sec-ond and subsequent iterations, it also yields two values: the return value of theprevious call to the block and the current element of the array
The inject-based version of the balance method looks like this:
Trang 22We’ll write the controller action in the next chapter Here, let’s specify whatthe customer object has to do with regard to the state of its own data when itchecks out It needs to change the status of every order to “paid”:
Either of these techniques is acceptable
We’ve now added a considerable amount of functionality to the composer, work,and customer models in the form of both soft and hard programmatic enhance-ment This draws us closer to a reasonably functional music store; and, most impor-
tant, it provides a display of the kinds of things you can do to, and with, your
ActiveRecord models when you know how to add programmatic value to them