# 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.