简体   繁体   中英

How to structure classes such that I can easily inherit from a “Persistor” class which pickles the inherting object?

I'm writing a new library and I want to persist some objects. I want to use a mixin or some sort of adapter so I don't have to implement a database right away. I'm using pickle right now to store objects.

Let's say I have a User class. I want to load a User from the folder if the pickle exists. I wrote a Persistor class which takes an object and writes it to a specified location. Do I make the User class inherit from the Persistor class? If so, when the User class is instantiated, how then do I replace the object with the loaded object if a pickle eists? Or do I create a UserPersistor class? I'm simply looking to abstract away the loading and saving of state from the User class.

class User(Persistor???):
    """
    Central class to hold all user attributes and relationships.
    """

    def __init__(
        self,
        first_name: str,
        username: str,
        date_of_birth: datetime.date
    ):
        self.first_name = first_name
        self.username = username
        self.date_of_birth = date_of_birth
import pickle
import os


class Persistor:
    """
    Class whose job is to save and load state until we need a database.
    """

    def __init__(
        self,
        persistence_key
    ):
        self.persistence_key = persistence_key
        self.persistence_path = "data/" + persistence_key

    @property
    def save_exists(self):
        return os.path.exists(self.persistence_path)

    def save(self):
        outfile = open(self.persistence_path, 'wb')
        pickle.dump(self, outfile)
        outfile.close()

    def load(self):
        if self.save_exists:
            infile = open(self.persistence_path, 'rb')
            db = pickle.load(infile)
            infile.close()
            return db

    def delete(self):
        if self.save_exists:
            os.remove(self.persistence_path)

Simple answer (and it's nothing Python-specific FWIW, this is just plain OO design): semantically, inheritance denotes a "is a" relationship - so if B inherits from A, then B is a A too (cf the Liskov substitution principle too). So the real question is: would you consider that a User is a Persistor ?

Another common, sensible and widely accepted design principle is the single responsability principle , which states that each component should have a single, well-defined responsability. So ask yourself if you really want persistance implementation details to become part of your User class responsabilities...

As for a last couple OO design guidelines, which are repeated all over the (long) introduction of the GoF's "Design patterns" book (actually one of the best texts on OO design FWIW - the introduction I mean, even if the patterns catalog is of interest too): you should favor composition/delegation over inheritance (inheritance being, technically speakin, a restricted form of composition/delegation), and you should always program to an interface ("interface" in the Java term - in most languages this means an abstract base class), never to an implementation (a concrete class). By combining both, you get well decoupled code that's easy to test, maintain and evolve. For example, if you make your User class inherit from Persistor (which is a concrete class), you cannot easily switch to other Persistor implementation, while if you use composition/delegation and an abstract base class defining the only the Persistor API, you can (theoretically...) use any Persistor implementation your User class.

Now that's for the theory, and reality is that some abstractions tend to be kind of leaky - for example, to be persisted using the pickle protocol, your User class has to be pickleable, so this is not totally transparent, so take the above principles with a grain of salt - more as food for thought than golden rules, that is. As far as I'm concerned I would probably use composition/delegation as the safest choice here, and eventually reconsider the whole design during implementation progress, as implementation often hilights design flaws (if you can spot them at least).

EDIT

From what I recall, you don't need to use interfaces in Python due to multiple inheritance. Does that change the "is a" rule just for Python?

Actually, inheritance serves two purposes: subtyping (as explained above), but also code reuse - which is why the GoF describes inheritance as "a restricted form of composition/delegation" (in that the child class has a reference to it's parent(s) and automagically delegates parts of it's operations).

Subtyping is a very important part of OO, since it forms the basis for generic code and polymorphic dispatch - so a given piece of code can work just the same with a collection of objects that are from different concrete type (implementation), as long as they implement the same interface.

Semantically , an "interface" is a defined set of (public) features (methods, attributes, properties etc). Now how an interface is technically defined depends on the language.

Statically typed languages need a formal definition of the interfaces a class implements so it can perform compile-time checks and eventually optimizations. Note that I wrote "interfaceS" - a class can implement more than one single interface, and that's actually a rather common case.

For (static) languages that support multiple inheritance, the obvious solution is to use abstract base classes for interface definitions. Java doesn't support multiple concrete inheritance (that's a design choice) so it has a distinct construct named "interfaces" - but it's really another name for pure abstract base classes ("pure": only definitions, no implementation at all)

In dynamic languages, there's actually no (technical) need for formal interface definitions (since there's no formal typechecking), so "interfaces" are quite often informal - you'll often find in dynamic languages docs terms like "list-like", "file-like" etc (to denote objects that behaves like lists or files).

Now while this works well in practice (most of the times at least), there are still good things to be said about formal interface definitions, if only for documentation (and also for the cases where simple polymorphic dispatch is not enough and you have to inspect your objects to find out what you should do with them - think recursing over a structure of arbitrarly nested dicts and lists of various objects) - so Python also have abstract base classes , and they are actually recommended for any non-trivial project.

In all cases, the "is a" relationship still holds to a certain degree (even if the child class is not necessarily a proper subtype according to Liskov's definition) since a subclass does inherit it's parent(s) class(es) implied interface(s).

NB : Python also have "protocols", which are more formally defined sets of special methods a class can implement and that will give instances of this class some special behaviours (cf descriptors or iterators ).

Can I have my User inherit from object and Persistor and that would be python's way of saying that it gets that functionality?

Well of course yes you can - there's no technical restriction on this, it's just a design question: should your User class also BE a Persistor, or would it be "better" (depending on the definition of "better") to have two clearly distinct classes - one for the domain model (User), and a technical one for persistance (UserPersistor for example).

If I delegate the persistence logic to the Persistor class, I still need to call that from my User class code

Not necessarily... If you decide that persistance is not the User's responsability, then you can just see things the other way round: it's the Persistor that calls on your User object -and in this case, you actually don't even need composition/delegation at all.

If you got yourself the GoF, I strongly suggest you take some time reading it before you go on with your design decisions, paying attention to the first part. Just a warning though : once you're done, avoid the tentation to use each and all of the patterns in your project, always ask yourself if it makes sense for the concrete problem at hand and whether the extra complication (if any) will ever be of any use for this particular part of your code ;-)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM