diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..01d6d7b5b Binary files /dev/null and b/.DS_Store differ diff --git a/Rakefile b/Rakefile new file mode 100644 index 000000000..deb52f2cd --- /dev/null +++ b/Rakefile @@ -0,0 +1,9 @@ +require 'rake/testtask' + +Rake::TestTask.new do |t| + t.libs = ["lib"] + t.warning = true + t.test_files = FileList['specs/*_spec.rb'] +end + +task default: :test diff --git a/design-activity.md b/design-activity.md new file mode 100644 index 000000000..36aa74b8e --- /dev/null +++ b/design-activity.md @@ -0,0 +1,88 @@ + + + + +Implementation A classes: CartEntry, ShoppingCart and Order +Implementation B classes: CartEntry, ShoppingCart and Order + +The list of classes between implementation A and B are the same. + + + +Implementation A classes: Both the CartEntry and ShoppingCart classes carry one single responsibility. However, all the logic to carry out those various responsibilities, such as calculating the subtotal and the total, are located in one place inside the Order class. The Order class is the "master" class that is in charge of all behavior, and other classes are only used to store state. +Implementation B classes: The logic has been delegated to "low level" classes, CartEntry and ShoppingCart. The Order class no longer carries all the behavior. + + + +Implementation A classes: The Order class creates an instance of the ShoppingCart class in its initialize. Then in the total_price method it carries out all the logic by finding each entry's unit price and quantity and calculating the subtotal and then the final total with tax included. + +Implementation B classes: Each class carries its own singal responsibility. The CartEntry class takes in each entry's quantity and price and calculates that entry's total price. Then, the ShoppingCart class adds up all entries and calculates the subtotal. Finally, the Order class calculates the final total with the sales tax. + + + +Implementation A classes: +CartEntry - unit_price & quantity +ShoppingCart - all entries +Order - the sales tax, all entries, the price of each entry, the subtotal and the final total with the sales tax + +Implementation B classes: +CartEntry - unit_price & quantity and the price of each entry +ShoppingCart - all entries and the subtotal +Order - the sales tax, all entries and the final total with the sales tax + +The Order class in implementation A carries all the responsibilities. While the responsibility is spread out evenly between all three classes in implementation B. + + + +Implementation A classes: +CartEntry - initialize +ShoppingCart - initialize +Order - initialize and total_price + +Implementation B classes: +CartEntry - initialize and price +ShoppingCart - initialize and price +Order - initialize and total_price + +The Order class in implementation A carries all the responsibilities. While the responsibility of calculating the prices are spread out evenly between all three classes in implementation B. + + + + + +Implementation A classes: the logic to compute the price is retained in Order. + +Implementation B classes: the logic to compute the price is delegated to "lower level" classes. + + + +Implementation A classes: yes + +Implementation B classes: no + + + +If items are cheaper when bought in bulk, then we could apply a discount to quantities that are equal or over a certain specific amount. This would be easier to modify in implementation B because the of how logic is evenly delegated between each class, the discount can be added in any of the three classes and its methods. Whereas in implementation A, the only class that the logic can go into is the Order class. + + + +Implementation B better adheres to the single responsibility principles because each class is responsible for one thing and the dependency the Order class has on the other two classes is very loose. + + + +Continuing off of my previous answer, implementation B is more loosely coupled because if there is a change in one class it will not affect the other two classes. Especially in the Order class. Whereas in implementation A, if any changes were to be done to the first two classes, this could drastically change the code of Order class because it depends on both "low level" classes. + + + + +Based on the answers to the above questions, one of the places in my Hotel project where a class takes on multiple roles is the Admin class. The Admin class has dependency on both the Room and Reservation class. Also, the private method to check the date range and to see if the date inputs are valid are repeated in both the Admin and Reservation class. + +Here are the steps I took to make my Hotel project more Object Oriented with single responsibilities: + +-> Created new files date_range.rb and date_range_spec.rb +-> Created the DateRange class to remove the date_range method from the Reservation and Admin classes. Also, the date_check method, which stayed as a private method. +-> Updated the Reservation class to not be dependent on the Room class and created instance variables, rate. +-> Updated the Admin class to not be dependent of the Room class. +-> Updated the specs accordingly and made sure the dependency is as loose as possible. + +Afte the re-design that I made, I believe the logic is no longer heavily within Admin class. Although the logic could be spread out more evenly onto the Room class. The Admin class is now only dependent on the Revservation class. Maybe if I am able to add the block function for Hotel wave 3 then I will consider moving more logic into the Room class. diff --git a/lib/.DS_Store b/lib/.DS_Store new file mode 100644 index 000000000..220d27202 Binary files /dev/null and b/lib/.DS_Store differ diff --git a/lib/admin.rb b/lib/admin.rb new file mode 100644 index 000000000..c44a5eb63 --- /dev/null +++ b/lib/admin.rb @@ -0,0 +1,78 @@ +require_relative 'reservation' + +module Hotel + class Admin + attr_reader :rooms, :reservations, :room_rate + + def initialize + @rooms = (1..20).to_a + @room_rate = 200 + @reservations = [] + end + + def add_reservation(room, check_in, check_out) + + # Check that the room exists + unless @rooms.include? room + raise ArgumentError.new("Invalid room number: #{room}") + end + + # Check that the room is available + unless available_rooms(check_in, check_out).include? room + raise ArgumentError.new("Room #{room} already has a reservation between #{check_in} and #{check_out}") + end + + reservation = Reservation.new(check_in, check_out, room, @room_rate) + @reservations << reservation + + return reservation + end + + def list_reservations(date) + + reserved_list = [] + + @reservations.each do |reservation| + if reservation.check_in <= date && reservation.check_out >= date + reserved_list << reservation + end + end + + return reserved_list + + end + + def available_rooms(check_in, check_out) + + DateRange.new(check_in, check_out) + + available_list = [] + + (check_in...check_out).each do |date| + available_list << empty_rooms(date) + end + + available_rooms = available_list[0] + (available_list.length - 1).times do |i| + available_rooms = available_rooms & available_list[i + 1] + end + + return available_rooms + + end + + private + + # check to see if the given date input has any avilable empty rooms + def empty_rooms(date) + reservation_list = list_reservations(date) + reserved_rooms = [] + + reservation_list.each do |reservation| + reserved_rooms << reservation.room + end + return @rooms - reserved_rooms + end + + end # end of Admin class +end # end of Hotel module diff --git a/lib/date_range.rb b/lib/date_range.rb new file mode 100644 index 000000000..279022236 --- /dev/null +++ b/lib/date_range.rb @@ -0,0 +1,33 @@ +module Hotel + class DateRange + class InvalidDateRange < StandardError ; end + + attr_reader :check_in, :check_out + + def initialize(check_in, check_out) + unless check_out > check_in + raise InvalidDateRange.new("Invalid dates #{check_in} to #{check_out}") + end + + @check_in = date_check(check_in) + @check_out = date_check(check_out) + end + + def nights + return @check_out - @check_in + end + + private + + # check to see if the check_in and check_out dates are invalid + def date_check(date) + if date.class == String + date = Date.parse(date) + elsif date.class != Date + raise ArgumentError.new("Invalid date: #{date}. Please enter date in the form of '2018, 03, 10'") + end + return date + end + + end +end diff --git a/lib/reservation.rb b/lib/reservation.rb new file mode 100644 index 000000000..b705094e6 --- /dev/null +++ b/lib/reservation.rb @@ -0,0 +1,18 @@ +require_relative 'date_range' + +module Hotel + class Reservation < DateRange + attr_reader :room, :rate + + def initialize(check_in, check_out, room, rate) + super(check_in, check_out) + @room = room + @rate = rate + end + + def total_cost + return nights * @rate + end + + end # end of Reservation class +end # end of Hotel module diff --git a/lib/room.rb b/lib/room.rb new file mode 100644 index 000000000..9ccc12cae --- /dev/null +++ b/lib/room.rb @@ -0,0 +1,20 @@ + +module Hotel + class Room + attr_reader :room, :rate + + def initialize(room, rate) + @room = hotel_room(room) + @rate = rate + end + + # raises ArgumentError if the room number is not between 1 and 20 + def hotel_room(room) + unless (1..20).include? room + raise ArgumentError.new("Invalid room number: #{room}") + end + @room = room + end + + end # end of Room class +end # end of Hotel module diff --git a/specs/.DS_Store b/specs/.DS_Store new file mode 100644 index 000000000..bf2b54724 Binary files /dev/null and b/specs/.DS_Store differ diff --git a/specs/admin_spec.rb b/specs/admin_spec.rb new file mode 100644 index 000000000..907273f8f --- /dev/null +++ b/specs/admin_spec.rb @@ -0,0 +1,117 @@ +require_relative 'spec_helper' + +describe "Admin class" do + + before do + @admin = Hotel::Admin.new + end + + describe "initialize" do + + it "must be an instance of admin" do + @admin.must_be_instance_of Hotel::Admin + end + + it "has access to a list of all 20 rooms" do + @admin.must_respond_to :rooms + @admin.rooms.must_be_kind_of Array + @admin.rooms.length.must_equal 20 + end + + it "tracks room rate" do + @admin.must_respond_to :room_rate + @admin.room_rate.must_equal 200 + end + + it "access to reservations" do + @admin.must_respond_to :reservations + @admin.reservations.must_be_kind_of Array + @admin.reservations.length.must_equal 0 + end + + end # end of describe "initialize" + + describe "add_reservation method" do + before do + @check_in = Date.new(2018, 4, 14) + @check_out = Date.new(2018, 4, 21) + @room = 9 + @reservation = @admin.add_reservation(@room, @check_in, @check_out) + end + + it "creates a instance of reservation with the check_in and check_out dates" do + @reservation.must_be_instance_of Hotel::Reservation + end + + it "raises an error for an invalid room" do + [105, -12, "foo"].each do |room| + proc { + @admin.add_reservation(room, @check_in, @check_out) + }.must_raise ArgumentError + end + end + + it "raises an error if no rooms are available" do + @admin.add_reservation(10, @check_in, @check_out) + + proc { + @admin.add_reservation(10, @check_in, @check_out) + }.must_raise ArgumentError + end + + it "correctly list the reserved room" do + @reservation.check_in.must_equal @check_in + @reservation.check_out.must_equal @check_out + @reservation.check_in.must_be_kind_of Date + @reservation.check_out.must_be_kind_of Date + @reservation.room.must_be_kind_of Integer + @reservation.total_cost.must_equal 1400 + end + + it "accurately addes the reserved room to the reservation list" do + number_of_reserved_rooms = @admin.reservations.length + + @admin.reservations.length.must_equal number_of_reserved_rooms + end + + end # end of describe "add_reservation method" do + + describe "list_reservations method" do + before do + @admin.add_reservation(1, Date.new(2018, 3, 22), Date.new(2018, 3, 24)) + @admin.add_reservation(2, Date.new(2018, 4, 21), Date.new(2018, 4, 24)) + @admin.add_reservation(3, Date.new(2018, 4, 22), Date.new(2018, 4, 24)) + end + + it "correctly adds all the new reservations" do + reservations = @admin.list_reservations(Date.new(2018, 4, 22)) + + @admin.reservations.length.must_equal 3 + reservations.length.must_equal 2 + end + + it "returns zero if no reservations are found" do + reservations = @admin.list_reservations(Date.new(2018, 3, 10)) + + reservations.length.must_equal 0 + end + + end # end of describe "list_reservations method" + + describe "available_rooms method" do + before do + @check_in = Date.new(2018, 4, 14) + @check_out = Date.new(2018, 4, 21) + end + + it "lists out all the empty rooms with the given date range" do + @admin.add_reservation(12, @check_in, @check_out) + + available_list = @admin.available_rooms(@check_in, @check_out) + + available_list.length.must_equal 19 + available_list.must_be_kind_of Array + end + + end # end of describe "available_rooms method" +end # end of describe "Admin class" diff --git a/specs/date_range_spec.rb b/specs/date_range_spec.rb new file mode 100644 index 000000000..6249525cc --- /dev/null +++ b/specs/date_range_spec.rb @@ -0,0 +1,52 @@ +require_relative 'spec_helper' + +describe Hotel::DateRange do + + describe "initialize" do + it "can be initialized with two dates" do + check_in = Date.new(2017, 01, 01) + check_out = Date.new(2017, 01, 04) + + @range = Hotel::DateRange.new(check_in, check_out) + + @range.check_in.must_equal check_in + @range.check_out.must_equal check_out + end + + it "correctly returns the date if the check_in and check_out dates are strings" do + check_in = "2018-4-14" + check_out = "2018-4-21" + @range = Hotel::DateRange.new(check_in, check_out) + + @range.check_in.must_be_kind_of Date + @range.check_out.must_be_kind_of Date + end + + it "must raise an ArgumentError if the check_in and/or check_out date is invalid" do + check_in = 201841 + check_out = 2018410 + proc { Hotel::DateRange.new(check_in, check_out) }.must_raise ArgumentError + end + + it "must raise an ArgumentError if the date range is invalid" do + check_in = Date.new(2018, 4, 12) + check_out = Date.new(2018, 4, 10) + proc { + Hotel::DateRange.new(check_in, check_out) + }.must_raise Hotel::DateRange::InvalidDateRange + end + end + + describe "nights" do + it "returns the correct number of nights" do + nights = 3 + check_in = Date.new(2017, 01, 01) + check_out = check_in + nights + + @range = Hotel::DateRange.new(check_in, check_out) + + @range.nights.must_equal nights + end + end + +end diff --git a/specs/reservation_spec.rb b/specs/reservation_spec.rb new file mode 100644 index 000000000..6b6811f20 --- /dev/null +++ b/specs/reservation_spec.rb @@ -0,0 +1,51 @@ +require_relative 'spec_helper' + +describe "Reservation class" do + + before do + @check_in = Date.new(2018, 4, 14) + @check_out = Date.new(2018, 4, 21) + @room = 9 + @rate = 200 + @reservation = Hotel::Reservation.new(@check_in, @check_out, @room, @rate) + end + + describe "initialize" do + + it "must be an instance of reservation" do + @reservation.must_be_instance_of Hotel::Reservation + end + + it "reads the check_in date" do + @reservation.check_in.must_be_kind_of Date + @reservation.check_in.must_equal @check_in + end + + it "reads the check_out date" do + @reservation.check_out.must_be_kind_of Date + @reservation.check_out.must_equal @check_out + end + + it "is a kind of DateRange" do + @reservation.must_be_kind_of Hotel::DateRange + end + + it "reads in the correct room information" do + @reservation.must_respond_to :room + @reservation.room.must_equal 9 + end + + it "reads in the correct rate information" do + @reservation.must_respond_to :rate + @reservation.rate.must_equal 200 + end + end # end of describe "initialize" + + describe "calculate_total_cost" do + it "calculates the price" do + expected_price = @rate * (@check_out - @check_in) + @reservation.total_cost.must_equal expected_price + end + end + +end # end of describe "Reservation class" diff --git a/specs/room_spec.rb b/specs/room_spec.rb new file mode 100644 index 000000000..5287915ba --- /dev/null +++ b/specs/room_spec.rb @@ -0,0 +1,37 @@ +require_relative 'spec_helper' + +describe "Room class" do + + describe "initialize" do + before do + @room = Hotel::Room.new(9, 200) + end + + it "must be an instance of Room" do + @room.must_be_instance_of Hotel::Room + end + + it "reads the room number" do + @room.must_respond_to :room + @room.room.must_be_kind_of Integer + @room.room.must_equal 9 + end + + it "reads the rate of the @room" do + @room = Hotel::Room.new(9, 200) + @room.must_respond_to :rate + @room.rate.must_be_instance_of Integer + @room.rate.must_equal 200 + end + + end # end of describe "initialize" + + describe "hotel_room method" do + + it "raises an ArgumentError for invalid room numbers" do + proc {Hotel::Room.new(0, 200)}.must_raise ArgumentError + proc {Hotel::Room.new(21, 200)}.must_raise ArgumentError + end + + end # end of describe "hotel_room method" +end # end of describe "Room class" diff --git a/specs/spec_helper.rb b/specs/spec_helper.rb new file mode 100644 index 000000000..f9d255583 --- /dev/null +++ b/specs/spec_helper.rb @@ -0,0 +1,17 @@ +require 'simplecov' +SimpleCov.start do + add_filter "/specs/" +end + +require 'date' +require 'minitest' +require 'minitest/autorun' +require 'minitest/reporters' +require 'minitest/skip_dsl' +require 'pry' + +Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new + +require_relative '../lib/admin' +require_relative '../lib/reservation' +require_relative '../lib/room'