Reflections on OOP
Author |
Message |
wtd
|
Posted: Mon Jul 11, 2005 5:41 pm Post subject: Reflections on OOP |
|
|
What is object-oriented programming? That's a terribly complex question, and one for which you will get no shortage of unique answers.
Perhaps rather we should ask: why use object-oriented techniques?
Some people use OOP because they've been told they have to to be cool, or because their boss tells them to use the "class" keyword or find a new job.
The good reason is something different. Object-oriented programming offers us a pretty darn good way to think about programming, and one that's really fairly natural.
When you look around the world, you see things. When you interact with things, you rarely know how they actually work, and even less frequently do you need to know.
For instance, let's look at creating a window and making it visible.
code: | w = Window()
w.set_title("hello world")
w.show() |
Notice how we never cared about the internal state of the window. There was no changing of variables directly. We told the window what do do, and it did it.
Of course, we can't be so nonchalant about such things when we decide to create our own classes of objects. Then we have to worry about the internal state and the ways in which that state can be modified and accessed.
That said, the state that you use should be one of the last things you decide on. First you should decide on how your object will look to the outside world. In this case, we'll want to give the user the opportunity to construct the object itself using certain pieces of data. A first and last name will suffice.
Then we want to give the user the ability to get the first and last name, but not modify them. And lastly, we want the ability to get the full name as a single entity.
Now that we have that down, let's think about the state.
We could store a single string, with the first and last name separated by a space. Then when we want the first or last name, simply split the string internally on that space and return the relevant piece.
But really, that's overly complex. Instead, let's store the individual names as separate variables and only combine them when necessary.
In a number of programming languages:
Ruby: | class Name
attr_reader :first, :last
def initialize(first, last)
@first, @last = first, last
end
def full_name
"#{@first} #{@last}"
end
end |
c++: | class Name
{
private:
std::string first_name, last_name;
public:
Name(std::string f, std::string l)
: first_name(f), last_name(l)
{ }
std::string first() const
{
return first_name;
}
std::string last() const
{
return last_name;
}
std::string full_name() const
{
return first() + " " + last();
}
}; |
Java: | class Name
{
private String firstName, lastName;
public Name(String f, String l )
{
firstName = f;
lastName = l;
}
public String first ()
{
return firstName;
}
public String last ()
{
return lastName;
}
public String fullName ()
{
return first () + " " + last ();
}
} |
c-sharp: | class Name
{
private string firstName, lastName;
public Name(string f, string l)
{
firstName = f;
lastName = l;
}
public string First
{
get { return firstName; }
}
public string Last
{
get { return lastName; }
}
public string FullName
{
get { return this.First() + " " + this.Last(); }
}
} |
Now, let's look at the ease with which we can create Name objects, and get their various methods "for free".
Ruby: | bobs_name = Name.new("Bob", "Smith")
puts bobs_name.full_name |
c++: | Name bobs_name("Bob", "Smith");
std::cout << bobs_name.full_name() << std::endl; |
Java: | Name bobsName = new Name("Bob", "Smith");
System. out. println(bobsName. fullName()); |
c-sharp: | Name bobsName = new Name("Bob", "Smith");
Console.WriteLine(bobsName.FullName()); |
But what if we could get even more "for free"? In truth, printing out the full name in this manner is probably what we'll deal with most commonly, so we don't want to have to include the method call in our code.
This is where some degree of polymorphism comes in. Wen we call something like "System.out.println()" in Java, we can provide many types of objects to that, and it just works.
How?
Because all of those objects have a toString() method that the println method calls. Let's look at how we implement this for each of our Name classes.
Ruby: | class Name
attr_reader :first, :last
def initialize(first, last)
@first, @last = first, last
end
def full_name
"#{@first} #{@last}"
end
alias :to_s :full_name
end |
c++: | class Name
{
private:
std::string first_name, last_name;
public:
Name(std::string f, std::string l)
: first_name(f), last_name(l)
{ }
std::string first() const
{
return first_name;
}
std::string last() const
{
return last_name;
}
std::string full_name() const
{
return first() + " " + last();
}
friend std::ostream& operator<<(std::ostream& out, const Name& n)
{
return out << n.full_name();
}
}; |
Java: | class Name
{
private String firstName, lastName;
public Name(String f, String l )
{
firstName = f;
lastName = l;
}
public String first ()
{
return firstName;
}
public String last ()
{
return lastName;
}
public String fullName ()
{
return first () + " " + last ();
}
public String toString ()
{
return fullName ();
}
} |
c-sharp: | class Name
{
private string firstName, lastName;
public Name(string f, string l)
{
firstName = f;
lastName = l;
}
public string First
{
get { return firstName; }
}
public string Last
{
get { return lastName; }
}
public string FullName
{
get { return this.First() + " " + this.Last(); }
}
public string ToString()
{
return this.FullName;
}
} |
Now we are free to simply write:
Ruby: | bobs_name = Name.new("Bob", "Smith")
puts bobs_name |
c++: | Name bobs_name("Bob", "Smith");
std::cout << bobs_name << std::endl; |
Java: | Name bobsName = new Name("Bob", "Smith");
System. out. println(bobsName ); |
c-sharp: | Name bobsName = new Name("Bob", "Smith");
Console.WriteLine(bobsName); |
Object-orientation can really show its power when it comes to inheritance, though. Inheritance defines an "is-a" relationship. This means that as the type system is concerned, an object of the subclass can go anywhere an object of the parent class would have been able to go.
We can see this readily with the interactive Ruby interpreter.
code: | irb(main):001:0> class A
irb(main):002:1> end
=> nil
irb(main):003:0> class B < A
irb(main):004:1> end
=> nil
irb(main):005:0> B.new.is_a? A
=> true |
But how does this apply to the Name class we've been working on?
Well, if we wanted a Name class that incorporates a title as well as a first and last name, then we could create a whole new class easily enough.
Ruby: | class FormalName
attr_reader :title, :first, :last
def initialize(title, first, last)
@title, @first, @last = title, first, last
end
def full_name
"#{title} #{@first} #{@last}"
end
alias :to_s :full_name
end |
But then, a FormalName really "is a" Name, isn't it? We can easily use it just as we would a name. So what if we make it a Name by inheriting the Name class?
By doing this, we can utilize the code we've already written for our Name class.
Ruby: | class FormalName < Name
attr_reader :title
def initialize(title, first, last)
super(first, last)
@title = title
end
def full_name
"#{title} #{super}"
end
end |
Here "super" is a call to the method as it exists in the parent (or super) class.
Of course, "super" often has different meanings in different languages. In Java and c-sharp, for instance, it's essentially "this", but seen as an object of the previous class.
Let's take a look.
c++: | class FormalName : public Name
{
private:
std::string name_title;
public:
FormalName(std::string t, std::string f, std::string l)
: Name(f, l)
, name_title(t)
{ }
std::string title() const
{
return name_title;
}
std::string full_name() const
{
return title() + " " + Name::full_name();
}
}; |
Java: | class FormalName extends Name
{
private String nameTitle;
public FormalName (String t, String f, String l )
{
super (f, l );
nameTitle = t;
}
public String title ()
{
return nameTitle;
}
public String fullName ()
{
return title () + " " + super. fullName();
}
} |
c-sharp: | class FormalName : Name
{
private string nameTitle;
public FormalName(string t, string f, string l)
: base(f, l)
{
nameTitle = t;
}
public string Title
{
get { return nameTitle; }
}
public string FullName
{
get { return this.Title + " " + super.FullName; }
}
} |
Now, let's look at a less trivial example. Using Python, and its HTMLParser class, we want to count the number of anchor tags in an HTML document.
How does object-oriented programming come into this?
Well, the HTMLParser class contains a "feed" member function, by which you feed source code into the parser. As it encounters different kinds of tags and data, it calls other member functions. The thing is, HTMLParser is useless to us by default, since it does nothing for each of these handlers.
But all of the groundwork is in place. We can subclass (or inherit from) HTMLParser, override some of these member functions, and have a working parser.
Python: | from HTMLParser import HTMLParser
class MyParser(HTMLParser):
def __init__(self):
HTMLParser.__init__(self)
self.__anchor_count = 0
def handle_starttag(self, tag, attrs):
if tag == 'a':
self.anchor_count += 1
def __get_anchor_count(self):
return self.__anchor_count
def __set_anchor_count(self, new_count):
self.__anchor_count = new_count
anchor_count = property(__get_anchor_count, __set_anchor_count) |
And now, when we want to count anchor tags, we need only:
Python: | html = """some HTML here"""
p = MyParser()
p.feed(html)
print p.anchor_count |
Could we have written a customized HTML parser that easily without inheritance? Not a chance. And most of that was just extra code to pretty it up (the anchor_count property).
There's obviously more to object-oriented programming that this, but hopefully this has been a good look at what it's basically about. Please, feel free to ask questions and ask for further explanation. |
|
|
|
|
|
Sponsor Sponsor
|
|
|
Tony
|
Posted: Mon Jul 11, 2005 5:52 pm Post subject: (No subject) |
|
|
Great as always. Please remind me again why you don't publish books?
It was pleasant to follow even though I'm already familiar with OOP. Reminded me how much I love Ruby and hate C++ |
|
|
|
|
|
wtd
|
Posted: Mon Jul 11, 2005 9:53 pm Post subject: (No subject) |
|
|
Thanks for the external validation. |
|
|
|
|
|
[Gandalf]
|
Posted: Mon Jul 11, 2005 10:20 pm Post subject: (No subject) |
|
|
Well written and explained. I spent more than an hour trying to comprehend it all. It cleared up some things about OOP, but I see that it is a very large topic, and I'm too early on in C++ to understand it all. I lack the knowledge of all the other languages, so I tried to follow along with C++, and sometimes looking upon Java. I still don't understand how private and public members are supposed to work, what if the user wanted to change the 'first_name'. Would you then call a function in that class to change it?
I'm also going to have to look up "friend functions" and some other concepts, but now I have a good reference for when I understand more .
For learnign OOP, do you think I should look at Java instead of C++? It seems the syntax is quite a bit easier, and it seems simpler to understand.
Maybe this should be stickified? OOP is probably one of the most important concepts in programming nowadays. |
|
|
|
|
|
lyam_kaskade
|
Posted: Mon Jul 11, 2005 10:53 pm Post subject: (No subject) |
|
|
[Gandalf] wrote:
For learnign OOP, do you think I should look at Java instead of C++? It seems the syntax is quite a bit easier, and it seems simpler to understand.
Not that I'm an expert, but if you want to learn OOP the best language would probably be Ruby. I just started it a few days ago and it's amazing. Makes me wonder why people even use C++ anymore.[/quote] |
|
|
|
|
|
wtd
|
Posted: Mon Jul 11, 2005 10:54 pm Post subject: (No subject) |
|
|
[Gandalf] wrote: I still don't understand how private and public members are supposed to work, what if the user wanted to change the 'first_name'. Would you then call a function in that class to change it?
A private member cannot be accessed outside of the class.
A public member may be accessed outside of the class.
As the class is defined, you wouldn't change first_name. That's part of the class' design. I didn't want the names to be able to be changed. By aking those variable private, and not allowing access to them outside the class, and by not providing any member function to alter the names... by doing all of that, I ensure that no one can alter the internal state of a name.
But there is a way.
There's a third qualifier, besides public and private. It's called "protected". It means that the member is still inaccessible outside the class, but that the member is accessible to subclasses.
If I had written the Name class as:
c++: | class Name
{
protected:
std::string first_name, last_name;
public:
Name(std::string f, std::string l)
: first_name(f), last_name(l)
{ }
std::string first() const
{
return first_name;
}
std::string last() const
{
return last_name;
}
std::string full_name() const
{
return first() + " " + last();
}
friend std::ostream& operator<<(std::ostream& out, const Name& n)
{
return out << n.full_name();
}
}; |
I could then write a subclass which allows me to set the first and last names.
c++: | struct MutableName : public Name
{
MutableName(std::string f, std::string l)
: Name(f, l)
{ }
void set_first(std::string f)
{
first_name = f;
}
void set_last(std::string l)
{
last_name = l;
}
}; |
[Gandalf] wrote: I'm also going to have to look up "friend functions" and some other concepts, but now I have a good reference for when I understand more .
First, let's recap the code causing the confusion.
c++: | friend std::ostream& operator<<(std::ostream& out, const Name& n)
{
return out << n.full_name();
} |
The key thing to understand frst is that this isn't a member function of the Name class. It's just a regular old function.
So, why do we have to call it a friend of Name?
In this case, as it happens, we actually don't have to since we're only calling a public member function on the Name passed in. But, imagine we wrote it as:
c++: | friend std::ostream& operator<<(std::ostream& out, const Name& n)
{
return out << n.first_name << " " << n.last_name;
} |
Do you see the problem?
The first_name and last_name members aren't public, so I can't access them... unless the function is declared to be a friend of Name, and thus is given access to Name's private members.
[Gandalf] wrote: For learnign OOP, do you think I should look at Java instead of C++? It seems the syntax is quite a bit easier, and it seems simpler to understand.
It may seem simpler, but that's because Java is lacking features that C++ has. I'm not necessarily going to say that makes C++ better, or Java better. That's for you to decide. Either way, though, you should make an informed choice. |
|
|
|
|
|
wtd
|
Posted: Mon Jul 11, 2005 11:03 pm Post subject: (No subject) |
|
|
lyam_kaskade wrote: Not that I'm an expert, but if you want to learn OOP the best language would probably be Ruby.
I'm not inclined to disagree. Ruby provides all of the OOP concepts you'll need for a good while, and makes them syntactically very nice.
Plus you get to write quick classes for testing purposes in the interactive interpreter. |
|
|
|
|
|
Delos
|
Posted: Tue Jul 12, 2005 10:16 am Post subject: (No subject) |
|
|
Really enjoyed that one. Now if only [syntax] highlighted Ruby code properly. I've only barely introduced myself to Ruby, so I can understand some of it. Will these Reflections be continuing some time soon? Or is that it for now?
And you have a type in there somewhere - "Wen" instead of "When". |
|
|
|
|
|
Sponsor Sponsor
|
|
|
wtd
|
Posted: Tue Jul 12, 2005 4:39 pm Post subject: (No subject) |
|
|
Delos wrote: Really enjoyed that one.
Glad I could be of service.
Delos wrote: Will these Reflections be continuing some time soon? Or is that it for now?
Ask and ye shell receive.
A common source of confusion for programmers new to object-oriented programming is the difference between single inheritance and multiple inheritance.
We've already seen single inheritance in action with the Name class and the HTMLParser example, but we've yet to see multiple inheritance. The reason is quite simple: multiple inheritance is more complex than single inheritance.
Indeed, for that reason many languages don't even include the ability to use multiple inheritance. Languages like Ruby, Java, C#, Objective-C, and plenty of others are in this category.
However, there are other languages that are perfectly happy to include multiple inheritance, and deal with the ramifications of that feature. C++, Python, Eiffel, and O'Caml are notable inclusions in this category.
One thing you will note as a major difference is that languages with single inheritance only tend to have all objects descend from a single base class. In Ruby and Java, for instance, all objects are ultimately descended from each language's respective Object class. On the other hand, multiple inheritance languages tend to feature no single root class from which all others descend. Rather they feature many root classes.
Now, I'm not going to tell you that one approach or the other is superior. If anything, I would tell you to understand both, because you will almost certainly run into both.
What I will do is show you an example of where we might use multiple inheritance.
A classic programming exercise is to read in 10 grades and find the average, minimum, and maximum grades. This is fairly simple, but let's think of it in an object-oriented manner. What objects do we have?
We have a student, and grades being read in, which are simple ints.
Now, let's think about the student. We can think about it in two ways. We can think about in terms of a "has a" relationship. That would suggest to us that a student has both a name and a collection of grades.
But we're looking for an "is a" relationship, and for that we can say a student is a name and a collection of grades. We already have a class which can handle a name.
c++: | class Name
{
private:
std::string first_name, last_name;
public:
Name(std::string f, std::string l)
: first_name(f), last_name(l)
{ }
std::string first() const
{
return first_name;
}
std::string last() const
{
return last_name;
}
std::string full_name() const
{
return first() + " " + last();
}
friend std::ostream& operator<<(std::ostream& out, const Name& n)
{
return out << n.full_name();
}
}; |
So we just have to think about a GradeCollection class. Fortunately C++ features a handy set of collections classes. For this purpose we'll use a std::vector of ints.
c++: | struct GradeCollection : public std::vector<int>
{
GradeCollection() : std::vector<int>() { }
GradeCollection(size_t initial_size)
: std::vector<int>(initial_size)
{ }
void add_grade(int grade)
{
push_back(grade);
}
int num_grades() const
{
return size();
}
int sum() const
{
int sum(0);
for (int i(0); i < size(); ++i)
sum += at(i);
return sum;
}
double average() const
{
return sum() / static_cast<double>(num_grades());
}
int min_grade() const
{
int min(at(0));
for (int i(1); i < num_grades(); ++i)
if (min > at(i))
min = at(i);
return min;
}
int max_grade() const
{
int max(at(0));
for (int i(1); i < num_grades(); ++i)
if (max < at(i))
max = at(i);
return max;
}
friend std::ostream& operator<<(std::ostream& out, const GradeCollection gc)
{
out << "(";
for (int i(0); i < gc.num_grades() - 1; ++i)
out << gc.at(i) << ", ";
out << gc.at(gc.num_grades() - 1) << ")";
return out;
}
std::string grades_as_string() const
{
std::ostringstream ss;
ss << *this;
return ss.str();
}
}; |
Now that works pretty well, but let's think about it. When we inherit std::vector, we get a lot of useful functions from it "for free", and it saves us a lot of time. But it also means that our GradeCollection is a std::vector. What if we don't want to establish that relationship. Well, C++ features the somewhat unique ability to let us inherit without establishing such a relationship. We need only replace the first line of the class with:
c++: | struct GradeCollection : private std::vector<int> |
This marks all of the members inherited from std::vector as private, and thus we nullify the "is a" relationship.
There's just one remaining problem. We may wish to create a more advanced GradeCollection subclass that will require access to the members from std::vector. The solution is to rely on that third, less well-known qualifier: protected.
c++: | struct GradeCollection : protected std::vector<int> |
All of the members from std::vector are now marked as protected, so we cannot use them externally on a GradeCollection object, but we can use them internally in subclasses.
And we return to our student. For our Student to be both a Name and a GradeCollection, it must publicly inherit from both of those classes.
c++: | struct Student : public Name, public GradeCollection
{
Student(std::string f, std::string l, size_t num_grades)
: Name(f, l)
, GradeCollection(num_grades)
{ }
}; |
That's it. That's the beauty of inheritance.
Now to create a student and go through the reading of ten grades.
c++: | #include <string>
#include <iostream>
#include "Student.h"
int main()
{
Student bob("Bob", "Smith", 10);
for (int i(0); i < 10; ++i)
{
int temp;
do
{
std::cout << "grade: ";
std::cout.flush();
std::cin >> temp;
} while (std::cin.fail());
bob.add_grade(temp);
}
std::cout << bob.full_name() << std::endl
<< bob.grades_as_string() << std::endl
<< "Average: " << bob.average() << std::endl
<< "Min grade: " << bob.min_grade() << std::endl;
<< "Max grade: " << bob.max_grade() << std::endl;
return 0;
}
|
Disclaimer: not all of this code has been tested. Please let me know if you have problems. |
|
|
|
|
|
wtd
|
Posted: Tue Jul 12, 2005 5:51 pm Post subject: (No subject) |
|
|
Not quite as sure of the quality of this one. Feel free to criticize constructively.
Of course, it's somewhat erroneous to say that languages like Java and C# lack multiple inheritance. Thus far we've only looked at implementation inheritance, where inheriting classes get the implementation of features from parent classes.
These languages and others also implement what we call "interface inheritance". An interface doesn't actually define how a particular method or set of methods works. It just says they're in the class, what arguments they take, and what they return.
Just as implementation inheritance created an "is a" relationship, we can think of interface inheritance as an "is a" relationship via a "does" relationship. Rather than being identified with a parent class and thus fitting a certain classification, the class fits that classification based on what it does.
Let's look at the Student class again, this time in C# (just to mix things up). We already have a Name class that will work quite nicely.
c-sharp: | class Name
{
private string firstName, lastName;
public Name(string f, string l)
{
firstName = f;
lastName = l;
}
public string First
{
get { return firstName; }
}
public string Last
{
get { return lastName; }
}
public string FullName
{
get { return this.First() + " " + this.Last(); }
}
public string ToString()
{
return this.FullName;
}
} |
Now we need an interface for an object that can collect grades. This spells out what we expect any class implementing the IGradeCollector interface to be capable of.
c-sharp: | interface IGradeCollector
{
public void AddGrade(int grade);
public int Sum { get; }
public int NumberOfGrades { get; }
public int Average { get; }
public int MinGrade { get; }
public int MaxGrade { get; }
public string GradesAsString { get; }
} |
We can now inherit Name and implement IGRadeCollector to get our Student class.
c-sharp: | using System.Collections;
using System.Text;
class Student : Name, IGradeCollector
{
private ArrayList grades;
public Student(string f, string l, int numGrades)
: base(f, l)
{
grades = new ArrayList(numGrades);
}
public void AddGrade(int grade)
{
grades.Add(grade);
}
public int Sum
{
get
{
int sum = 0;
IEnumerator e = grades.GetEnumerator();
while (e.MoveNext())
sum += e.Current;
return sum;
}
}
public int NumberOfGrades
{
get { return grades.Count; }
}
public int Average
{
get { return (double)this.Sum / this.NumberOfGrades; }
}
public int MinGrade
{
get
{
int min = this.Item[0];
IEnumerator e = grades.GetEnumerator(1, grades.Count - 1);
while (e.MoveNext())
if (min > e.Current)
min = e.Current;
return min;
}
}
public int MaxGrade
{
get
{
int max = this.Item[0];
IEnumerator e = grades.GetEnumerator(1, grades.Count - 1);
while (e.MoveNext())
if (max < e.Current)
max = e.Current;
return max;
}
}
public string GradesAsString
{
get
{
StringBuilder sb = new StringBuilder("(");
IEnumerator e = grades.GetEnumerator(0, grades.Count - 2);
while (e.MoveNext())
sb.AppendFormat("{0}, ", e.Current);
sb.AppendFormat("{0})", grades.Item[grades.Count - 1]);
return sb.ToString();
}
}
} |
And I won't bother to post the finished , since it'd look pretty much like the C++ program in the previous post.
The important thing to realize about interfaces is that they permit you to specify what an object can do, while leaving the details of how it does that upto the implementing class. |
|
|
|
|
|
bugzpodder
|
|
|
|
|
Zeroth
|
Posted: Fri Jun 27, 2008 5:28 pm Post subject: Re: Reflections on OOP |
|
|
I know this is a pretty old topic, but I thought this may be the best place to post this. A friend showed me this: Myths about OOP. Of course, he goes way overboard in his hatred of OOP, but it has some good points and gotchas for OOP programmers. Mostly, all I see are good points, that can also be applied to procedural programming. Rather, instead of being problems with OOP in general, they are more, "Don't overdo things, idiot." or the DOTI principle. With any code, you need to evaluate the benefit the feature provides, as well as potential work that would need to be done in any future changes. |
|
|
|
|
|
michaelp
|
Posted: Tue Jul 01, 2008 6:55 pm Post subject: RE:Reflections on OOP |
|
|
He really tries going against OOP there.
It is a great concept there, I can't imagine doing anything large scale without it.
And the first chapter of Thinking in C++, Volume 1, is probably one of the best introductions to it that I have ever read. ( I haven't read that much though, but while I was reading it, I just knew it was good) |
|
|
|
|
|
wtd
|
Posted: Tue Jul 01, 2008 7:49 pm Post subject: RE:Reflections on OOP |
|
|
Any criticism of OOP with no reference to functional programming is inherently lacking in credibility. |
|
|
|
|
|
Zeroth
|
Posted: Tue Jul 01, 2008 8:27 pm Post subject: Re: Reflections on OOP |
|
|
Mostly, he just tries to push his own paradigm, which makes sense in a way... except he's one of those people that believe databases are adequate programming languages. |
|
|
|
|
|
|
|