Chapter 5. When to Use Object-oriented Programming

In the previous chapters, we've covered many of the defining features of Object-oriented Programming. We now know the principles and paradigms of Object-oriented Design, and we've covered the syntax of Object-oriented Programming in Python.

Yet, things are still hazy when it comes to knowing how and when to apply these principles and syntax. Here, we'll discuss some of the more useful applications of the knowledge we've gained, while in Chapter 7, we'll cover something just as important: when not to use them! In this chapter we'll cover:

  • How to recognize objects
  • Data and behaviors, once again
  • Wrapping data in behavior using properties
  • Restricting data using behavior
  • The Don't Repeat Yourself principle
  • Recognizing code that is the same

Treat objects as objects

This may seem obvious, but you should generally give separate objects in your problem domain a special class in your code. We've seen examples of this in the case studies in previous chapters; the process is generally to identify objects in the problem and then model their data and behaviors.

Identifying objects is a very important task in object-oriented analysis and programming. But it isn't always as easy as counting the nouns in a short paragraph, as we've been doing. Remember, objects are things that have both data and behavior. If we are working with only data, we are often better off storing it in a list, set, dictionary, or some other Python data structure (which we'll be covering thoroughly in the next chapter). On the other hand, if we are working with only behavior, with no stored data, a simple function is more suitable.

An object, however, has both data and behavior. Most Python programmers use built-in data structures unless (or until) there is an obvious need to define a class. This is a good thing; there is no reason to add an extra level of abstraction if it doesn't help organize our code. Sometimes, though, the "obvious" need is not so obvious.

A Python programmer often starts by storing data in a few variables. As our program expands, we will later find that we are passing the same set of related variables to different functions. This is the time to think about grouping both variables and functions into a class. If we are designing a program to model polygons in two-dimensional space, we might start with each polygon being represented as a list of points. The points would be modeled as two-tuples (x,y) describing where that point is located. This is all data, stored in two nested data structures (specifically, a list of tuples):

	square = [(1,1), (1,2), (2,2), (2,1)]

Now, if we want to calculate the distance around the perimeter of the polygon, we simply need to sum the distances between the two points, but to do that, we need a function to calculate the distance between two points. Here are two such functions:

	import math
	
	def distance(p1, p2):
		return math.sqrt((p1[0]-p2[0])**2 + (p1[1]-p2[1])**2)

	def perimeter(polygon):
		perimeter = 0
		points = polygon + [polygon[0]]		
		for i in range(len(polygon)):		
			perimeter += distance(points[i], points[i+1])
		return perimeter

Now, as object-oriented programmers, we clearly recognize that a polygon class could encapsulate the list of points (data) and the perimeter function (behavior). Further, a point class, such as we defined in Chapter 2 might encapsulate the x and y coordinates and the distance method. But should we do this?

For the previous code, maybe, maybe not. We've been studying object-oriented principles long enough that we can now write the object-oriented version in record time:

	import math

	class Point:
		def __init__(self, x, y):
			self.x = x	
			self.y = y

		def distance(self, p2):
			return math.sqrt((self.x-p2.x)**2 + (self.y-p2.y)**2)

	class Polygon:
		def __init__(self):
			self.vertices = []
		
		def add_point(self, point):
			self.vertices.append((point))
	
		def perimeter(self):
			perimeter = 0
			points = self.vertices + [self.vertices[0]]
			for i in range(len(self.vertices)):
				perimeter += points[i].distance(points[i+1])
			return perimeter

As you can see from the highlighted sections, there is twice as much code here as there was in our earlier version, although we can argue that the add_point method is not strictly necessary.

Now, to understand the difference a little better, let's compare the two APIs in use. Here's how to calculate the perimeter of a square using the object-oriented code:


>>> square = Polygon()
>>> square.add_point(Point(1,1))
>>> square.add_point(Point(1,2))
>>> square.add_point(Point(2,2))
>>> square.add_point(Point(2,1))
>>> square.perimeter()
4.0

That's fairly succinct and easy to read, you might think, but let's compare it to the function-based code:


>>> square = [(1,1), (1,2), (2,2), (2,1)]
>>> perimeter(square)
4.0

Hmm, maybe the object-oriented API isn't so compact! On the other hand, I'd argue that it was easier to read than the function example: How do we know what the list of tuples is supposed to represent in the second version? How do we remember what kind of object (a list of two-tuples? That's not intuitive!) we're supposed to pass into the perimeter function? We would need a lot of external documentation to explain how these functions should be used.

In contrast, the object-oriented code is relatively self documenting, we just have to look at the list of methods and their parameters to know what the object does and how to use it. By the time we wrote all the documentation for the functional version, it would probably be longer than the object-oriented code.

Besides, code length is a horrible indicator of code complexity. Some programmers (thankfully, not many of them are Python coders) get hung up on complicated, "one liners", that do incredible amounts of work in one line of code. One line of code that even the original author isn't able to read the next day, that is. Always focus on making your code easier to read and easier to use, not shorter.

As a quick exercise, can you think of any ways to make the object-oriented Polygon as easy to use as the functional implementation? Pause a moment and think about it.

Really, all we have to do is alter our Polygon API so that it can be constructed with multiple points. Let's give it an initializer that accepts a list of Point objects. In fact, let's allow it to accept tuples too, and we can construct the Point objects ourselves, if needed:

		def __init__(self, points = []):
			self.vertices = []
			for point in points:
				if isinstance(point, tuple):
					point = Point(*point)
				self.vertices.append(point)

This example simply goes through the list and ensures that any tuples are converted to points. If the object is not a tuple, we leave it as is, assuming that it is either a Point already, or an unknown duck typed object that can act like a Point.

As we can see, it's not always easy to identify when an object should really be represented as a self-defined class. If we have new functions that accept a polygon argument, such as area(polygon) or point_in_polygon(polygon, x, y), the benefits of the object-oriented code become increasingly obvious. Likewise, if we add other attributes to the polygon, such as color or texture, it makes more and more sense to encapsulate that data into a class.

The distinction is a design decision, but in general, the more complicated a set of data is, the more likely it is to have functions specific to that data, and the more useful it is to use a class with attributes and methods instead.

When making this decision, it also pays to consider how the class will be used. If we're only trying to calculate the perimeter of one polygon in the context of a much greater problem, using a function will probably be quickest to code and easiest to use "one time only". On the other hand, if our program needs to manipulate numerous polygons in a wide variety of ways (calculate perimeter, area, intersection with other polygons, and more), we have most certainly identified an object; one that needs to be extremely versatile.

Pay additional attention to the interaction between objects. Look for inheritance relationships; inheritance is impossible to model elegantly without classes, so make sure to use them. Look for the other types of relationships we discussed in Chapter 1: association and composition. Composition can, technically, be modeled using only data structures; for example, we can have a list of dictionaries holding tuple values, but it is often less complicated to create an object, especially if there is behavior associated with the data.

Note

Don't rush to use an object just because you can use an object, but never neglect to create a class when you need to use a class.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.226.170.187