Python OOP advice

S

Simon Hibbs

I'm rewriting a design application for a science fiction game. in it
you design your own starships. Each component has a mass and cost, but
the mass can either be fixed or it can be expressed as a percentage of
the tonnage of the overall ship.

Orriginaly I thought I'd need to have a hull object which contains
component objects, but the component objects need access to members of
the hull object (e.g. the hull size) so that looks messy to implement.

I have defined a base class Component with a class member variable
'hull_size' so that all components can see the hull size. I've then
got two child classes called Fixed_Component and Percent _Component
that implement their mass, mass_percent and cost properties
appropriately for their type. I've also defined a Hull class which
also inherits from Component and provides methods for access to the
hull_size class variable.

I'm not sure where to go from here. One possibility is to have a Ship
object containing a list of components, but I'd need to have a way to
ensure there's only ever one hull object so maybe that shouldn't go in
the list?

I think the fact that the hull_size is a class member also means I
can't ever have an application that loads two designs at the same
time, because they'd share the same hull_size class variable. Is that
so, and is there a way round that? I suspect the inheritance model
will work fine at first, but is too rigid in the long run.

Is there a way to cleanly implement a parent-child relationship
between objects that gives child objects limited access to members of
the parent?

Simon Hibbs
 
P

Paul McGuire

I'm rewriting a design application for a science fiction game. in it
you design your own starships. Each component has a mass and cost, but
the mass can either be fixed or it can be expressed as a percentage of
the tonnage of the overall ship.

Orriginaly I thought I'd need to have a hull object which contains
component objects, but the component objects need access to members of
the hull object (e.g. the hull size) so that looks messy to implement.

I would not put this kind of intelligence into the components.

I think the issue here is that your Ship container is not really just
a generic container of ship components, but an assembly with some
specific requirements (must have 1 and only 1 hull, must have 1 or
more engines, etc.) and structure. I would create a class called
ShipDesign that had specific members for those components that have
special logic attached to them, and then more generic list members for
collection-ish components.

Since the hull is such a significant constraint, I would make it an
initialization argument. I would also put some kind of property on
hull representing its "capacity" (oh I see, you have something call
hull_size).

One way to generalize the fixed-cost vs. percentage-cost components
would be to have all components implement a compute_load function that
takes the ShipDesign as an argument. Those that are fixed-cost simply
return their fixed value, those that are percentage-cost can return
their percentage of the ShipDesign's hull.hull_size - this leaves the
door open for other variations on load, that could be based on other
properties besides the hull size.

Here's how I envision your ShipDesign class:

class ShipDesign(object):
def __init__(self, hull):
self.hull = hull
self.engines = []
self.shields = []
self.weapons = []
self.other = []

def compute_consumed_capacity(self):
load = 0
for itemlist in (self.engines, self.shields,
self.weapons, self.other)):
load += sum(item.compute_load(self)
for item in itemlist)
return load

def add_engine(self,e):
engload = e.compute_load(self)
if engload + self.compute_consumed_capacity() >
self.hull.hull_size:
raise ExceededHullCapacityException()
if len(self.engines) == MAXIMUM_ALLOWED_ENGINES:
raise ExceededMaximumConfigurationLimitException()
self.engines.append(e)

def set_hull(self, hull):
if self.compute_consumed_capacity() <= hull.hull_size:
self.hull = hull
else:
raise NewHullTooSmallException()

def is_valid(self):
if not self.engines:
raise InvalidDesignException("must have at least 1
engine")
...etc...

class GenericItem(object):
def __init__(self, name, load):
self.name = name
self.load = load
crewQuarters = GenericItem("Crew Quarters", 50)
disco = GenericItem("Discotheque", 10)
....etc...

Once you have a valid ShipDesign, you can then use it to construct
multiple Ship instances.

Here is how I would work around your "only one hull at a time"
problem. Define several classes for different kinds of hulls:

class CheapHull(Hull):
capacity = 100
name = "EconoHull 1000"
class MediumHull(Hull):
capacity = 500
name = "Mainliner X50"
class TopOTheLineHull(Hull):
capacity = 1000
name = "LuxeMaster 5000"

and then create ship designs with a CheapHull, a MediumHull, or a
TopOTheLineHull. In this case, the member variable of the ShipDesign
is really a class, which you would later use to construct hull
instances as part of making Ship instances from your ShipDesign.

class Ship(object):
def __init__(self, design):
self.hull = design.hull()
self.engines = design.engines[:]
...etc...

This way, each Ship will have its own Hull instance, so that you
can track instance-specific properties, such as damage percentage.

If you don't want to hard-code the hull types, then you can do
something similar with instances of a generic Hull class, which you
would then use as prototypes when constructing Ship instances. Just
be careful that you don't accidentally have all ships sharing the same
hull instance!

-- Paul
 
S

Simon Hibbs

Great contributions, thanks both of you. I'm self-tought when it comes
to Python and OOP and I haven't yet grown an intuitive feel for how to
do things sensibly.

Simon
 
B

Bruno Desthuilliers

Ben Finney a écrit :
Was it as messy as this::

class ShipHull(object):
def __init__(self, size):
self.components = dict()
self.size = size

class ShipComponent(object):
def __init__(self, name, hull):
self.name = name
self.hull = hull


It seems to me the hull is an attribute of the component, and the size
is an attribute of the hull. Why would the hull size be a *class*
attribute?


class FixedShipComponent(ShipComponent):
def _get_mass(self):
return calculation_foo(self.hull.size)
mass = property(_get_mass)

def _get_cost(self):
return calculation_bar(self.hull.size)
cost = property(_get_cost)

class PercentShipComponent(ShipComponent):
def _get_mass(self):
return calculation_spam(self.hull.size)
mass = property(_get_mass)

def _get_cost(self):
return calculation_eggs(self.hull.size)
cost = property(_get_cost)

Or use the strategy pattern (dummy example, don't have time to read your
specs !-):

class FixedMassCostStrategy(object):
def get_mass(self, hull):
return calculation_foo(hull.size)
def get_cost(self):
return calculation_bar(hull.size)

class PercentMassCostStrategy(object):
def get_mass(self, hull):
return calculation_spam(hull.size)
def get_cost(self):
return calculation_eggs(hull.size)


class ShipComponent(object):
def __init__(self, name, hull, masscost_strategy):
self.name = name
self._hull = hull # implementation detail
self._strategy = masscost_strategy

mass = property(lambda self: self._strategy.get_mass(self._hull))
cost = property(lambda self: self._strategy.get_cost(self._hull))
 
B

Bruno Desthuilliers

Simon Hibbs a écrit :
Great contributions, thanks both of you. I'm self-tought when it comes
to Python and OOP and I haven't yet grown an intuitive feel for how to
do things sensibly.

While part of the OO design patterns are mostly workaround for lack of
dynamism in languages like C++ or Java, and while pattern abuse is
certainly not good design, you'd probably learn a lot from the GOF's
Design Patterns book.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,743
Messages
2,569,478
Members
44,898
Latest member
BlairH7607

Latest Threads

Top