Search code examples
oopanalysissoftware-design

OO Analysis--Operation placement


I'm confused as where I should place the operation/function when identifying classes. The following example--taken from the lecture slides of object-oriented design using UML, patterns and Java--particularly confuses me. In this example 3 classes are identified from the following part of use case description "The customer enters the store to buy a toy". 2 functions are also identified, one is enters() (placed in the Store class) and the other is buy() (placed in the Toy class).

Why those functions are not associated with the Customer who perform them? Is there any heuristic to help with operation placement?


Solution

  • Your example is extremely simple, and it's hard to say something about it without a context. Anyway, I'll try to answer your question. So, first of all: oo modeling is not about building your classes in a "natural" way. The reason is very simple: even if we wanted to model the "real world" objects, it's simply impossible. The relations between real-world (Customer, Store, Toy) objects are almost infinitely complex. Let's think about your case for a while. When a customer enters a store, there is a lot of things happening, let's try to order them:

    Customer enters a store

    1. Customer needs to interact with the "Store gateway" somehow, for example with a door. Even this interaction can be complex: store can be closed, full, an accident can happen, door can be blocked, etc
    2. When customer finally is inside the store, maybe there's a special store policy to greet customers (or every n-th customer). We can also imagine a lot of other things.
    3. Finally, the customer wants to buy a toy. First, she needs to find that toy, which might not be so easy (how would you model this interaction?).
    4. When the desired toy is found, she needs to take it and add to the shopping basket.
    5. Then, customer goes to the queue and waits for her turn.
    6. When waiting is over, the customer interacts with the cashier (a lot of small things, like take the toy, check it's price, maybe some quick chat...)
    7. Finally, the customer can pay for the toy (check if she have enough money, select the paying method (cash, card, nfc?), leave the queue...).
    8. The customer leaves the store (similar to the "enters a store" interaction, plus maybe security checking).

    I'm absolutely sure I forgot about something. As you can see, the simple scenario is in fact very complex in real world. That's why it's impossible to model it exactly the same way. Even if we tried, the naive 1-to-1 mapping would probably lead to the design, where almost every action is a method of the Customer class: customer.enter(), customer.leave(), customer.buy(), customer.findToy(), customer.interactWithCashier(), customer.openDoor()... and lot more. This naive mapping would be entirely bad, because every step in the "Customer enters a store" scenario is in fact a collaboration of multiple objects, each somehow connected with another. From the other hand, if we tried to implement this scenario with all interactions, we would create a system that would take years to build and would be simply impossible to deal with (every change would require insane amounts of hours).

    Ok, so how to follow ood principles? Take just a part of the interaction. Do not try to model it exactly the same way as it works in the real world. Try to adjust the model to the needs of your client. Don't overload your classes with responsibility. Every class should be easy to understand, and relatively small. You can follow some of the basic principles of software modeling, such as SOLID, YAGNI. Learn about design patterns in practice (find some GOF patterns and try to implement them in your projects). Use code metrics to analyze your code (Lack of Cohesion of methods, Efferent coupling, Afferent coupling, Cyclomatic complexity) to keep your code simple.

    Let's get back to your specific example. According to the rules I mentioned before, the very important part of object modeling is to place methods where they belong. So, the data and the methods should be "coherent" (see Lack of Cohesion of Methods metric). So, your classes should generally do one thing. In your example, the responsibility of the Store class could be, for example, to allow customers to buy toys. So, we could model it this way:

    public class Store {
    
        public void buyToy(Toy toy, Customer customer) 
        throws ToyNotAvailableException, InsufficientFundsException {
            // some validation - check* methods are private
            if(!checkToyIsAvailable(toy)) {
                throw new ToyNotAvailableException();
            }
            if(!checkCustomerHasFunds(customer, toy.price())){
                throw new InsufficientFundsException();
            }
            // if validation succeeds, we can remove the toy from store
            // and charge the customer
            // removeFromStore is a private method      
            removeFromStore(toy);           
            customer.charge(toy.price());
        }
    
    }
    

    Keep in mind that this is just a simple example, created to be easy to understand and read. We should refine it many times to make it production-ready (for example handle payment method, number of items etc).