Skip to content

Commit

Permalink
Re-write main documentation.
Browse files Browse the repository at this point in the history
  • Loading branch information
Warren Smith committed Jun 23, 2019
1 parent a9fc9cf commit b6b51f7
Showing 1 changed file with 291 additions and 8 deletions.
299 changes: 291 additions & 8 deletions staticmodel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,299 @@
Overview
********
**Static Model** provides a simple framework for modeling objects that
might otherwise be modeled using persistence technologies such as
the Django ORM, but that do not belong in the database.
**Static Model** provides a simple framework for modeling
complex constants.
******
Models
******
***************************
What is a complex constant?
***************************
A model is defined using a class definition to create a sub-class
of StaticModel.
The simple answer is that it is a constant associated with multiple values.
The detailed answer takes more explanation. That requires an exploration
of constants in increasing level of complexity.
Let's first look at simple constants:
>>> ANIMAL_TYPE_ID_DOG = 1
>>> ANIMAL_TYPE_ID_HAWK = 2
>>>
>>>
>>> class Animal(object):
... def __init__(self, name, type_id):
... self.name = name
... self.type_id = type_id
...
... self.can_fly = type_id == ANIMAL_TYPE_ID_HAWK
... self.domesticated = type_id == ANIMAL_TYPE_ID_DOG
...
... def fly(self):
... if self.can_fly:
... return "This is {}. Taking off now.".format(self.name)
... elif self.domesticated:
... return "This is {}. My owner is nuts.".format(self.name)
... else:
... return "This is {}. Unable to comply.".format(self.name)
Naming and using constants this way is common. The shared prefix in the name
establishes that these constants are associated with each other. The code
compares variables to the constants to make decisions.
But, what if we need the constant values to have associated values? That sounds
like the perfect use case for a mapping:
>>> ANIMAL_TYPE_ID_DOG = 1
>>> ANIMAL_TYPE_ID_HAWK = 2
>>>
>>>
>>> ANIMAL_TYPE_ID_NAME = {
... ANIMAL_TYPE_ID_DOG: 'Dog',
... ANIMAL_TYPE_ID_HAWK: 'Hawk',
... }
>>>
>>>
>>> class Animal(object):
... def __init__(self, name, type_id):
... self.name = name
... self.type_id = type_id
...
... self.can_fly = type_id == ANIMAL_TYPE_ID_HAWK
... self.domesticated = type_id == ANIMAL_TYPE_ID_DOG
...
... def fly(self):
... type_name = ANIMAL_TYPE_ID_NAME[self.type_id]
... if self.can_fly:
... return "This is {}. I'm a {}. Taking off now.".format(self.name, type_name)
... elif self.domesticated:
... return "This is {}. I'm a {}. My owner is nuts.".format(self.name, type_name)
... else:
... return "This is {}. I'm a {}. Unable to comply.".format(self.name, type_name)
This works ok. But when the code gets more complex and we add additional
constant values, our code gets more complicated:
>>> ANIMAL_TYPE_ID_DOG = 1
>>> ANIMAL_TYPE_ID_HAWK = 2
>>> ANIMAL_TYPE_ID_CAT = 3
>>> ANIMAL_TYPE_ID_SNAKE = 4
>>>
>>>
>>> ANIMAL_TYPE_ID_NAME = {
... ANIMAL_TYPE_ID_DOG: 'Dog',
... ANIMAL_TYPE_ID_HAWK: 'Hawk',
... ANIMAL_TYPE_ID_CAT: 'Cat',
... ANIMAL_TYPE_ID_SNAKE: 'Snake',
... }
>>>
>>>
>>> class Animal(object):
... def __init__(self, name, type_id):
... self.name = name
... self.type_id = type_id
...
... self.can_fly = type_id == ANIMAL_TYPE_ID_HAWK
... self.domesticated = type_id in (ANIMAL_TYPE_ID_DOG, ANIMAL_TYPE_ID_CAT)
... self.can_swim = type_id != ANIMAL_TYPE_ID_HAWK
...
... if type_id == ANIMAL_TYPE_ID_SNAKE:
... self.leg_count = 0
... elif type_id == ANIMAL_TYPE_ID_HAWK:
... self.leg_count = 2
... else:
... self.leg_count = 4
...
... def fly(self):
... type_name = ANIMAL_TYPE_ID_NAME[self.type_id]
... if self.can_fly:
... return "This is {}. I'm a {}. Taking off now.".format(self.name, type_name)
... elif self.domesticated:
... return "This is {}. I'm a {}. My owner is nuts.".format(self.name, type_name)
... else:
... return "This is {}. I'm a {}. Unable to comply.".format(self.name, type_name)
...
... def walk(self):
... if self.leg_count > 0:
... return "Walking"
... else:
... return "No. I'm a {}.".format(ANIMAL_TYPE_ID_NAME[self.type_id])
...
... def swim(self):
... return "Swimming" if self.can_swim else "No. I'm a {}.".format(ANIMAL_TYPE_ID_NAME[
... self.type_id])
This code can be improved a bit by giving the constant mapping values
multiple attributes. We can use some more mappings to make this happen.
>>> ANIMAL_TYPE_ID_DOG = 1
>>> ANIMAL_TYPE_ID_HAWK = 2
>>> ANIMAL_TYPE_ID_CAT = 3
>>> ANIMAL_TYPE_ID_SNAKE = 4
>>>
>>>
>>> ANIMAL_TYPE_ATTRIBUTES = {
... ANIMAL_TYPE_ID_DOG: {'name': 'Dog', 'can_fly': False, 'domesticated': True, 'leg_count': 4,
... 'can_swim': True},
... ANIMAL_TYPE_ID_HAWK: {'name': 'Hawk', 'can_fly': True, 'domesticated': False, 'leg_count': 2,
... 'can_swim': False},
... ANIMAL_TYPE_ID_CAT: {'name': 'Cat', 'can_fly': False, 'domesticated': True, 'leg_count': 4,
... 'can_swim': True},
... ANIMAL_TYPE_ID_SNAKE: {'name': 'Snake', 'can_fly': False, 'domesticated': False,
... 'leg_count': 0, 'can_swim': True},
... }
>>>
>>>
>>> class Animal(object):
... def __init__(self, name, type_id):
... self.name = name
... self.type_id = type_id
...
... self.type_attrs = ANIMAL_TYPE_ATTRIBUTES[self.type_id]
...
... def fly(self):
... type_name = self.type_attrs['name']
... if self.type_attrs['can_fly']:
... return "This is {}. I'm a {}. Taking off now.".format(self.name, type_name)
... elif self.type_attrs['domesticated']:
... return "This is {}. I'm a {}. My owner is nuts.".format(self.name, type_name)
... else:
... return "This is {}. I'm a {}. Unable to comply.".format(self.name, type_name)
...
... def walk(self):
... if self.type_attrs['leg_count'] > 0:
... return "Walking"
... else:
... return "No. I'm a {}.".format(self.type_attrs['name'])
...
... def swim(self):
... return "Swimming" if self.type_attrs['can_swim'] else "No. I'm a {}.".format(
... self.type_attrs['name'])
The changes have made our main class a little simpler, but have
made our constant declarations much more complicated. There is
lots of duplication and ugly noise created by using mappings with
string literals this way. The code in our main class has also got
uglier by having to index the mappings with string literals.
We can get rid of some of the duplication and ugliness by replacing
the mappings with namedtuples.
>>> from collections import namedtuple
>>>
>>>
>>> ANIMAL_TYPE_ID_DOG = 1
>>> ANIMAL_TYPE_ID_HAWK = 2
>>> ANIMAL_TYPE_ID_CAT = 3
>>> ANIMAL_TYPE_ID_SNAKE = 4
>>>
>>>
>>> AnimalTypeAttrs = namedtuple('AnimalTypeAttrs', (
>>> 'name', 'can_fly', 'domesticated', 'leg_count', 'can_swim'))
>>>
>>>
>>> ANIMAL_TYPE_ATTRIBUTES = {
... ANIMAL_TYPE_ID_DOG: AnimalTypeAttrs('Dog', False, True, 4, True)
... ANIMAL_TYPE_ID_HAWK: AnimalTypeAttrs('Hawk', True, False, 2, False),
... ANIMAL_TYPE_ID_CAT: AnimalTypeAttrs('Cat', False, True, 4, True),
... ANIMAL_TYPE_ID_SNAKE: AnimalTypeAttrs('Snake', False, False, 0, True),
... }
>>>
>>>
>>> class Animal(object):
... def __init__(self, name, type_id):
... self.name = name
... self.type_id = type_id
...
... self.type_attrs = ANIMAL_TYPE_ATTRIBUTES[self.type_id]
...
... def fly(self):
... type_name = self.type_attrs.name
... if self.type_attrs.can_fly:
... return "This is {}. I'm a {}. Taking off now.".format(self.name, type_name)
... elif self.type_attrs.domesticated:
... return "This is {}. I'm a {}. My owner is nuts.".format(self.name, type_name)
... else:
... return "This is {}. I'm a {}. Unable to comply.".format(self.name, type_name)
...
... def walk(self):
... if self.type_attrs.leg_count > 0:
... return "Walking"
... else:
... return "No. I'm a {}.".format(self.type_attrs.name)
...
... def swim(self):
... return "Swimming" if self.type_attrs.can_swim else "No. I'm a {}.".format(
... self.type_attrs.name)
This is better, but there are still issues. There is a lot of
duplication in the constant declarations. Look at all the repetition
of `ANIMAL_TYPE_ID`. The prefix-as-namespace convention is not ideal.
In addition, the relationship between the constant declarations and
the main class is a bit tenuous. It would be better if the constant
declarations were within the main class namespace.
It is also often necessary to build a list of two-item tuples
for use as choices with things like Django model fields. We could
use a comprehension to create such a list from our
`ANIMAL_TYPE_ATTRIBUTES`. However, since mappings are unordered,
we will want to change our mapping into an OrderedDict.
Lets attempt to address as many of these issues as we can.
>>> from collections import namedtuple, OrderedDict
>>>
>>>
>>> class Animal(object):
... class TYPE_ID:
... DOG = 1
... HAWK = 2
... CAT = 3
... SNAKE = 4
...
... TypeAttrs = namedtuple('AnimalTypeAttrs', (
... 'name', 'can_fly', 'domesticated', 'leg_count', 'can_swim'))
...
... TYPE_ATTRS = OrderedDict([
... (TYPE_ID.DOG, TypeAttrs('Dog', False, True, 4, True)),
... (TYPE_ID.HAWK, TypeAttrs('Hawk', True, False, 2, False)),
... (TYPE_ID.CAT, TypeAttrs('Cat', False, True, 4, True)),
... (TYPE_ID.SNAKE, TypeAttrs('Snake', False, False, 0, True)),
... ])
...
... TYPE_CHOICES = [(type_id, type_attrs.name) for type_id, type_attrs in TYPE_ATTRS.items()]
...
... def __init__(self, name, type_id):
... self.name = name
... self.type_id = type_id
...
... self.type_attrs = self.TYPE_ATTRS[self.type_id]
...
... def fly(self):
... type_name = self.type_attrs.name
... if self.type_attrs.can_fly:
... return "This is {}. I'm a {}. Taking off now.".format(self.name, type_name)
... elif self.type_attrs.domesticated:
... return "This is {}. I'm a {}. My owner is nuts.".format(self.name, type_name)
... else:
... return "This is {}. I'm a {}. Unable to comply.".format(self.name, type_name)
...
... def walk(self):
... if self.type_attrs.leg_count > 0:
... return "Walking"
... else:
... return "No. I'm a {}.".format(self.type_attrs.name)
...
... def swim(self):
... return "Swimming" if self.type_attrs.can_swim else "No. I'm a {}.".format(
... self.type_attrs.name)
*************
Static Models
*************
A static model is defined using a class definition to create
a sub-class of StaticModel.
Member field names are declared with the `_field_names` class
attribute. The value should be a sequence of strings.
Expand Down

0 comments on commit b6b51f7

Please sign in to comment.