Object Oriented Design

A good object oriented design is not easy. What classes do you want to make? What goes into what class? What methods should each class include? What should the instance variables be?

These are tough questions, and a good computer scientist spends more time thinking about the design than the actual coding. A good design makes the project scalable and fits a more general problem, which enables future extensions.

This is why in CS1 we have so many extra credit options. It rewards students who implement good designs – if you have, you won’t have to change very much code to do the extra credit options on the labs.

But before we talk about the design principles around object oriented programming, we need to discuss one last concept: inheritance.

Inheritance

As you know, in Python, everything is an object. But we’ve learned that objects are always instances of classes. So where do all these classes come from? They all come from an uber-class called object. This class is the parent class from which every other class spawns.

So why would we use inheritance (other than the fact that it’s already built into Python). Let’s go back to our student example. We have this Student class, where each student has some properties. Let’s build a model to predict given some factors like weather how many students will show up to class.

We all know there are different types of students. For the sake of this model, let’s assume there are three types of students: slackers, indifferents, and try-hards. Now comes the tricky part: each student has his or her own formula to determine whether to attend class.

Whenever you begin designing a class, you should start grouping together large common elements. This is kind of like those brainstorming web-diagrams when you begin writing an essay.

We certainly need a Student class, as that’s what we’re dealing with. What is the integral information for each Student? They probably have a gpa, a grade associated with CS1 so far, and a major. Students who are majors in Computer Science may weight CS1 with more importance than non-majors. Okay, with that information here is our Student class.

class Student(object):
    '''Defines the Student class, descriptive of a Dartmouth student.'''

    def __init__(self, gpa=3.0, grade=85, major='Economics'):
        self.gpa = gpa
        self.major = major
        self.grade = grade

You’ll notice something different about the class declaration.

class Student(object):

instead of the usual

class Student:

This means that Student is a child class of object; in otherwords, every Student object has all the methods that any instance of the object class has. This is the super class that all Python objects descend from. In Python, this is considered better style than the implicit header.

Now, as we discussed above, there are three separate types of students. Let’s go ahead and make the child classes.

class Slacker(Student):
    '''Defines the slacker student.'''

    def go_class(self):
        return false            

Wow, that was pretty simple. As it turns out, a slacker student never goes to class. The important thing here is that by being a subclass of Student, all Slacker objects automatically get access to all methods and instance variables of the Student class! So you could make a new Slacker object simply by doing

fratbro = Slacker(2.5, 77, 'Alcohol')

And we have a new object! It even inherits the __init__ method. The good student class is easy, too.

class TryHard(Student):
    '''Defines the model student.'''

    def go_class(self):
        return true

Okay, let’s talk about the class that affects 99% of us. The average student.

class Average(Student):
    '''Defines the typical Dartmouth student.'''

    def go_class(self, weather=35):
        return not (self.gpa > 3.8 or self.grade > 99 or weather < -15)

So the average student makes the decision based on a combination of factors including her grade point average, her grade in the class, and the weather on that particular day.

>>> bob = Average(4.0, 80, 40)
>>> print bob.go_class()
False

Design Patterns

The concept of inheritance can be confusing at first, but it isn’t too bad once you get used to it: A subclass automatically gets all the methods and instance variables of the parent class.

The harder question is how to design the class breakdown. How did we decide that Student would be the parent class and that there would be three subclasses. For that matter, how did we even decide that there would be a parent class? After all, it’s not necessary for all problems.

For your labs, you will have to practice good object oriented design which involves thinking about

This is more of an art than a science, and you will get a feel for this as you do more object oriented programming. For those of you continuing with computer science, you will cover this in great depth in CS10, which teaches a purely object oriented language known as Java.

For now, let’s discuss some general guidelines.

  1. Group common elements and functions together.

If there is a great commonality between things that have many different functions, then that is a great place to create a class. This rule led to the decision to create a Student class. A Student object needs to know many things, and they have many methods.

  1. Carve out the differences.

Seldom will a single class obviate all your needs for the entire problem. Usually it is more complicated than that. Find out and list all the different functionalities your class has to take care of.

Sometimes, you’ll have to create a new class or a subclass to take care of them. Othertimes you won’t have to. When to do which is a tough choice. Again, there is no right or wrong answer here. In this problem, I decided to make different classes instead of different methods.

I could have done the following instead

class Student(object):

    def average_go_class(self):
        # Some code here
        pass

    def slacker_go_class(self):
        # Some code here
        pass

    def tryhard_go_class(self):
        # Some code here
        pass

I could have broken it up into methods instead. Why did I choose a subclass? I decided that, given the nature of the problem, the differences between the types of students would not simply be limited to the way they decide to go to class. The way they do homework, engage in extra curriculars, etc would all be different. These differences, to me, were large enough to warrant a separate class.

  1. Think about generality.

I’ve stated this above, but this is again why we encourage you to do extra credit. Imagine an extra credit option were now to add four other methods to the three different types of students: study, extra_curriculars, drink, sleep.

If I had not made separate classes, it would quickly become a really messy class. I’d have to add a separate instance variable called type within the Student class so that each object could remember what type of student it was. Even worse, I’d have to do something like this.

def study(self):
    if self.type == 'Average':
        self.average_study()
    elif self.type == 'Slacker':
        self.slack_study()
    else
        self.hard_study()

And every method would have to look like that. That’s really awful, and it’s a bad way to extend the class. So as we add more general features, we quickly realize that this is becoming unmanageable. That is a classic symptom of a design problem.

The best thing to do here is sit back and redesign. By making minor design changes, the entire coding process becomes much simpler and easier. An added benefit is that a good design also makes the code more readable, which is important to convey professionalism and for others reading your code (ahem – those of us that grade, for example).

If you’re not completely clear on all of this, don’t worry about it. The only way to get better is to practice, and every lab from now on will provide you a chance to do that. In recitation we’ll be discussing object oriented design so you can get further ideas as to how to approach problems.