28-01-2022
Meta Stuff
config, updates, installs, software, etc
Today's Foci
Focus 1
- What/How?
- Measure
If I'm with others ask how productive they want to be, and determine how chatty I should be in return
- ask 1 / 10?
- what are you goals in this session?
Topic
Challenge! Which one?
Both have twilio / text stretch goals appear to use a twilio gem 'twilio-ruby'
Use Mocks/stubs so that actual texts aren't sent
Use ruby environment variables
The office management has 10 simple user stories
MeetingRoomOBJ with
- attributes @name:String, @available:Bool, methods
- check_room return @available
- enter_room @available = false - Raise error if entering occupied room
- exit_room @available = true
OfficeOBJ with
-
attributes @listrooms:Array
-
methods add_room @listrooms push MeetingRoomOBJ
-
available @listrooms while MeetingRoomOBJ.available == true
ManagementInterfaceOBJ ( uses twilio API )
-
attributes
-
methods
-
room_free ? return array of rooms or a single room? maybe both? room x is free here is a list of rooms? goes beyond User Story, but I would intuit that is what the client wants.
- while array @listrooms, if element ( MeetingRoomOBJ ).exit_room, activate twilio API, sending MeetingRoom, and list of available rooms?
-
room_occupied returns from @listrooms, while MeetingRoom.available==false, return MeetingRoomOBJ.name and MeetingRoomOBJ.team?
?
list of teams or team object? see how much time I have I guess?
TeamOBJ (used for entering rooms) attribute name:String?
if so, modify the MeetingRoomOBJ.enter and .exit methods to take a Team object and return the TeamOBJ.name
I think the takeaway challenges are a little more than meets the eye - but there are only 4 of them. Hmmm.
I'm leaning toward the takeaway challenge
takeaway - array of hashes for dish=>price
from above array, remove items
show array.val(returns price), individually and array.sum
receive a text to confirm the order has been placed and a time (datetime + 30 mins?)
30 mins down, rough plan - Office Task decided
Office seems more interesting!
Before anything, lets use the Apprenticeship Providers advice and make use of their fantastic tables! (hidden somewhere in my resources file...)
User Stories
As a staff member
In order to distinguish between meeting rooms *names must be unique?*
I would like my meeting room to have a name
As an office manager
So that staff can coordinate meetings
I would like to add a meeting room to the office
As an office manager
So that I can manage meeting rooms
I would like to list all the meeting rooms in the office
As a staff member
In order to have meeting,
I would like to check if the meeting room is available or not (true or false)
As a staff member
In order to have a meeting,
I would like to be able to enter the meeting room and this should make it unavailable
As a staff member
In order to end a meeting
I would like to be able to leave the meeting room and this should make it available again
As a staff member
So that I can see where to have my meeting
I would like to be able to see a list of available rooms in the office
As a staff member
So that I can avoid interrupting a meeting
I would like an error if I try to use a room that has already been entered
As an office manager
So that I can find out when a room becomes available
I would like to receive a text message whenever a meeting room becomes available again
As an office manager
So that I can have visibility of how the rooms are being used
I would like to see the name of the meeting and the name of the team that is using the room
Voila!
Diagram Modelling - OOP
Noun | Verb | Prop? |
---|---|---|
MeetingRoom | ||
name | ||
occupied | ||
enter_room | ||
exit_room | ||
Office | ||
room_list | ||
add_room | ||
list_all_rooms | ||
list_available_rooms | ||
ManagerInterface | ||
receive_message | ||
meeting_details | ||
?Team | ||
name | ||
meeting_name |
I feel the table below is something I've alreaady covered, but I will continue regardless - perhaps I will find something I've missed?
Noun | Property or Owner? |
---|---|
MeetingRoom | Owner |
MeetingRoom.name | Property |
MeetingRoom.occupied | Property |
Office | Owner |
Office.room_list | Property |
text_message | Property (of what)? |
?Team.name | Property? |
Team.meeting_agenda | Property |
Having done the above, i feel the first table covered a lot of ground - it describes the actions and properties of an object, and its owner. It's like an informal UML. I'm gonna skip the next one, but maybe look into the propertys they change?
Actions | Owned by |
---|---|
Actions | Property it Reads or Changes |
---|---|
MeetingRoom.enter_room | occupied |
MeetingRoom.exit_room | occupied |
Office.add_room | room_list |
Office.list_all_rooms | room_list |
Office.list_available_rooms | room_list where MeetingRoom.occupied == true |
ManagerInterface.receive_message | MeetingRoom.exit_room ( and Office.list_available_rooms? ) |
ManagerInterface.meeting_details | Office.list_rooms - Office.list_available_rooms? (maybe works because of Ruby arrays? will see), Team.name, Team.meeting_agenda |
Again, table one has done most of the legwork - ( straight to UML next week maybe? ) don't think I need the below
Class | CLASSNAME |
---|---|
Properties (instance variables) | |
Actions (methods) |
It's been about an hour and a half 10:52, break til 11
TDD through the User Stories
Commit after each successful test!
but first GH, fork the repo local, clone MY GH fork check Gemfile for ruby version 3.0.0 > 3.0.2, ace! run bundle
add twilio-ruby in advance, less to think of later tbh rubygems.org, 5.63.1 as of Jan 26 2022 - brilliant places in :test AND :development groups in Gemfile - I think that's correct - test for the RSpec, and development will give accesswhilst running in a REPL
check git log
bundle appears to have worked!
User Story 1
As a staff member
In order to distinguish between meeting rooms *names must be unique?*
I would like my meeting room to have a name
I might as well check for unique names if possible, right?...
./spec/meetingroom_spec.rb
require './lib/meetingroom'
describe MeetingRoom do
let(:room) { MeetingRoom.new("some_room") }
it '#has a name' do
expect(room.name.class).to be String
end
end
./lib/meetingroom.rb
I chose the accessor because its reasonable that an office may want to change the name of a meeting room. What security features does this break / bend?
class MeetingRoom
attr_accessor :name
def initialize(name)
@name = name
end
end
User Story 2
As an office manager
So that staff can coordinate meetings
I would like to add a meeting room to the office
I need to create an office class, with a room_list instance variable of type Array. It must have a method to take a MeetingRoom, and push it to the room_list Array.
How do I test for this? that a room has been pushed to the in the array.
I will try using a double - its not 100% necessary, but is good practice. if it works, huzzah!
./spec/office_spec.rb
require './lib/office'
describe Office do
let(:office) { Office.new }
let(:room1) { double(name: "Azure", occupied: false) }
it '#add\'s meeting rooms' do
office.add_room(room1)
expect(office.room_list).to include(room1)
end
end
remember - as 'git commit -am' adds and commits
NERDTree - 'ma' will make a new file/subdirectory in highlighted directory
Failures
- LoadError, no file
- Forgot to make the attr_reader
- Gave an argument to the instance variable - derp!
class Office
attr_reader :room_list
def initialize
@room_list = []
end
def add_room(room)
@room_list << room
end
end
Success, green and the double worked ( i think as intended? ) happy days!
User Story 3 4.0 - whoops!
As a staff member
In order to have meeting,
I would like to check if the meeting room is available or not (true or false)
A meeting room shall be given the instance variable @occupied, with a boolean value. default is true. It is not constructor argument - all meeting rooms at creation would surely be false.
how do I check for this?
expect MeetingRoom.occupied to return true or false, i think
./spec/meetingroom_spec.rb
it '#it\'s occupied or not' do
expect(room.occupied).to be(true).or be(false)
end
./lib/meetingroom.rb
class MeetingRoom
attr_accessor :name
attr_reader :occupied
def initialize(name)
@name = name
@occupied = false
end
end
we don't want to change the meeting room occupied from outside the meeting room - so it only has a reader
Errors
- NoMethodError - expected
- Error in RSpec - false not true - i wrote the code wrong
Fixed the error, and it worked!
Interim
I forgot to make the MeetingRoom names unique to the office.
test
I will expect a MeetingRoom to raise an error if it is being pushed to the Office.room_list array, if it's already present
./spec/office_spec.rb
it '#won\'t add room, if already in room_list' do
office.add_room(room1)
p room1.name
expect { office.add_room(room1) }.to raise_error "A meeting room of the same name exists within this office. Please choose another name for the meeting room."
end
./lib/office.rb
def add_room(room)
return raise ROOM_EXISTS_ERROR if @room_list.include?(room)
@room_list << room
end
User Story 4.1
I'm going to change my plan somewhat. According to my plan, I think staff would physically check the meeting room, in order to see if it's occupied.
Now, were I in the Gherkin, I wouldn't want to go through every office and knock on every door, to check for occupancy. I'd ask the front desk.
I'd want to check them against all meeting rooms the office has. ( If I were to extend this, i'd perhaps add a datetime object to an occupied room at clock in, expected-clockout, and actual-clockout )
test
expect the function Office.check_room to return a true or false value from MeetingRoom.occupied
./spec/office_spec.rb
it '#checks room occupancy' do
office.add_room(room1)
expect(office.check_room(room1)).to eq("#{room1.name} is available").or eq("#{room1.name} is unavailable")
end
failures
- NoMethodError
./lib/office.rb
def check_room(room)
return "#{room.name} is available" if room.occupied == false
"#{room.name} is unavailable"
end
worked first time!
is this worrisome because room.occupied could have a truthy value, rather than explicitly being the Boolean, true?
Interlude 2 Gondor calls for Aid!
So this is a solo challenge.
However
Some were stuck on some stuff - some fundamental stuff to do with classes, instantiation, the main programme, the testing environment AND the unit tests.
My intention was to only push in the right direction - I think I did so.
And I think we all made good progress. It was productive for everyone. Step throughs, line by line, experimenting together.
I felt at the present time, it was the right thing to do. I feel in hindsight it was the right thing to do. It was neither copying, nor telling people what to write. I'm not a compsci tutor, but I had to try. Together, we prevailed - we didn't add much, but we did refactor to make everything function, AND to be able to explain how and why things were working.
Immediately after, I saw the group use this knowledge to create their own answer for the next test, without my aid (other than my incessant and insufferable questioning) as to whether a string is its' own object. There was tangible progress.
If there are repercussions, well... Probably there will be. But I made the right choice.
The hour grows late - but code - code never sleeps!
User Story 3 - actually 3, this time haha!
As an office manager
So that I can manage meeting rooms
I would like to list all the meeting rooms in the office
We want to check that the @room_list instance variable returns all rooms
Hmm, the test was green - by giving the room_list an attr_reader, this sort of anticipated this question
I added the test below, but feel its a little redundant
Perhaps, if I have wish, I should try and make a factory to construct multiple rooms for me? Then use that to look for an error. Use the factory to run the feature test, etc.
I added the test below - to be honest, it feels redundant - I've already covered this? perhaps I should change the getter to a function? I don't know.
let(:room2) { double(name: "e3", occupied: true) }
...
it '#lists all rooms' do
office.add_room(room1)
office.add_room(room2)
expect(office.room_list).to include(room1).and include(room2)
end
User Story 5 and 8
As a staff member
In order to have a meeting,
I would like to be able to enter the meeting room and this should make it unavailable
given that this will require a guard statement, I'm going to also add User Story 8 here
As a staff member
So that I can avoid interrupting a meeting
I would like an error if I try to use a room that has already been entered
Referring back to my tables above, I need to create an enter_room method which changes the MeetingRoomINST.occupied to false
check_room should return "#{MeetingRoomINST} is unavailable}"
so my test should first change change MeetingRoomINST via the enter_room method, and then check MeetingRoomINST.occupied == false?
I can add a guard clause to raise an error if the MeetingRoomINST.occupied is true
./spec/meetingroom_spec.rb
it '#is occupied when entered' do
room.enter_room
expect(room.occupied).to be true
end
it '#raises error if accessing room in-use' do
room.enter_room
expect { room.enter_room }.to raise_error "room in use"
end
Failures
- NoMethodError
./lib/meetingroom.rb
def enter_room
return raise "room in use" unless @occupied == false
@occupied = true
end
User Story 6
As a staff member
In order to end a meeting
I would like to be able to leave the meeting room and this should make it available again
again this is a method on the MeetingRoomINST
I will be testing that, once occupied, a MeetingRoomINST.exit_room will make MeetingRoomINST.occupied == false
./spec/meetingroom_spec.rb
it '#is unoccupied when exited' do
room.exit_room
expect(room.occupied).to be false
end
Failures
- NoMethodError
def exit_room
@occupied = false
end
User Story 7
As a staff member
So that I can see where to have my meeting
I would like to be able to see a list of available rooms in the office
Looking back at my plan, the Office class needs a method 'list_available_rooms'
im expecting in the code to do a kind of array.do | ele | and run ele.occupied, to return an array containing a list of rooms which are available
in the test, I want a OfficeINST.list_available_rooms to return an array that does include an room.occupied == false, and excludes room.occupied == true - can I do this in the test using RSpecs include and maybe !include?o
looking at the RSpec docs, I would need to use .not_to include(MeetingRoomINST) - fantastic!
oooo found this here! unsure if i need the second line - once green I can test and remove as appropriate
RSpec::Matchers.define_negated_matcher :not_include, :include
RSpec::Matchers.define_negated_matcher :not_eq, :eq
I'm guessing that RSpec method .define_negated_matcher takes 2 args; arg2 looks like the original symbol, whilst arg1 is the negated derivision of arg2?
refactored office_spec vars more clearly - room_unoccupied == room1, room_unoccupied == room2
./spec/office_spec.rb
require './lib/office'
RSpec::Matchers.define_negated_matcher :not_include, :include
RSpec::Matchers.define_negated_matcher :not_eq, :eq
describe Office do
let(:office) { Office.new }
let(:room_unoccupied) { double(name: "Azure", occupied: false) }
let(:room_occupied) { double(name: "e3", occupied: true) }
...
it '#can list only the available rooms' do
office.add_room(room_unoccupied)
office.add_room(room_occupied)
expect(office.list_available_rooms).to include(room_unoccupied).and not_include(room_occupied)
end
Failures
- NoMethodError
Failures:
1) Office #can list only the available rooms
Failure/Error: expect(office.list_available_rooms).to include(room_unoccupied).and not_include(room_occupied)
expected [#<Double (anonymous)>] to include #<Double (anonymous)>
...and:
expected [#<Double (anonymous)>] not to include #<Double (anonymous)>
Diff for (include #<Double (anonymous)>):
<The diff is empty, are your objects producing identical `#inspect` output?>
Diff for (not include #<Double (anonymous)>):
<The diff is empty, are your objects producing identical `#inspect` output?>
# ./spec/office_spec.rb:40:in `block (2 levels) in <top (required)>'
Finished in 0.02235 seconds (files took 0.10873 seconds to load)
10 examples, 1 failure
Failed examples:
rspec ./spec/office_spec.rb:36 # Office #can list only the available rooms
Well!... that's a new one! something to do with the doubles. They're not attached to the MeetingRoom classes. I think they're spies?... Hmmm...
I p'd out the list_available_rooms, and maybe it worked - there's only 1 item within. But it returns the anonymous double. Hmmm.
Ah. I gave my doubles names, to stop them being anonymous; then I p'd out my OfficeINST.list_available_rooms, only to find, everything was working opposite of how I'd expected. Mainly because I was adding only the unavailable rooms. Whoops!
let(:room_unoccupied) { double("Azure", name: "Azure", occupied: false) }
let(:room_occupied) { double("e3", name: "e3", occupied: true) }
Changed the true value to false, and all is well
./lib/office.rb
def list_available_rooms
available_rooms = []
@room_list.each do |room|
if room.occupied == false
available_rooms << room
end
end
available_rooms
end
let's experiment with the RSpec Matcher a little - hahahaaha, wonderful! works as i'd guessed
RSpec::Matchers.define_negated_matcher :cats_for_brains, :include
...
expect(office.list_available_rooms).to include(room_unoccupied).and cats_for_brains(room_occupied)
I'll commit that with the cats_for_brains, and change it back before the next commit, and nobody need ever know... ;)
User Story 9 - Twilio
this might be difficult. I'm anticipating environment variables, which I can store in .env - how will i access those? a gem? or simply require them in? i don't know. I need to look at best practices first.
I also need to check a twilio account - i think I might have one already? I'm uncertain. Let's do that first.
ugh, I got caught refactoring the last user Story... instead of a whole block i can use .select, i think, but I need the opposite of .select?...
god dayuuummm, sometimes I'm too good, or at least, the ruby resources are, hahah!
./lib/office.rb
before
def list_available_rooms
available_rooms = []
@room_list.each do |room|
if room.occupied == false
available_rooms << room
end
end
available_rooms
end
after
def list_available_rooms
@room_list.reject(&:occupied)
end
the above returns a new array, where the argument evaluates to true or false statement evaluates to false
User Story 9, proper
As an office manager
So that I can find out when a room becomes available
I would like to receive a text message whenever a meeting room becomes available again
I think it's time to make the class ManagerInterface and it's spec. hmmm.o
Right, so, looking at the twilio API, the first thing I wanted to do was
add .env to my .gitignore
sign up to twilio and get the SID and auth_token
make a .env file, and add SID and auth_token
add dotenv from rubygems.org to my Gemfile
run bundle
it worked!
then use
dotenv -t .env
to have the dotenv Gem make a template of my .env file at .env.template, which I can push to git as an example - ace!
time to make the twilio account work
so I think I have the code and the twilio account set up - I should receive a JSON when I run some code - I hope? fingers crossed.
need to require the ManagerInterface in the MeetingRoom file
when MeetingRoomINST.exit_room, this code in ManagerInterfaceINST.receive_sms will run
so I'm going to expect the ManagerInterfaceINSTJson to maybe call another method that parses the JSON data?
ugh this is getting complicated hahaha and its 22:30. hmm. No rest for the wicked.
in which case, I'm expecting ManagerInterfaceINST.receive_sms to receive a string saying "#{room} is available"
./spec/managerinterface_spec.rb
require './lib/managerinterface'
describe ManagerInterface do
let(:interface) { ManagerInterface.new }
let(:room_unoccupied) { double("RoomID:001", name: "Hyperion Suite", occupied: false) }
it '#receives an SMS when meeting room becomes available' do
expect(interface.receive_sms(room_unoccupied)).to eq "#{room_unoccupied.name} is now available"
end
end
Failures
- many, first was using the dotenvs k/v's as instance variables on the ManagerInterface
- Then was a Gemfile issue, installing rack
- then a few issues with a phone number
- finally, despite the docs telling me i was receiving a JSON, I believe the twilio-gem does some magic to provide me access via method calls such as 'message.body' below
./lib/managerinterface.rb
require 'twilio-ruby'
require 'dotenv/load'
class ManagerInterface
def initialize
@account_sid = ENV['TWILIO_ACCOUNT_SID']
@auth_token = ENV['auth_token']
@number = ENV['PHONE_NUMBER']
end
def receive_sms(room)
@client = Twilio::REST::Client.new @account_sid, @auth_token
message = @client.messages.create(
body: "#{room.name} is now available",
to: @number,
from: '+15005550006',
)
message.body
end
end
Okay, so this one is a little rough. How do I do this? I could jam the receive_sms method into the MeetingRoom.exit_room class; but that would increase the coupling. It would be the simplest solution. However - what I think I want, is an Observer pattern?
But where?
I think to monitor the office, for when a room is exited - but how can I access the MeetingRoom.exit_room method?
in OfficeInst, while office_hours, for room in @room_list, if room.exit_room, ManagerInterfaceINST.receive_text
how does an observer watch the OfficeINST, the ManagerInterfaceINST, and the MeetingRoomINSTS simultaeneously?
ugh, it's past midnight, and fun as this is, it's quickly becoming unweildy, fudge.
hmmm, apparently an observer is a many observers-to-one subject - I want many subjects to one observer.
many rooms, to one manager.
could I get an MeetingRoomINST.exit room to send a message to an Office interface? that might work?
then whenever that office interface is triggered, the ManagerInterfaceINST.receive_sms is triggered, with the parameter of the appropriate room?
I think this starts in the MeetingRoom.exit_room method - how do I send a message to a different class?
A New Day
It is 11AM Saturday morning. And it's time to sort this twilio stuff out.
So:
-
At present, I don't have the knowledge, nor genuine need, to implement some kind of interfacing class between a ManagerInterfaceINST and the MeetingRoomINST.
-
I have a plan, however
-
Meet the criteria. I'm gonna plug the manager interface object into the MeetingRoom.exit_room method as an argument - it will call the receive room from there. Shoddy, a bit hacky, but it will do for now.
-
I will implement the 10th User Story, the Team, probably as a class
-
IF i wish, I will try and do something like:
make a ManagerInterface method
that monitors the states of each element
in an office's room_list array attribute
for either:
element.occupied changing from true, to false
or
when element.exit_room is called
Onward
Done. Oof. I'm unsure how I feel. It works; but I feel it should work betterer. Gah. Frustration. I can refactor later, and maybe ask for some help also, after I've pushed to remote, and done the pull request.
Anyway - the test.
./spec/managerinterface_spec.rb
it '#receives an sms when room becomes available' do
expect(room.exit_room(interface)).to eq "#{room.name} is now available"
end
Failures
- The first error was my not having made a parameter in the MeetingRoomINST.exit_room method.
- My second was passing in only @name - the error message showed it was calling the .name method / accessor (are they one and the same in Ruby? I must look into that) on the @name I was passing in. A breath, and I realised the error. Maybe I needed to pass in self?
- There was green! But an earlier test was broken - i needed to pass in a parameter there too.
./lib/meetingroom.rb
def exit_room(interface)
@occupied = false
interface.receive_sms(self)
end
./spec/meetingroom_spec.rb
it '#is unoccupied when exited' do
interface = ManagerInterface.new
room.exit_room(interface)
expect(room.occupied).to be false
end
lucky I checked git status there - for some reason, I think because of the .env.template file,
git commit -am "some message"
didn't add the files before commiting.
User Story 10
in all honesty I need to refactor the test as well - they're calling Twilio on a test account - so I'm not charged, but I need to create a dummy message class to call message.body on, or to create a stub. I'll look at that later
because I had a brainwave.
right
Team is gonna be a class. On MeetingRoomINST.enter_room an argument of TeamINST is gonna be taken. TeamINST is gonna have an agenda, and list of team members (maybe a hash where keys are name, role), and an agenda
but lets not get ahead of ourselves - lets grab the User Story
As an office manager
So that I can have visibility of how the rooms are being used
I would like to see the name of the meeting and the name of the team that is using the room
Okay - so my above idea is a little overboard, which is a shame. I need a hash perhaps, where keys are :meeting_name, and :team_name.
I still think a team class is useful.
Similiar to above, TeamINST perhaps passes these into the MeetingRoomINST temporarily, and on exit_room, returns them to nil?
ugh, thats a lot of tests
more than one user story required. Overcomplicated.
it's gonna break a lot of stuff - it's fine, I have git.
let's do this.
- Team tests
i want a team to have a team_name i want a team to have a meeting_name
I want these to be accessible - perhaps even an accessor?
maybe I can do this in one test?
i want TeamINST to have attributes team_name AND meeting_name.
./spec/team_spec.rb
require './lib/team'
describe Team do
let(:team_proton) { Team.new("proton") }
it '#can set an agenda' do
expect(team_proton).to respond_to(:set_agenda)
end
it '#has attributes name and agenda' do
team_proton.set_agenda("TypeScript refactor")
expect(team_proton).to have_attributes(name: 'proton', agenda: "TypeScript refactor")
end
end
Failures
- LoadError, as expected - no team.rb to require
class Team
attr_reader :name, :agenda
def initialize(name)
@name = name
@agenda = nil
end
def set_agenda(agenda)
@agenda = agenda
end
end
there we go; but this is becoming complicated, and things are becoming coupled. Fudge. Sigh. I need to understand interfaces.
I'm thinking the Team goes into the meeting room, there is a method that sends that info to the managerinterface, that is called in the manager class.
hmmm. I feel I should be using the office class more to be honest.
from the office_occupied? I could use select?
In ./lib/managerinterface.rb?
def check_team(MeetingRoomINST)
return raise "room doesn't exist" unless OfficeINST.room_list.include?(MeetingRoomINST)
return "room empty" if OfficeINST.list_available_rooms.include?(MeetingRoomINST)
MeetingRoomINST.meeting_info
end
In ./lib/meetingroom.rb?
def meeting_info
return "room empty" unless @occupied == true
"Team #{Team.name} is working on #{Team.agenda}"
end
Perhaps I need to use something like a chain; the meeting room info moves to the office, moves to the manager? that means there are far more functions and tests though. This is definitely some kind of interface class, external to the office/meetingroom/managerinterface/team. aaaggghhhhh!
but then the test will start going crazy? but maybe they should? The classes, should be simple, and the tests should use that logic to build complexity; right?
gah.
I'm feeling class 5 coming on. This is a bad idea.
I haven't even properly implemented class 4.
And aren't I supposed to make an interface for different messaging patterns? So I couldn't use the same one for the SMS and the room info from the manager?
what would it look like?
require './lib/meetingroom'
require './lib/office'
class RoomInfoInterface
def check_room(OfficeINST, MeetingRoomINST)
return raise "room doesn't exist" unless OfficeINST.room_list.include?(MeetingRoomINST)
return "room empty" if OfficeINST.list_available_rooms.include?(MeetingRoomINST)
MeetingRoomINST.meeting_info
end
end
This feels overly complicated. The sheer amount of testing I will need to complete for this feature is probably the same as the initial task itself. It's a bad idea.
What's the alternative? Again, the shoddy way, just attach it to the MeetingRoom class. Sigh.
'A common anti-pattern is making an interface that matches what the consumed class offers.'
Right, I think we're doing this. We're gonna write our own "user stories" for the implementation of a RoomInfoInterface.
So that the manager accesses only an interface to gain access to room.meeting_info
-
interface accesses the office.room_list, to check room exists
-
interface accesses the office.list_available_rooms to check room is occupied
-
interface can access the room.meeting_info, from within the office
-
room has attributes :name, :occupied, :team, :agenda
-
when a team enters a room, room shares :team and :agenda with teams' :name and :agenda
-
when a team exits a room, room returns :team and :agenda to nil
let's do 4, 5, and 6 first. They're going as one commit. I'm not doing one test at a time at this point.o
Oof, that was a little tougher but we got there in the end. The tests are becoming bulkier and I don't like it, but hey, what can you do.
./spec/meetingroom_spec.rb
it '#has teams name and agenda when in use' do
room.enter_room(team_red)
expect(room.meeting_info).to eq "Team #{team_red.name} in meeting #{team_red.agenda}"
end
it '#team and agenda are nil after exit' do
room.enter_room(team_red)
room.exit_room(interface)
expect(room.check_clear).to eq({ team: nil, agenda: nil })
end
./lib/meetingroom.rb
require_relative 'managerinterface'
class MeetingRoom
attr_accessor :name
attr_reader :occupied
def initialize(name)
@name = name
@occupied = false
@team = nil
@agenda = nil
end
def enter_room(team)
return raise "room in use" unless @occupied == false
@occupied = true
@team = team.name
@agenda = team.agenda
end
def exit_room(interface)
@occupied = false
@team = nil
@agenda = nil
interface.receive_sms(self)
end
def meeting_info
return "room empty" unless @occupied == true
"Team #{@team} in meeting #{@agenda}"
end
def check_clear
{ team: @team, agenda: @agenda }
end
end
Implement RoomInfoInterface
-
interface accesses the office.room_list, to check room exists
-
interface accesses the office.list_available_rooms to check room is occupied
-
interface can access the room.meeting_info, from within the office
./spec/roominfointerface_spec.rb
require './lib/office'
require './lib/meetingroom'
require './lib/roominfointerface'
describe RoomInfoInterface do
let(:info_interface) { RoomInfoInterface.new }
let(:office) { Office.new }
let(:room1) { MeetingRoom.new("001") }
let(:team) { double(name: "Mystery Inc", agenda: "Jinkies!") }
it '#can access meeting_info' do
office.add_room(room1)
room1.enter_room(team)
expect(info_interface.get_meeting_info(office, room1)).to eq "Team Mystery Inc in meeting Jinkies!"
end
end
Failures
- Well, the first error is pretty standard - there's no associated class ...
There were more failures than mere StandardErrors! At some point TDD gave way to a mix of test-making development. I coudln't figure out how I wanted to test the interface in advance - I suppose I hadn't planned to make one, but such as it is, I have a working interface. I only fell at the last hurdle, and it was a hurdle I willingly chose to go for. Better luck next time!
./lib/roominfointerface.rb
require_relative 'office'
require_relative 'meetingroom'
class RoomInfoInterface
def get_meeting_info(office, room)
return raise "room doesn't exist" unless office.room_list.include?(room)
return "room empty" if office.list_available_rooms.include?(room)
room.meeting_info
end
end
There are things I could have done better. Up until the last moment, I think things were going quite well. Will I try refactoring the sms into an interface tomorrow? ugh, I don't know. I'm tired haha. Honestly, the difficult part was refactoring the other classes to work with the interface. I definitely could have done the MeetingRoom / Team classes better. The idea of them temporarily sharing the attributes is useful, but I should have worked something else out, as now the Team class looks messy
Full-Code below
Office Challenge
./spec/office_spec.rb
require './lib/office'
RSpec::Matchers.define_negated_matcher :not_include, :include
describe Office do
let(:office) { Office.new }
let(:room_unoccupied) { double("Azure", name: "Azure", occupied: false) }
let(:room_occupied) { double("e3", name: "e3", occupied: true) }
it '#add\'s meeting rooms' do
office.add_room(room_unoccupied)
expect(office.room_list).to include(room_unoccupied)
end
it '#won\'t add room, if already in room_list' do
office.add_room(room_unoccupied)
expect { office.add_room(room_unoccupied) }.to raise_error "A meeting room of the same name exists within this office"
end
it '#lists all rooms' do
office.add_room(room_unoccupied)
office.add_room(room_occupied)
expect(office.room_list).to include(room_unoccupied).and include(room_occupied)
end
it '#checks room occupancy' do
office.add_room(room_unoccupied)
expect(office.check_room(room_unoccupied)).to eq("#{room_unoccupied.name} is available").or eq("#{room_unoccupied.name} is unavailable")
end
it '#can list only the available rooms' do
office.add_room(room_unoccupied)
office.add_room(room_occupied)
expect(office.list_available_rooms).to include(room_unoccupied).and not_include(room_occupied)
end
end
./lib/office.rb
class Office
ROOM_EXISTS_ERROR = "A meeting room of the same name exists within this office".freeze
attr_reader :room_list
def initialize
@room_list = []
end
def add_room(room)
return raise ROOM_EXISTS_ERROR if @room_list.include?(room)
@room_list << room
end
def check_room(room)
return "#{room.name} is available" if room.occupied == false
"#{room.name} is unavailable"
end
def list_available_rooms
@room_list.reject(&:occupied)
end
end
./spec/meetingroom_spec.rb
require './lib/meetingroom'
require './lib/managerinterface'
describe MeetingRoom do
let(:room) { MeetingRoom.new("Marylebone Suite") }
let(:interface) { ManagerInterface.new }
let(:team_red) { double("Red Leader", name: "Red Ten", agenda: "Standing By") }
let(:team_rocket) { double("Team Rocket", name: "Team Rocket", agenda: "Blasts off; again...") }
it '#has attributes name, occupied' do
expect(room).to have_attributes(name: "Marylebone Suite", occupied: false)
end
it '#it\'s occupied or not' do
expect(room.occupied).to be(true).or be(false)
end
it '#is occupied when entered' do
room.enter_room(team_red)
expect(room.occupied).to be true
end
it '#raises error if accessing room in-use' do
room.enter_room(team_red)
expect { room.enter_room(team_rocket) }.to raise_error "room in use"
end
it '#is unoccupied when exited' do
room.exit_room(interface)
expect(room.occupied).to be false
end
it '#has teams name and agenda when in use' do
room.enter_room(team_red)
expect(room.meeting_info).to eq "Team #{team_red.name} in meeting #{team_red.agenda}"
end
it '#team and agenda are nil after exit' do
room.enter_room(team_red)
room.exit_room(interface)
expect(room.check_clear).to eq({ team: nil, agenda: nil })
end
end
./lib/meetingroom.rb
require_relative 'managerinterface'
class MeetingRoom
attr_accessor :name
attr_reader :occupied
def initialize(name)
@name = name
@occupied = false
@team = nil
@agenda = nil
end
def enter_room(team)
return raise "room in use" unless @occupied == false
@occupied = true
@team = team.name
@agenda = team.agenda
end
def exit_room(interface)
@occupied = false
@team = nil
@agenda = nil
interface.receive_sms(self)
end
def meeting_info
return "room empty" unless @occupied == true
"Team #{@team} in meeting #{@agenda}"
end
def check_clear
{ team: @team, agenda: @agenda }
end
end
./spec/managerinterface_spec.rb
require './lib/managerinterface'
require './lib/meetingroom'
require './lib/office'
describe ManagerInterface do
let(:interface) { ManagerInterface.new }
let(:info_interface) { RoomInfoInterface.new }
let(:room_unoccupied) { double("RoomID:001", name: "Hyperion Suite", occupied: false) }
let(:room) { MeetingRoom.new("Seychelles Suite") }
let(:team) { double(name: "The Incredibles", agenda: "Misplaced Super-Suits") }
let(:office) { Office.new }
it '#can receive an sms' do
expect(interface.receive_sms(room_unoccupied)).to eq "#{room_unoccupied.name} is now available"
end
it '#receives an sms when room becomes available' do
expect(room.exit_room(interface)).to eq "#{room.name} is now available"
end
it '#can get team info for an occupied room' do
office.add_room(room)
room.enter_room(team)
expect(interface.check_team(office, room)).to eq "Team The Incredibles in meeting Misplaced Super-Suits"
end
end
./lib/managerinterface_spec.rb
require 'twilio-ruby'
require 'dotenv/load'
require './lib/roominfointerface'
class ManagerInterface
def initialize
@account_sid = ENV['TWILIO_ACCOUNT_SID']
@auth_token = ENV['auth_token']
@number = ENV['PHONE_NUMBER']
end
def receive_sms(room)
@client = Twilio::REST::Client.new @account_sid, @auth_token
message = @client.messages.create(
body: "#{room.name} is now available",
to: @number,
from: '+15005550006',
)
message.body
end
def check_team(office, team)
info_interface = RoomInfoInterface.new
info_interface.get_meeting_info(office, team)
end
end
./spec/team_spec.rb
require './lib/team'
describe Team do
let(:team_proton) { Team.new("proton") }
it '#can set an agenda' do
expect(team_proton).to respond_to(:make_agenda)
end
it '#has attributes name and agenda' do
team_proton.make_agenda("TypeScript refactor")
expect(team_proton).to have_attributes(name: 'proton', agenda: "TypeScript refactor")
end
end
./lib/team.rb
class Team
attr_reader :name, :agenda
def initialize(name)
@name = name
@agenda = nil
end
def make_agenda(agenda)
@agenda = agenda
end
end
./spec/roominfointerface_spec.rb
require './lib/office'
require './lib/meetingroom'
require './lib/roominfointerface'
describe RoomInfoInterface do
let(:info_interface) { RoomInfoInterface.new }
let(:office) { Office.new }
let(:room1) { MeetingRoom.new("001") }
let(:team) { double(name: "Mystery Inc", agenda: "Jinkies!") }
it '#can access meeting_info' do
office.add_room(room1)
room1.enter_room(team)
expect(info_interface.get_meeting_info(office, room1)).to eq "Team Mystery Inc in meeting Jinkies!"
end
end
./lib/roominfointerface.rb
require_relative 'office'
require_relative 'meetingroom'
class RoomInfoInterface
def get_meeting_info(office, room)
return raise "room doesn't exist" unless office.room_list.include?(room)
return "room empty" if office.list_available_rooms.include?(room)
room.meeting_info
end
end