# Designing Object-Oriented Programs ## Animal classes Let's (pretend to) use Python to model some ecological ideas. Consider four concepts that we will model using classes: * Animal * Tiger * Moose * Habitat Types of objects can stand in different kinds of relations to each other. One kind is an "is-a" relationship: a tiger is an animal, and a moose is an animal. In Python we typically represent this relationship using subclasses: we would define a superclass `Animal` that is extended by the `Tiger` and `Moose` subclasses. ```python class Animal: pass class Tiger(Animal): pass class Moose(Animal): pass ``` Where does a habitat fit into this picture? An animal isn't a habitat, and a habitat isn't an animal. There's functioanlity that a habitat has that an animal doesn't, and vice versa. But there's still a connection: habitats "contain" animals. We can call this a "has-a" relationship. These tend to be represented by data fields in an object. ```python class Habitat: def __init__(self): self.animals = [] ``` ## Car classes We can play the same game with another list of concepts: * Car * Engine * Convertible Every convertible is a car, so this is an is-a relationship. And every car has an engine. (Transitively, every convertible has an engine.) ```python class Engine: pass class Car: def __init__(self): self.engine = Engine() class Convertible(Car): def __init__(self): super().__init__() miata = Convertible() print(miata.engine) ``` ## Library classes For yet another example, consider the `Library` code we've been writing. Books and movies *are* library items, so they are subclasses. But if we define a `Library` class that contains a list of library items, the picture gets more complicated. What is the has-a relationship? A `Library` has library items, but each library item is also associated with some library, so the arrow could go the other direction. How do we represent this in our code? It depends what we want to model! Depending on how our code will be applied, we may want a library to track its items, a library item to track its library, or both. Tracking a bi-directional "has-a" relationship can be a bit tricky. Consider this code: ```python class Library: def __init__(self): self.items = [] def add(self, item): self.items.append(item) def search(self, query: str) -> list: return [item for item in self.items if item.matches(query)] class LibraryItem: def __init__(self, library: Library): self.checked_out = False self.library = library def checkout(self): self.checked_out = True class Book(LibraryItem): def __init__(self, title: str, author: str, library: Library): self.title = title self.author = author super().__init__(library) def matches(self, query: str) -> bool: return (not self.checked_out) and (query in self.title or query in self.author) class Movie(LibraryItem): def __init__(self, title: str, director: str, actors: list, library: Library): self.title = title self.director = director self.actors = actors super().__init__(library) def matches(self, query: str) -> bool: return (not self.checked_out) and \ query in self.title or query in self.director or \ any([query in actor for actor in self.actors]) ProvPubLib = Library() cc = Book("Cat's Cradle", "Kurt Vonnegut", ProvPubLib) ``` We've created a book that belongs to the Providence Public Library, but the library doesn't know about it yet! This isn't exactly broken code, but it could get very confusing -- it's a reasonable assumption for us to make as developers that a book belongs to a library if and only if that book is in that library's list of items. We might write code that assumes this and will break when it comes across this orphaned copy of Cat's Cradle. So we need to be careful, either not to break this symmetry, or not to assume that this symmetry holds. More generally we call this kind of property an *invariant*. ## Space classes For one final inheritance exercise: let's figure out the relations between the terms * Star * Planet * Gas giant * Galaxy * Spiral Galaxy * Earth * Jupiter <details> <summary><B>Think, then click!</B></summary> The answer isn't unique. One reasonable way to do it would be: A spiral galaxy is a galaxy. A gas giant is a planet. Earth is a planet. Jupiter is a gas giant. A galaxy has stars and planets. A star has planets (that orbit it). But let's focus on the lines "Earth is a planet. Jupiter is a gas giant." Again, depending on what exactly we're trying to model, this could be interpreted in different ways: ```python class Planet: pass class GasGiant(Planet): pass class Earth(Planet): pass class Jupiter(GasGiant): pass ``` or, instead, ```python class Planet: pass class GasGiant(Planet): pass earth = Planet() jupiter = Planet() ``` A class describes a kind of thing. If we wanted to model multiple versions of Earth -- say, one now, one a billion years ago -- we could create two objects of the `Earth` class. But maybe we only want one unique Earth, that carries its own data. Then we would use the latter approach. Any *value* that we want to compute with should be an object of some class. </details> ## Dataclasses One last note on setting up classes. We used the `@dataclass` feature for a while, and then slowly dropped it for more flexible plain `class`es. But using `dataclass` does some nice things for us. We can display them easily, and also compare them: two objects of a data class are equal exactly when all of their fields match. As a general heuristic, it makes sense to use dataclasses to store "records," pieces of data that are merely lists of values that will not change themselves during execution. In our library example: it might make sense to track authors, directors, etc. using a `Person` dataclass that stores a name, year of birth, etc. This is static information. But it wouldn't make sense for a `Library` to be a data class, since the list of available books will be updated over time, and we'll want to define search and checkout methods in this class.