design pattern docs
This commit is contained in:
parent
ee898302d3
commit
ea4d79dbe5
230
README.md
230
README.md
@ -13,11 +13,184 @@ Notably absent from this list: "make a useful tool" and "demonstrate the *right*
|
||||
This library demonstrates *ways* to do things, there is almost never a single correct way.
|
||||
Sometimes choices will be made to provide more interesting code to learn from, or a more fun API to use.
|
||||
|
||||
## Running
|
||||
|
||||
Check this repo out locally, then:
|
||||
|
||||
```
|
||||
poetry install
|
||||
poetry run python -m doodles.main
|
||||
```
|
||||
|
||||
Press left/right arrow to toggle through examples.
|
||||
|
||||
Feel free to modify the existing examples (`doodles/examples` or add your own).
|
||||
|
||||
## Architecture
|
||||
|
||||
TODO
|
||||
|
||||
## SOLID Principles
|
||||
|
||||
*Single Responsibility*
|
||||
|
||||
The most clear example of the single responsibility principle is that shapes do
|
||||
not draw themselves.
|
||||
This seems like a good idea, and indeed as I was drafting this they did, each shape
|
||||
has a `draw` method that could draw itself.
|
||||
|
||||
Instead these methods dispatch to the currently active DrawEngine which contains
|
||||
all drawing specific code.
|
||||
|
||||
This follows single responsibility, and gains a tangible benefit from it.
|
||||
|
||||
(See `lines.py draw` for a bit more discussion of this.)
|
||||
|
||||
*Open/Closed*
|
||||
|
||||
This principle asks that behaviors are extensible by inheritance,
|
||||
not requiring modification to the base classes behavior.
|
||||
|
||||
The `Doodle.random()` method demonstrates this, the base random method
|
||||
randomizes the properties common to all doodles (position, color).
|
||||
|
||||
Derived classes override this (see `Line.random()`, `Circle.random()`, etc.) by calling the base
|
||||
implementation, and then adding their own random elements.
|
||||
|
||||
(See `lines.py random` for a bit more discussion of this.)
|
||||
|
||||
*Liskov Substitution*
|
||||
|
||||
Anywhere that the underlying code expects a `Drawable` it should be possible
|
||||
to substitute any child class `Line`, `Rectangle`, etc.
|
||||
|
||||
This is ensured by having all of them define a common `draw` interface, and having
|
||||
their constructors not take dozens of additional parameters. (See `Line.__init__`.)
|
||||
|
||||
*Interface Segregation*
|
||||
|
||||
This is largely given by counter-example, the principle here states that we must not force child classes to implement methods they do not use.
|
||||
|
||||
A counter-example to this would be if the pre-render, then draw dance that complicates the `Text` interface was extended to all types.
|
||||
|
||||
One could imagine having placed this logic at the `Doodle` class instead of `Line`.
|
||||
That would mean that `Circle`, `Rectangle`, etc. would need to implement a (likely empty) method to fulfill this.
|
||||
|
||||
*Dependency Inversion*
|
||||
|
||||
Entities should depend on abstractions, not concrete implementations.
|
||||
|
||||
`World` depends on the `Doodle` interface (most explicitly through `draw`), but not on `Circle`, `Font`, etc.
|
||||
|
||||
This ensures adding new drawable `Doodles` only requires deriving a class, not modifying `World`.
|
||||
|
||||
## Design Patterns
|
||||
|
||||
A number of design patterns are utilized in this library.
|
||||
|
||||
I'm open to suggestions for more to add, especially where an interesting feature can demonstrate the utility.
|
||||
|
||||
### Prototype
|
||||
|
||||
<https://refactoring.guru/design-patterns/prototype>
|
||||
|
||||
The `Doodle` class (and override in `Group`) demonstrate the utility of the Prototype pattern.
|
||||
|
||||
Letting these objects specify how they are copied makes the desired behavior possible, the utility of which can be seen in `examples/copies.py`.
|
||||
|
||||
This would often be done via the `__copy__/__deepcopy__` methods, but for simplicity as well as API compatibility this is done in `.copy()` here.
|
||||
|
||||
### Singleton
|
||||
|
||||
<https://refactoring.guru/design-patterns/singleton>
|
||||
|
||||
The `World` class is treated as a Singleton and contains notes on alternate implementations.
|
||||
|
||||
### Strategy & Bridge
|
||||
|
||||
<https://refactoring.guru/design-patterns/strategy>
|
||||
|
||||
The interface created in `draw_engine.py` as well as implementation in `world.py` show the strategy pattern in action.
|
||||
|
||||
While this pattern can sometimes be achieved in a functional way by passing a function around, here the drawing strategy is more complex, and so gets aggregated into a class that allows swapping the entire strategy.
|
||||
|
||||
An alternate implementation of this class could draw to a PDF or web browser instead of Pygame window.
|
||||
|
||||
<https://refactoring.guru/design-patterns/bridge>
|
||||
|
||||
At the moment, the `World` and `PygameDrawEngine` are very tightly coupled,
|
||||
this make sense because they both require `pygame`.
|
||||
|
||||
This creates a bridge pattern between the two, `World` handles update logic/the generic
|
||||
form of drawing doodles, but the actual drawing is implemented in the draw engine.
|
||||
|
||||
### Composite
|
||||
|
||||
<https://refactoring.guru/design-patterns/composite>
|
||||
|
||||
This pattern allows objects to form a tree, usually by tracking child/parent relationships.
|
||||
|
||||
The `Group` class works closely with `Doodle` (both in `doodles.py`) to form a composite.
|
||||
|
||||
Groups of doodles can be moved/colored/etc. together by having the `Group` delegate method calls to the child elements.
|
||||
|
||||
Since `Group` is-a `Doodle`, groups of groups of groups can be created, allowing trees of any shape or size.
|
||||
|
||||
### Template Method
|
||||
|
||||
<https://refactoring.guru/design-patterns/template-method>
|
||||
|
||||
The core behavior of an object is implemented in the `Doodle` class which
|
||||
has methods for positioning, coloring, etc.
|
||||
|
||||
The `update()` and `draw()` methods are templates, that if overriden, can
|
||||
further refine the behavior
|
||||
|
||||
See `balls.py` for an example of the `update` method being used to demonstrate
|
||||
the template method pattern.
|
||||
|
||||
### Command
|
||||
|
||||
<https://refactoring.guru/design-patterns/command>
|
||||
|
||||
The command pattern turns an intention into a packet of information about the
|
||||
intended operation, so execution can be deferred.
|
||||
|
||||
Note that in all of the examples, the drawing does not happen when the object
|
||||
is created, the creation of an object indicates that there will be some future
|
||||
operation performed, based on the properties of the command class.
|
||||
|
||||
This allows our chained interface to work since
|
||||
|
||||
`Circle().move(20, 20).color(255, 0, 0)` does not immediately draw a circle when `move` is called, instead it is informing future draws to include that position. This avoids a non-red circle from being drawn, since the subsequent call to `color(255, 0, 0)` to
|
||||
also run before the command is executed (via the `draw` method).
|
||||
|
||||
### Flyweight Cache
|
||||
|
||||
<https://refactoring.guru/design-patterns/flyweight>
|
||||
|
||||
Fonts need to be loaded once and are loaded in to shared memory
|
||||
(currently in `PygameDrawEngine`) instead of having each `Text` object
|
||||
load the same font.
|
||||
|
||||
This is done since it is common to use the same font multiple times in an
|
||||
application, so having each text object load its own instance of a font would
|
||||
be immensely wasteful.
|
||||
|
||||
This is the most complex part of the code currently, but you can see the logic in the font methods on the DrawEngine and you'll see it is mostly a dictionary mapping
|
||||
parameters to created objects.
|
||||
|
||||
### Others
|
||||
|
||||
Some other patterns under consideration would be Observer, Factor, and Visitor.
|
||||
|
||||
All of these might have interesting applications in making art this way. (PRs welcome :))
|
||||
|
||||
## Rationales
|
||||
|
||||
I will document the reason that certain decisions were made, particularly when they were not my first thought.
|
||||
A few more rationales for reason that certain decisions were made, particularly when they were not my first thought.
|
||||
|
||||
If any decisions are unclear, I consider that a bug, please file a corresponding GitHub issue.
|
||||
If any decisions elsewhere in the code are unclear and undocumented, I consider that a bug, please file a GitHub issue.
|
||||
|
||||
### Mutability
|
||||
|
||||
@ -39,56 +212,3 @@ This complicates things a bit, since you can't write `self.x(100)` and use `self
|
||||
The unorthodox decision was made to use `x(100)` as a setter function, and use `.x_val` as the property name.
|
||||
|
||||
This is primarily because setting properties is more common than getting properties in doodles.
|
||||
|
||||
## Design Patterns
|
||||
|
||||
TODO: flesh this out with more notes
|
||||
|
||||
A number of design patterns are utilized in this library.
|
||||
I'm open to suggestions for more to add, especially where an interesting feature can demonstrate the utility.
|
||||
|
||||
### Factory
|
||||
|
||||
TODO: No factory yet surprisingly, should be easy to add one to add shapes, but can consider other options.
|
||||
|
||||
### Prototype
|
||||
|
||||
The `Doodle` class (and an override in `Group`) demonstrate the utility of the Prototype pattern.
|
||||
|
||||
Letting these objects specify how they are copied makes the desired behavior possible, the utility of which can be seen in `examples/copies.py`.
|
||||
|
||||
This would often be done via the `__copy__/__deepcopy__` methods, but for simplicity as well as API compatibility this is done in `.copy()` here.
|
||||
|
||||
### Singleton
|
||||
|
||||
The `World` class is treated as a Singleton and contains notes on alternate implementations.
|
||||
|
||||
### Bridge / Strategy
|
||||
|
||||
TODO: the planned Renderer class will demonstrate these
|
||||
|
||||
### Composite
|
||||
|
||||
The Group class (along with Doodle) forms a composite.
|
||||
|
||||
### Command
|
||||
|
||||
The structure of the Doodle object is a command class.
|
||||
It stores the information about the action to be performed encapsulated.
|
||||
It's entire purpose is to provide arguments to a draw* method.
|
||||
|
||||
### Observer
|
||||
|
||||
TODO: room to implement, dynamic properties?
|
||||
|
||||
### Template Method
|
||||
|
||||
The update method, the draw method.
|
||||
|
||||
### Visitor
|
||||
|
||||
TODO: maybe use along with group?
|
||||
|
||||
### Flyweight Cache
|
||||
|
||||
Text's Font Cache is a flyweight
|
||||
|
@ -25,8 +25,6 @@ class DrawEngine(abc.ABC):
|
||||
while writing in the first place, and ensure that code needing
|
||||
isolation (such as a library you want to avoid tight coupling to)
|
||||
only is added to a specific class or module.
|
||||
|
||||
TODO: this got behind the current impl
|
||||
"""
|
||||
@abc.abstractmethod
|
||||
def init(self):
|
||||
|
@ -21,6 +21,10 @@ class Ball(Circle):
|
||||
self.speed = 9 + random.random() * 5
|
||||
|
||||
def update(self):
|
||||
"""
|
||||
This update method loops the balls around the screen
|
||||
when they fall off the bottom. (A raindrop like effect.)
|
||||
"""
|
||||
self.move(0, self.speed)
|
||||
if self.world_y > world.HEIGHT + 20:
|
||||
self.move(0, -world.HEIGHT - 20)
|
||||
@ -33,6 +37,9 @@ class GravityBall(Circle):
|
||||
self.speed = random.random() * 10
|
||||
|
||||
def update(self):
|
||||
"""
|
||||
This update method simulates acceleration and a bounce.
|
||||
"""
|
||||
self.speed += self.accel
|
||||
self.move(0, self.speed)
|
||||
if self.world_y > world.HEIGHT - 10:
|
||||
|
Loading…
Reference in New Issue
Block a user