Barns, animals, feeding: Software class design

Started by Dullstar, February 18, 2020, 02:23:53 AM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

Dullstar

We seem to have several reasonably experienced programmers on these forums, so I thought I might put this question here.

I'm still trying to learn how classes work and when to use them. From what I've read about them, they sound like they'd help me with organization on larger projects. I might just be having some sort of misconception about how to use classes, but I've come up with the following illustrative example (the actual project has nothing to do with barnyard animals, but I think it's a simple way to illustrate why I want to organize things this way that I suspect the language doesn't want me organizing them):

Suppose we have barns filled with animals and food. In this example, the food supplies in each barn are shared between animals in the same barn, but not between individual animals. Each animal has a daily routine that involves eating if there is enough food in the barn (else, the animal starves). So I'd like to organize things like this:

Note that this isn't intended to be partial code in any language. It's just pseudocode.
class Barn:
    contains animals and food

    method have_animals_do_stuff:
        for each animal in animals:


My first instinct would be to try nesting classes, which the interpreter does seem to allow, but it seems like this doesn't actually accomplish anything since from what I could find it sounds like Python doesn't let inner classes access outer class variables.

Simon

#1
Visibility of objects. This is the heart of the design problem: An animal in the barn shall access the food in the same barn, but the animal shall not see the food in other animals or anywhere else.

There are several ways to have an animal access the food:
  • The animal knows permanently about its barn. (An animal has a field that is a reference to its barn, and we set this field, e.g., when the animal is constructed.) The barn has a method that provides food. The animal can then get food from its barn.
  • The animal knows about the barn only at feeding time: When the barn calls the animal, the barn gives itself as an argument, so that the animal can get food from there.
  • At feeding time, the barn asks the animal how much food it wants. The barn then deducts this much food from itself and passes that food to the animal.
  • The barn contains not only the animals, but also a food source, an object of a new class to design. At feeding time, the barn points each animal to the food source, and lets the animal interact with the food source. The animals need not now about barns, but only about food sources.
None of these is best in all cases. It's up to taste, and maybe we will reorganize this in a couple months when the system expands.

Asking the animal for the exact amount of food, then giving it that much food, violates Tell, don't ask: Hunger is a decision of the animal, not of the barn, therefore we should put the code in the animal, not into the barn. The barn shall tell the animal to get food, not ask the animal about food.

Decoupling the food source from the barn is the most extra code to write, but is good once we herd animals outside a barn. The developer of the system can tell best how much decoupling/generalization is appropriate. :lix-grin:

It also depends on how much other work the barn is doing. If the barn has tractors, hay, ..., that's a reason to delegate the animal-keeping/feeding to another part of the program: separation of concerns. If the barn became empty after we removed its keeping/feeding, concerns were probably separated well in the original design already.




Modularity. (Inner classes, moving types to different files.) This example is small enough such that every type may still see every other type, and everything can go in the same source file.

Nonetheless, I'll go into detail here to show why inner classes will not help solve our problem.

In Python, classes are Python objects because everything is a Python object. A class Y can be a member of another object x of class X. This doesn't gain much from the classic OO viewpoint: An object y instantiated from that inner class Y has no special relationship anymore with x. Any association must be set up as normal, e.g., when y is constructed, x might pass itself as a parameter to Y's constructor.

In Java, classes can be inner classes and access the outer classes' private members. I don't believe they are commonly used. If some classes only make sense within a part of the Java project, it's natural to express this via the package system (code in same file, or at least in a file in same directory).

In many languages, you would move classes, types, functions, ..., that loosely belong to each other into different files (modules) in the same directory (package), and import modules to use the types. Whether a single file becomes too big is again a matter of taste and convention.




Data hiding, invariants. This is typically the first mantra that one learns about OO, that one shall hide the raw data. It's merely not the heart of our design problem here; in a way, the objects themselves were hidden too well already, and we're trying to find the best associations between the objects.

For encapsulation, a class hides its data and instead exposes methods that enforce invariants. E.g., the barn or the food source tracks an amount of food, and allows the animals access to the food via methods. These methods enforce that animals cannot take more food than the barn has, and that animals cannot add food. The animals have no direct access to any integer that tracks food, they cannot set it negative.

The benefit of this is that if we ever have a bug with negative food, the encapsulation tells us that the bug must be within the barn.

-- Simon

Simon

Restored this thread from Recycling in accordance with Dullstar. Enjoy!

-- Simon