4

I am getting confused when objects interact using OOP. Here i'm trying to model the interaction between a vending machine and a coin. The coin is inserted into the vending machine. The interaction is represented by the functions InsertCoin() and Insert().

The resulting main() is very confusing, how are we to know which function to call first?

class Coin;

class VendingMachine
{
public:
    /*
     * Inserts a coin into the the vending machine.
     */
    void InsertCoin(Coin* coin)
    {
        number_of_coins++;
    }
    
private:
    int number_of_coins;
};

class Coin
{
public:
    /*
     * Inserts the coin into the vending machine.
     */
    void Insert(VendingMachine* vending_machine)
    {
        vending_machine->InsertCoin(this);
        
        this->in_vending_machine = true;
    }
    
private:
    bool in_vending_machine;
};

int main()
{
    VendingMachine* vending_machine;
    Coin* coin;
    
    vending_machine = new VendingMachine();
    coin = new Coin();
    
    coin->Insert(vending_machine);               // which function goes first? this one?
    vending_machine->InsertCoin(coin);           // or this one?
    
    delete vending_machine;
    delete coin;
    
    return 0;
}
8
  • 6
    In your case the method called on the first line also invokes the method called on the second, so you're double-counting.
    – jonrsharpe
    Commented Jun 2, 2021 at 10:46
  • 8
    Are you writing software for a real-world vending machine, or you simulating a vending machine in software?
    – JacquesB
    Commented Jun 2, 2021 at 14:10
  • 4
    The main reason why your main function is confusing is because well over 50% of it is unnecessary clutter. Clutter is the enemy of course readability. Learn to avoid it. Commented Jun 2, 2021 at 18:32
  • 4
    "The interaction is represented by the functions InsertCoin() and Insert()" - nothing in OOP says you have to do it that way; how you represent the interaction can vary for the same problem (two devs may come up with a different structure), and it depends on how you think about the problem and on the needs of your application. E.g. if you're thinking of the Vending Machine as a primary object, you could just have vending_machine->AcceptCoin(coin) (and no Insert method on coin; still, the machine could interrogate the coin for its value). 1/1 Commented Jun 2, 2021 at 19:22
  • 1
    In disregard to the actual question: Please read up on basics of current C++. Avoid using new and delete. Manual memory management is not necessary here. It seems you come from a C# background. The languages are very different. Skipping those basics to implement OOP in C++ might make it more difficult to understand OOP in the long term.
    – sbecker
    Commented Jun 4, 2021 at 10:20

7 Answers 7

16

I'm not going to criticize your design because I see that it's not the point. Indeed coins shouldn't know about vending machines, and the related arguments, are valid. Let's assume that these classes are just dummies like Foo and Bar.

The crux of your question seems to be in these code comments:

coin->Insert(vending_machine);               // which function goes first? this one?
vending_machine->InsertCoin(coin);           // or this one?

At the highest level of abstraction, there should only be one function call here. There is a single operation. The verb is insert, and the nouns are vending machine and coin.

