The most prominent questions to last week's article, "How I got into FAANG", revolved around the Object Design interview. Namely, "what it is?", "how to prepare for it?" as well as what the OOP interview at Amazon actually looks like.
Object Oriented Design
Object Oriented Programming is a programming paradigm based on the concept of objects, which can contain properties and functionality.
When designing the OOP way, we could define a class Human
. With attributes such as height
and hairColor
and methods such as eat()
and sleep()
.
Our desire could now be to extend the functionality of the Human
Object by the method code()
. But since we don't want every Human
to be able to code()
, we'd sub-class Human
, to inherit all attributes and methods.
We then have the ability to add further functions or attributes, or override existing ones.
class SoftwareEngineer: Human {}
When designing objects, make sure to think about what kind of attributes should be exposed and what kind of parent properties can potentially be extracted into a super-class.
Let's look at another example.
class Human {
func eat(_ food: Food) -> Bool { // 1
return food.isEdible
}
}
class Vegan: Human { // 2
override func eat(_ food: Food) -> Bool { // 3
return food.isEdible && food.isVegan
}
}
- The eat function returns a boolean, indicating whether or not the food has been eaten. Our human has free choice.
- Now we want to define a sub-class of Human to give our object a more detailed definition.
- We're now able to
override
the returned value just for allVegan
objects, while everyHuman
object continues to consume everything edible.
The Interview
It's fair to say that the process is pretty much almost always the same. As interviewee, you'll be presented with some kind of a real world scenario like "Design a Parking Lot", and then you'll be asked to create a technical design for that.
In this article, we're going to design a "Puppy Hotel".
The hotel has a clearly defined amount of hotel rooms for small dogs, rooms for medium sized dogs, and the same for large dogs.
In the interview, what is described above is literally all we'll get as a scenario description. It is now on us to ask the right questions to the interviewer. These would include questions to clarify what functionality the design is expected to full-fill.
Never ever make assumptions. It's part of the interview to see how you handle vague questions. Are you asking clarifying questions, or are you jumping straight into code, potentially wasting your interview coding into the wrong direction?
We'd ask the interviewer what ability the hotel needs to have and we learn that we need to be able to "check in" and to "check out".
Once I know what I need to develop, I'm starting out with some code snippets as a conversation starter with the interviewer. While typing the code I'm explaining what I'm doing.
class Hotel {
func checkIn(_ dog: Dog) -> String // 1
func checkOut(_ dog: Dog) -> Dog? // 2
}
- We're creating a
Hotel
class and we're giving it the following definitions a function to check a dog in that returns a roomID. - And a function that returns the Dog to when checking out.
Next I'd design a quick type for the Dog.
struct Dog { // 1
let uid: String // 2
let size: DogSize // 3
}
enum DogSize {
case small, medium, large
}
- Since we're iOS engineers, we want to use a light weight type like a struct
- Every dog needs an id for identification
- And since the hotel cares for the sizes of the dogs, we're creating a type definition as well. An integer's intention is not easy to read for humans, Strings are of high risk of errors due to typos. Tell your interviewer, that type-safety is important to you. Really hard to disagree.
Now that we have a type definition for the sizes of our dogs and the Dog
construct itself, we need to add the data structures to the Hotel
. Since we want to prove to the interviewer that we "Think Big", we will have a custom initializer passing the size definition for each room size. We can reuse this class for every hotel we might own at some point.
class Hotel { // 1
let smallRoomsCount: Int // 2
let mediumRoomsCount: Int
let largeRoomsCount: Int
var smallRooms = [String: Dog]() // 3
var mediumRooms = [String: Dog]()
var largeRooms = [String: Dog]()
init(small: Int, medium: Int, large: Int) { // 4
smallRoomsCount = small
mediumRoomsCount = medium
largeRoomsCount = large
}
}
- Same Hotel class as above. Just new lines of code.
- 3 individual integers, that reflect the available rooms for each category.
- The 3 dictionaries will serve as the data structure for the 3 room size classes.
- A custom initializer opens this class for generic re-use for more than one specific physical hotel.
We're using dictionaries as the data structure of choice, because every operation has a perfect constant Big O
operation cost for each time and space.
During the interview, it's important to verbally express your thinking. The interviewer wants to hear that you're thinking about making a class or function generic
. That means you don't hard code variables, like the amount of available rooms. It shows seniority, and that you're thinking about the bigger picture.
"Thinking small is a self-fulfilling prophecy. Leaders create and communicate a bold direction that inspires results. They think differently and look around corners for ways to serve customers."
- Amazon Leadership Principle: Think Big.
Next we'll add the function declarations for the stubs of before.
class Hotel {
func checkIn(_ dog: Dog) -> String? { // 1
var roomID: String? // 2
if dog.size == .small, smallRooms.keys.count < smallRoomsCount { // 3
roomID = UUID() // 4
smallRooms[roomID] = dog // 5
}
if dog.size == .medium, mediumRooms.keys.count < mediumRoomsCount { // 6
roomID = UUID()
mediumRooms[roomID] = dog
}
if dog.size == .large, largeRooms.keys.count < largeRoomsCount {
roomID = UUID()
largeRooms[roomID] = dog
}
return roomID // 7
}
func checkOut(dogWithID dogID: String, outOfRoom roomID: String) -> Dog? { // 8
if let dog = smallRooms[roomID], dog.uid == dogID { // 9
return dog
}
if let dog = mediumRooms[roomID], dog.uid == dogID {
return dog
}
if let dog = largeRooms[roomID], dog.uid == dogID {
return dog
}
return nil
}
}
roomID
is now optional. It's possible, that there are no rooms available anymore, so we need to make nil a valid option.roomID
is declared but not initiated.- We check for the dog size and we check if the desired room size is available
- We only generate a PIN, if there's a room available. To safe memory. Mention that to your interviewer. That's part of "scalability".
- We use the ID to assign a room for the dog. Of course there are other ways to do this. But this is an easy way and it makes sense. No reason to overcomplicate it in an interview.
- Repeat for the other sizes
- Lastly return the ID that can be nil in case of no availability.
- We've created a more Swifty syntax for
checkOut
. That shows that we care about writing readable code. We now require thedogID
and theroomID
to check out. That makes more sense. If the IDs are invalid, then the return is nil. - For simplicity, we'll just check, if there's a dog in the room for a given
roomID
, and then if thedogIDs
are matching. A good interviewer will ask you about efficiency of any approach. Make sure to study up onBig O
and explain your approaches.
The Follow Up Questions
With above basic construct in place and hopefully some time left in the interview, we're expecting some follow up questions from the interviewer. The purpose of these questions is to see how well you understand writing scalable code.
Are you locking yourself into a corner, or are you able to identify opportunities for scalability already? How well are you communicating your thinking and planning process?
I intentionally left some room for improvement in the code snippets above. Right now in our design, if all rooms for small dogs are taken, the customer is sent away. To improve the user experience, we want to add the ability to have small dogs occupy medium sized rooms as well.
class Hotel {
public func checkIn(_ dog: Dog) -> String? { // 1
var roomID: String?
getRooms(for: dog) { rooms in // 2
roomID = UUID().uuidString // 3
rooms[roomID!] = dog
}
return roomID
}
private func getRooms(for dog: Dog, result: (inout [String: Dog]) -> Void) { // 4
if dog.size == .large, largeRooms.keys.count < largeRoomsCount { // 5
result(&largeRooms)
}
if dog.size == .medium {
if mediumRooms.keys.count < mediumRoomsCount {
result(&mediumRooms)
}
if largeRooms.keys.count < largeRoomsCount {
result(&largeRooms)
}
}
if dog.size == .small {
if smallRooms.keys.count < smallRoomsCount {
result(&smallRooms)
}
if mediumRooms.keys.count < mediumRoomsCount {
result(&mediumRooms)
}
if largeRooms.keys.count < largeRoomsCount {
result(&largeRooms)
}
}
}
}
- A major improvement of our design, is to define the access level of a function. We mark functions as public, if we want consumers to be able to access it.
- In the new design we're going to try to get a
roomID
from aprivate
method. This function is not async. It's just written with a closure, because we're returns a reference. That's the inout equivalent of returns. - If we have rooms, then we want to create an ID, assign the dog to that room and lastly return it to the consumers.
- To define a clear API for consumers of our object, we're going to set the access level of this function to private. No one outside of this class needs access to it. The function executes synchronous and utilizes a closure to return the reference of the available sizes dictionary. It's possible to optimize or further refactor this function. But try to think small. You got less than 20 mins! Make it work, only then make it pretty!
- In the new version we're checking from large to small and we're passing the best and appropriate rooms's reference to the closure. Small dogs can comfortably fit into small, medium and large rooms and so on.
The hotel is now able to utilize room sizes based on demand, and we have optimized the code by setting the access levels of our functions.
The API
clearly states that checkIn
is a function available on any Hotel
Object, while getRooms
is a private function only available to the Hotel
object.
What methods and attributes are public
and what are private
depends on the circumstances of the project, but a good rule is the "need-to-know" basis. Only expose functions and attributes the consumer NEEDS to know about.
class Hotel {
private let smallRoomsCount: Int
private let mediumRoomsCount: Int
private let largeRoomsCount: Int
private var smallRooms = [String: Dog]()
private var mediumRooms = [String: Dog]()
private var largeRooms = [String: Dog]()
public func checkIn(_ dog: Dog) -> String? {
}
Interview Preparation
There are many ways to prepare for Object Design interviews. If you're just reading this because you're curios, but you're not forced to study up on this topic, then I'd suggest for you to take it easy.
Expertise comes through practice. The more often you design objects, and the more feedback you can gather, the more you'll learn and the better your designs will become. A mentor or a study group is always a good idea for such trainings.
If you're about to interview and you'd like some last minute ideas and influences, then YouTube provides a flood of content. I'd look for something short with a high view count. That typically gets to the point fairly fast.
Then try to design a few objects.
- Design a Parking Lot
- Design a Package Locker
- Design an Airport
Try to predict some follow up questions an interviewer might ask.
- How could you optimize?
- How could you extend functionality?
- Drawbacks?
- Potential time/space improvements?
Ask friends, colleagues, or strangers on Twitter. There is a whole community of friendly students and trainers, as well as professionals available at pretty much every major social media platform.
There are tons of books available for this topic as well. Although, in my experience, the 5 minute introduction followed by "learning by doing" served me better than reading a book for a week.
Conclusion
In the end, it all narrows down to the same thoughts and principles, all the same ideas and concepts. No matter if you design a House
, a Car
or a Spaceship
, you'll always go with a clearly defined API–private and public attributes that define the access level and functions with the best possible efficiency and security.
This article illustrated my process when faced with an Object Design situation. I'm typing some code snippets, I'm re-thinking, and I'm adjusting my code based on the verbal discussions and potential changes in scope.
In an interview or code review scenario, I'm trying my best to "think out loud", and I'm defining and verbally justifying custom type definitions like the DogSize
enum. This shows the interviewer that my code is type safe and scalable.
I'm setting expectations and I'm documenting my code well. That way, it's easier to refactor and adjust the code later, as well as to debug crashes or issues.
Learning Object Design is not a step by step tutorial. It's a process and as you'll learn from mistakes of the past, from talks, articles and suggestions, you'll naturally get better over time.
While you evolve, so does your understanding of Object Oriented Design.
New article 🎉☺️
— David Seek (@DavidSeek) February 2, 2021
This one has been sitting on draft for a while. I'm talking about my past, my career and how I got into FAANG.
Less technical, and more personal.
Retweet and like for reach highly appreciated 🥰https://t.co/EWskDl5Jmm