What do you think ‘Object Oriented Thinking’ means?
That’s a really good question. But before I can attempt to discuss object oriented thinking, I think I should attempt to define an object, don’t you? I could say an object is a ‘thing’ in memory used by our application.
That’s not really helping.
You’re right, that simple definition is just a little too simple. Let me clarify. Let’s assume the following discussion is from the perspective of a hypothetical application we’re running. First, when I say an object is ‘in memory’ I don’t really know where it is. It might be on another machine, in a separate process from our application, in the same process as our application, or it might still be on the hard disk waiting to be loaded when it’s needed. It doesn’t really matter where the object is, we just have to know that the ‘thing’ will exist at run time. Second, what makes the ‘thing’ special is that it represents a class in our application. In contrast, C functions also exist at run time, but they don’t represent classes. So, for our discussion they would not be objects.
Ok, I think I got it. but can you say it again for me, all together?
Yes, an object is a representation of a class that exists at run time. How’s that?
Perfect. So, when we talk about object oriented thinking we’re talking about controlling how many representations of a given class in our application are created at run time?
Not always. Object oriented thinking can involve writing classes that ensure there are only a certain number of themselves represented in memory (Singletons, for instance), but that isn’t exclusively what is meant by object oriented thinking. I would say that object oriented thinking is thinking about classes and imagining what the objects that represent them will look like at run time. For example, one very useful tool of object oriented thinking is design patterns. Design patterns provide object oriented developers with a place to start when solving problems. Although design patterns are outside the scope of this post, two common examples are provided below:
|Singleton||An application must ensure that only a single object exists at run time. For example, a single window acting as the application GUI.||Write a class that creates an instance of the GUI object exactly once. Commonly, this will be a lazy instantiation when the application tries to access the object for the first time.|
|Proxy||An application needs an object to represent data that will be populated by a third party at run time. For example, an object that represents data retrieved from a relational database.||Write a base class whose members are all virtual so that the class may be subclassed dynamically at run time. Then, anywhere in the application where the base class is required the dynamically created subclass object can be safely used.|
In the Proxy example, notice two things. First, subclasses of the base class aren’t written by the developer. They are created dynamically at run time. Second, the dynamically created objects that represent a subclass that was never written can be used anywhere the base class can be used. The ability to use a subclass anywhere its base class can be used is known as Liskov Substitution Principle
So, everything I need to know about object oriented thinking I can get by learning design patterns, and understanding the Liskov Substitution Principle?
Almost, but there is still one question, when do I write a new class?
Wait, wasn’t that covered by design patterns and the Liskov Substitution Principle?
Sometimes, but we can make applications that don’t require the Liskov Substition Principle and don’t use design patterns. Regardless, we need to think about what classes we make. Allow me to demonstrate. Let’s say that I want to have an application that shows me all of the students that a university professor teaches in a given semester. Aside from the GUI code, I might make the following two classes: the Student class and the Professor class shown below:
|firstName||Student’s first name.|
|lastName||Student’s last name.|
|firstName||Professor’s first name.|
|lastName||Professor’s last name.|
|students||A set of the professor’s students.|
|getAllStudents()||Returns the students set.|
So far, that looks ok to me. I don’t see any design patterns, but I also don’t see any violations of the Liskov Substitution Principle. Writing these classes looks mostly like intuition.
That’s right. Maybe there’s a better way to write these two classes, but the current implementation certainly works and isn’t complicated to read, so it’s fine. At run time, the objects might look like the following:
This looks like a good model of the objects. One Professor object exists and that object has references to three Student objects. In reality, the Professor object has a reference to a set that has references to each student object, but for the purposes of our model we can view the set as transparent giving the above structure. Now, let’s say I want to show this structure in a table that looks like this:
I decide to write a method to flatten this structure. The flatten method returns a list of three objects. Each object has a Professor’s first name, Professor’s last name, and a reference to a single student. But what class should the returned object represent?
The Professor class already does that. Just return a list of Professor objects, but only have one Student object in each set. So, we would have the below list:
This would work.
I don’t think so. While the Liskov Substitution Principle isn’t violated because the returned objects aren’t subclasses of any class there is still a subtle problem with this implementation. Namely, what prevents us or future developers from passing these ‘incomplete’ objects to a client by mistake? For instance, these objects still have a getAllStudents() method. If a client receives one of these ‘incomplete’ objects and calls getAllStudents() on it, then the client will only get one Student object. That isn’t the intent of the class or the getAllStudents() method. Imagine if the university pays its professors based on the number of students they teach and its the payroll system calling getAllStudents() on one of the ‘incomplete’ objects.
That’s going to be one unhappy professor!
Indeed! And maybe one very unhappy developer trying to debug why the application isn’t returning all of the professor’s students.
So, instead of returning an ‘incomplete’ version of my object, maybe I should write a new class that isn’t compatible with the existing Professor class so that it can’t be used anywhere an existing Professor object can be used?
I think that’s a better idea. I think those three things: design patterns, the Liskov Substition Principle, and avoiding the creation of ‘incomplete’ objects covers a lot of what is meant by object oriented thinking.