There are two main schools of thought in the world of OOP about this: one which is mainstream (C++'s object system being in this category), and one that is a little off the beaten path.

Mainstream OOP is primarily about creating service abstractions. Specifically, in such a way that the service abstractions can be implemented in multiple different ways, but used the same way. Under this view, in a given object collaboration scenario, we identify which object or objects are the service providers. In this situation, that is probably the vending machine. So what we want is just this:

vending_machine->InsertCoin(coin);

The user of these two objects should ideally not be required to initiate any more steps in order to get the vending machine and the coin to interact.

Now, it may be a fact in the implementation that the coin needs to know that it has been inserted into the vending machine. That aspect has been criticized in some of the other answers and comments. Those criticisms are valid when they are literally about vending machines and coins. In general, there are situations where one object is added to another (that one being the main service abstraction) and yet has to know.

Here is where the non-mainstream OOP makes a valuable contribution. There are object-oriented systems that have multiple dispatch. Instead of methods, there are generic functions. Generic functions are specialized into numerous methods which are dispatched on the run-time type of all of the arguments. Under multiple dispatch OOP, we have this:

insert(machine, coin);

insert is a generic function. The generic function looks at the type of the machine object and the coin object, and then selects a method which best matches the combination. That method then contains logic that is specific to both the type of the machine and the type of coin; it does whatever is necessary without the concern of "which class has this responsibility".

Back under the mainstream OOP, when the responsibility cannot be entirely pinned to one class, we introduce some object collaboration protocols to make up for this.

The collaboration for the insert operation might start with:

vending_machine->InsertCoin(coin);

We make coin itself contain a little service. Anything receiving the coin must follow the protocol of announcing itself to the coin. Thus the vending machine's InsertCoin method does this:

void VendingMachine::InsertCoin(CoinBase *coin)
{
    // notify coin of insertion
    coin->Inserted(this); 

    // ..
}

This is almost like a multiple dispatch. First we dispatch the method InsertCoin based on the run-time type of the vending machine. Then, a second dispatch takes place to complete the operation by doing the coin-side handling: a specific method is chosen based on the type of coin.

Side note: thanks to C++ overloading, the base class CoinBase can have multiple overloads for different kinds of vending machines, which are (statically) resolved by the this argument in the Inserted call.

One of the most useful documentation/design methods for this kind of programming is the sequence diagram:

main                   machine               coin

 |---- Insert(coin) --> |
                        |
                        |---Inserted(machine)->|
                                               |

If you're confused, try drawing sequence diagrams.

These diagrams capture very specific scenarios, often related to use cases; no single sequence diagram gives you the complete picture.

For instance, we would have a dedicated sequence diagram for some use case of inserting a coin into the machine, and successfully obtaining a product, as well as change.

2
  • Brilliant! Thank you!
    – user362602
    Commented Jun 3, 2021 at 18:27
  • That container and contained mutually need to know about each other is common in GUI object trees: The window must be able to update its children, but those must on occasion report back to the window. Commented Jun 3, 2021 at 19:37
35

In your design, coins need to know about vending machines. This unnecessary coupling seems to be a wrong start:

  • The vending machine’s responsibility is to provide a product if the price is paid. The latter is achieved by inserting coins. So the vending machine needs to know about coins and inserting coins to achieve its purpose. Moreover it probably needs to manage a state depending on the coins inserted.

  • But whatever coin’s responsibility is in your scenario (e.g representing a financial value? representing a 3d physical object for a physical simulation?) it is probably not to insert itself in the machine.

    The coin does not even need to know who owns it. Does it change something for the coin if it is in somebody’s purse or within the stomach of a vending machine? It still has the same shape, and still has the same value.

Such a proper separation of concerns is one of the first thing you need to think of in your design. In your case, the interaction would probably involve an object that “owns” the coin, that invokes the inserting feature of the vending machine, providing it a coin as parameter. That’s all.

12
  • 8
    In essence, attach your verbs to the object doing the doing, not the object being done to?
    – benxyzzy
    Commented Jun 2, 2021 at 19:21
  • 2
    Keenly(!) observe the notion of Separation of Concerns. "A coin" shouldn't care if it's being inserted into a vending machine or a tollbooth or a fountain. And, "a vending machine" just wants to be able to accept payment, and to know how much payment it has received in order to know whether to dispense the product. "That's it." Commented Jun 3, 2021 at 2:16
  • Should it be possible to use one coin in an arbitrary number of machines to buy an arbitrary number of products? Any coin which can be passed around directly by reference must know when the value represented thereby is transferred from one owner to another. There are a variety of ways this could be handled, but a coin might e.g. have a "transfer ownership" function which accepts a reference to its new owner, passes that reference along with a reference to itself to a "verify ownership release" function of its previous owner, and calls the "verify ownership acquisition" function of...
    – supercat
    Commented Jun 3, 2021 at 21:13
  • 1
    @supercat The coin still doesn't need to know who owns it. You probably have a Purse object holding the coin as long as the person has it. And you probably want to have a transfer(from, to) method that transfers coins from one collection to another, so that you can transfer the coin from one inventory to another. I simply don't see why I would ever want to model this whole thing in such a way that the coin needs to know who owns it. The coin just needs to "know" what currency it is and what denomination. I wouldn't ever model a data object such as a coin in any other way.
    – Polygnome
    Commented Jun 4, 2021 at 10:26
  • 1
    @supercat "Allowing" to transfer is something that the owner of the coin does/has to decide, not the coin itself. Its not the responsibility of the coin itself.
    – Polygnome
    Commented Jun 4, 2021 at 10:27
13

The code is being too literal in attempting to model the real world. Object oriented programming does not need to model the real world. Instead, it should model the business process of purchasing something from a vending machine.

Instead of a Coin class, consider using an integral type representing the number of cents (if using USD) or another small unit of currency. Within the realm of vending machines, a 32-bit integer giving you a max price of around 4 billion units of currency should be more than sufficient, unless your target audience is ultra rich people purchasing something extravagant from the machine (hey, it could happen).

You do not need to model "inserting money" either. Just model the actual transaction. A transaction with a vending machine requires little more than an amount of money and a choice:

vending_machine->purchase(75, "D1")

I'm not sure you even need to model the product that was dispensed unless the calling code can make use of it. Internally, the vending_machine would decrement the item number if the currency amount was sufficient or throw an exception if the current amount was not sufficient. Or the purchase method could return an integer amount of the difference:

difference = vending_machine->purchase(75, "D1")

The difference would be zero for exact amount, positive if change is required to be dispensed, or negative if the user still requires more money to complete the purchase.

7
  • 2
    This answer may be perfectly right. However, to be fair, the question does contain hardly enough details to know if a Coin class makes sense or not within the context of the program the OP is going to write. Following your line of argumentation, it may be enough to have a standalone integer value "number_of_inserted_coins", and not even a vending_machine object.
    – Doc Brown
    Commented Jun 2, 2021 at 13:52
  • 2
    @DocBrown: why would a vending machine object not be needed? Something needs to enforce the rules for paying and dispensing products. I guess the main point of my answer is to not focus on the "things" in the outside world, but the business process you are modeling. Commented Jun 2, 2021 at 16:03
  • 3
    We don't see any "rules for paying and dispensing products" in the code of this question - and you might say "yes, the OP left them out for the sake of this example" - but the same may be true for a coin - maybe there are more methods in the coin class the OP left out for the same reason, so replacing it by an integral type is not an options. I stress this to make it more apparent how many assumptions are made in this answer. Of course, as I wrote, these assumptions may be perfectly right.
    – Doc Brown
    Commented Jun 2, 2021 at 19:48
  • @DocBrown: completely agreed. I made many assumptions in my answer. Commented Jun 2, 2021 at 20:42
  • 1
    This answer may be right if you're just modeling transactions. But what if you're actually trying to show the graphics of someone inserting coins into the machine?
    – Barmar
    Commented Jun 3, 2021 at 14:06
4
  • Coin should have only getters about their value and no insert method since it doesn't have any responsability other that being a legit coin.
  • VendingMachine should have an Accept(Coin* coin) method.

Hence:

vending_machine->Accept(coin); 
6
  • A coin also needs to have a link to a value holder, and a means a vending machine or other recipient can use to request that value be atomically transferred from its value holder into the vending machine's value holder.
    – supercat
    Commented Jun 3, 2021 at 21:05
  • Actually, the Vending Machine should have an Accept method, but the parameter to that method should be something that implements an IRepresentMoney interface that defines a value getter, a boolean IsFixedValue getter and a Deduct method. Both a Coin class and a PaperMoney class would implement an appropriate value getter and an IsFixedValue that returns true. A DebitCard implementation would have a different implementation; one that returns false for the IsFixedValue and relies on Deduct to figure things out.
    – Flydog57
    Commented Jun 3, 2021 at 23:47
  • @supercat Coins are value tokens. They don't have any need to know where they are, how they are being interacted with, etc. A coin doesn't act any differently in your wallet, your bank account, the vending machine or down the back of your couch. Nothing changes about the coin depending on its location. When you put a coin in a vending machine, you are performing the action, not the coin. You are moving the value from your wallet(/pocket/purse/couch cushions) to the vending machine.
    – Corey
    Commented Jun 4, 2021 at 10:42
  • @Corey: Object-oriented languages use easily-copied references to identify objects. Coins would be pretty useless as a method of payment if someone who had coin #47193 could simply go up to a bunch of vending machines and say to each one "I'm inserting coin #47193; give me some soda", without the machines having any way of checking whether coin #47193 had already been used.
    – supercat
    Commented Jun 4, 2021 at 13:18
  • @supercat Which is why it's silly to have code representations of objects like that. Coins in the real world are non-replicable. In code we don't create coins and notes and such, we just move values around. In the real world we do that with value tokens.
    – Corey
    Commented Jun 4, 2021 at 13:25
2

Not to be too overly literal here... but

"How do interactions between objects work?"

has one answer:

Exactly as you define them to.

Other answers go into more detail and more specifically address your detailed question... definitely see those. But I think you're missing a core point, just by the way this question is asked. Don't get so lost in jargon and "best practices" that change constantly anyway. Understand what you're doing instead, and you can adapt to the changing landscape as technology evolves.

Side note, OOP isn't always the best solution. Again, it goes back to understanding vs regurgitating what you've been told/read.

2
  • 3
    I am a huge fan of OOP, but I don't think it's effectively taught in school. Perhaps the biggest problem is that OOP becomes increasingly useful as a system increases in complexity (assuming you design your system well), and yet in school, the projects are often too simple to see those benefits, and in the cases where you do have a large project, there is never enough time to refactor and improve the design as it grows. You can put up with the poor design because you throw it away at the end of the semester, and so you don't really get a chance to discover the benefits of "best practices".
    – TallChuck
    Commented Jun 3, 2021 at 15:30
  • @TallChuck I agree 100% with your statements. I'm in no way "anti-oop" or whatever. I just think its stressed far too much, and frankly I think everyone should learn procedural style in notepad before anything else. Yes its harder, and not a good way to design software in general, especially not complex systems, but I think it'd help separate out OOP design from the way the systems are actually working, and I don't think you can design well for OOP without that understanding
    – TCooper
    Commented Jun 3, 2021 at 16:44
1

I would argue that the vendingMachine should be responsible for setting the status of the coin.

You want to minimize the risk that someone does something incorrectly like calling coin->Insert but not vending_machine->InsertCoin. Requiring sequences of calls like this to be done for correct functioning makes the system very difficult to use correctly.

  • Present only one alternative, like vending_machine->InsertCoin, and ensure this does everything needed.
  • Allow both alternatives, and ensure that vending_machine->InsertCoin and coin->Insert does the same thing, and that either or both can be called, in arbitrary order, with the same result.

It might be helpful to think of classes like APIs, you would want a class, or a set of classes, to work as the user would expect them to. How a set of classes distribute responsibilities between themselves is less important as long as it is consistent and correct (and tested). Another good suggestion is to make it easy to do do common things correctly, and difficult to do unusual or incorrect things, sometimes referred to the pit of success.

An example could be a point and a transform. I want to apply the transform to the point to produce a new point, and there are various ways to write this:

  • myPoint.Transform(myTransform)
  • myTransform.Transform(myPoint)
  • Transform(myPoint, myTransform)

I do not think it really matters what you pick, as long as it is consistent. In each case the it is clear what the method does, and it is not clear that a majority of the users would prefer any single option.

0

One important aspect that all other answers have not touched upon is ownership.

Your coin represents a resource. Ideally, the same coin cannot be spent twice in a row without having been recovered in the mean time. I think that this is what your in_vending_machine boolean attempts to model.

It is indeed possible to use such a boolean, but this doesn't hold up very well. At some point the coin is retrieved from the vending machine and deposited to the bank, should it have a in_bank boolean too?

Instead, you can leverage the language (move constructors!) to physical move the resource:

class Coin {
public:
    //  Coins can be created and destructed.
    Coin() = default;

    explicit Coin(int cents): cents(cents) {
        assert(cents > 0);
    }

    ~Coin() = default; // if cents != 0, the equivalent of 
                       // your change rolling into a grate.

    //  Coins can be transferred.
    Coin(Coin&& other): cents(std::exchange(other.cents, 0)) {}

    Coin& operator=(Coin&& other) {
        assert(this != &other);
        cents = std::exchange(other.cents, 0);
        return 0;
    }

    //  Coins do not duplicate themselves.
    Coin(Coin const&) = delete;
    Coin& operator=(Coin const&) = delete;

    //  The value of the coin, in cents.
    //  FIXME: use better type to represent money, also... currency!
    int value() const { return cents; }

private:
    int cents{ 0 };
};

Now, we can move our coin into the vending machine, and then the previous owner doesn't have the coin any longer, but only a shell of a coin worth 0.

Note: unfortunately, C++ forces any move-only class to have a "default" state; it's sad, but it's the reality we have to live with.

And thus:

class VendingMachine {
public:
    //  Inserts the coin in the vending machine, increasing the amount
    //  available to buy from it.
    void insert_coin(Coin coin) {
        assert(coin.value() > 0);

        current[coin.value()] += 1;
    }

    //  Returns the coins inserted since the last purchase.
    std::vector<Coin> cancel() {
        std::vector<Coin> coins;

        for (auto const [value, number] : current) {
            for (std::size_t i = 0; i < number; ++i) {
                 coins.push_back(Coin(value));
            }
        }

        return coins;
     }

     //  Purchase...
private:
    //  If you only care about the total, could just be "cents",
    //  but this would not represent non-fungible resources well.
    std::map<int, std::size_t> current;
    std::map<int, std::size_t> coins;
};

int main() {
    VendingMachine vending_machine;

    vending_machine.insert_coin(Coin(10));
    vending_machine.insert_coin(Coin(10));

    auto coins = vending_machine.cancel();
    assert(coins.size() == 2);
    assert(coins[0].value() == 10);
    assert(coins[1].value() == 10);
}