-
Notifications
You must be signed in to change notification settings - Fork 45
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Lindsey Deuel - Pipes - Hotel #24
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was mentioned to me that you were interested in a thorough / more "real-world" review of your Hotel project, so I've started on that process.
Thus far I've only reviewed a few files (daterange_specs.rb
, hotel_specs.rb
, and hotel.rb
), but I plan to follow this up with the remainder of my review.
I've also got some general comments that aren't specific to any particular line:
- Indentation - There is a mix of spaces and tabs in some files (
specs/daterange_spec.rb
). We should stick to consistent use of either spaces or tabs, mixing them will frequently result in code that appears to be mis-indented depending on your editor's settings for tab width. The Ruby Style Guide recommands using two spaces per indentation, and no tabs. If you want to automatically check whether your code meets the style guide you can use Rubocop. - Using overly complex data structures - This may be a result of reading the user stories and building data structures that match the functionality in those stories as closely as possible, or it may be the result of thinking about how to make the code more performant. In either case, the use of hashes in several places actually complicates the code a fair bit and we could simplify things by sticking to bare arrays and judicious use of Enumberable methods.
- More logic should be moved out of
Hotel
and into eitherDateRange
,Block_Of_Rooms
orReservation
as appropriate. The reason for this is less about DRYing up the code (becauseHotel
is the only use of this functionality) and more about designing your application by defining new verbs (likeDateRange#overlaps?
) and nouns (likeReservation
) that can be used to construct more complex sentences (likeHotel#reserve_room
) without specifying all of the details "in-line". - The choice to allow the user to specify a particular room number when making a single or block reservation significantly complicates the logic involved with the reservation process. This isn't a criticism at all, just an observation (we didn't specify either way what your application should support).
Feel free to check out my (in-progress) implementation of the Hotel project for additional inspiration on how to simply the code.
@@ -0,0 +1,72 @@ | |||
<!DOCTYPE html> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general we don't want to check into Git any generated files, such as this file and all others in the specs/
path (they are generated by Simplecov when we run our tests), and all .DS_Store
files (they are generated by the macOS Finder).
The theory behind that rule is that Git will store all of the original data created by people, and if we ever need to obtain the software-generated files we can git checkout
the appropriate version and re-run the software.
This is not always the case because not all software is deterministic, or the generated files might have made use of data that's not tracked by Git. However, in most cases we'll see at Ada the rule works fine.
So I think our best practice for future projects should be to start the project off with a .gitignore
file which will tell Git to prevent checking in any files we know we don't need to track.
Thankfully other developers have put in the legwork to identify the most common generated files and have created pre-built .gitignore
files for us. There's one for macOS, one for Ruby, and even one for Rails when we get there. You can use gitignore.io to automatically generate a custom file by mixing together OS- and language-specific ones.
Using these templates is not always enough, though. In some cases we know about specific software or gems we're using which will generate files we want to ignore. In that case we'll need to make our own additions to the .gitignore
file. For this project we would want to add a line that said /specs/coverage/
to ignore the Simplecov generated files.
it "checks initialize when start date is after end date" do | ||
start_date = Date.new(2017,1,1) | ||
end_date = Date.new(2017,1,3) | ||
proc {DateRange.new(end_date, start_date)}.must_raise Exception |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When using the must_raise
matcher we should be as specific as possible about what we expect to be raised. When we are not specific enough we risk that the test will pass even when unexpected errors occur, which can obscure bugs we would otherwise be notified about.
At the very least we should stick to expecting StandardError
instead of Exception
. The reasons for not raise
-ing or expecting Exception
are detailed here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additionally, I would recommend changing the code in this test so that we create start_date
with a later date than end_date
, rather than swapping them in the call to DateRange.new
.
My reasons for this are purely subjective, but even after having seen this pattern in other students' code it took me a couple of readings to catch why this test was passing. My guess is that most other programmers would recognize that the variables are initialized to "bad" values more quickly than they would notice the parameters are transposed.
require_relative '../lib/daterange' | ||
|
||
describe "DateRange" do | ||
describe "initialize" do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could have another test for initialize
verifying that it raises ArgumentError
if either of the parameters is not a Date
object.
|
||
hotel.must_respond_to :reservations_by_room | ||
keys = hotel.reservations_by_room.keys | ||
keys.length.must_equal 20 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could also check that hotel.reservations_by_room.keys.sort.must_equal hotel.rooms.sort
|
||
hotel.reserve_room(1, date) | ||
|
||
hotel.reservations_by_room[1].wont_be_nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To make this test more meaningful we should probably also check that hotel.reservations_by_room[1]
is nil
before calling reserve_room
.
However, from the code in Hotel#initialize
we can see that each entry in reservations_by_room
starts as an empty array and therefor is not nil
.
So what we should be checking in this test is that reservations_by_room[1]
increases in length by 1.
end | ||
|
||
|
||
def get_all_rooms |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method is exactly equivalent to the method that attr_reader :rooms
gets us, so we can safely remove this (and change any references to it to rooms
instead).
open_rooms = [] | ||
#makes a boolean array flagging all rooms as available | ||
20.times do |i| | ||
open_rooms[i] = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are a few places in the codebase where we are using this technique of creating an array of booleans that maps to an array of other values, then we loop through the values and set the booleans according to some conditional checks, and finally we create a new array from all of the values which are now mapped to a true boolean.
This is a pretty common pattern in programming, generally referred to filtering, and Ruby's Enumerable class has a method for it: Enumerable#select
.
We could reduce the code needed for this method by using it thusly:
def get_open_rooms(date)
available_rooms = reservations_by_date.values.flatten.select do |res|
# This should be a valid check for overlapping date ranges
(res.start_date <= date.start_date && res.end_date > date.start_date) ||
(date.start_date <= res.start_date && date.end_date > res.start_date)
end.map(&:room_number)
return available_rooms
end
As per my other comments, the above implementation could be simplified further by using a single @reservations
array, and by implementing the date check as something like DateRange#overlap?
.
if number_of_rooms > 5 | ||
raise ArgumentException, "Room blocks must be 5 rooms or less" | ||
elsif number_of_rooms > available_rooms.length | ||
raise ArgumentException, "There are only #{available_rooms.length} available for the dates selected" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because both of these first two cases end in a raise
statement we can assume that if either of them are hit we won't continue running the rest of this method. This means we don't need to contain the actual "meat" of the method's logic inside of an else
block.
room = get_open_room_from_block(current_block) | ||
new_reservation = Hotels::Reservation.new(room, current_block.date, 175) | ||
else | ||
puts "There are no rooms avaiable for this block" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be raising an error of some kind (probably a custom one), instead of printing to the screen with puts
.
@@ -0,0 +1,17 @@ | |||
require_relative 'room' | |||
module Hotels | |||
class Block_Of_Rooms < Room |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general with Ruby we name classes using CamelCase
rather than (capitalized) Snake_Case
.
rooms = block.room_booked | ||
rooms.each_with_index do |value, index| | ||
if value == false | ||
block.room_booked[index] = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should avoid doing something like this as much as possible. Specifically what we're doing here is accessing a "read-only" attribute from the Block_Of_Rooms
class (room_booked
) and then modifying it.
Technically Ruby lets us modify the room_booked
array because all arrays in Ruby can always be modified, but the intent implied by attr_reader :room_booked
is that code from outside of the Book_Of_Rooms
class will not be changing it. Basically this represents a case of very tight coupling between these two classes, and breaks the principle of encapsulation that is fundamental to Object Oriented Programming.
Instead we should add a method to Block_Of_Rooms
which takes the index
variable and internally updates the room_booked
array. That way, if we need to refactor Block_Of_Rooms
in the future, possibly to not use an array for room_booked
at all, our code here will not break.
HotelWhat We're Looking For
|
Hotel
Congratulations! You're submitting your assignment!
Comprehension Questions