1
\$\begingroup\$

I have an exercise requires me to write a service to management menu.

The requirement like this: Food menu includes: breakfast menu, lunch menu, dinner menu. The three menu haves list menu item( name, price, description, image). Write CRUD operation for food menu.

At first, I separate completely three types of food menu to each service and write service for each food menu:

My model ( at first):

Food Menu model:

public class FoodMenu{
    private BreakfastMenu breakfastMenu;
    private DinnerMenu dinnerMenu;
    private LunchMenu lunchMenu;

    public FoodMenu(BreakfastMenu breakfastMenu, DinnerMenu dinnerMenu, LunchMenu lunchMenu){
        this.breakfastMenu = breakfastMenu;
        this.dinnerMenu = dinnerMenu;
        this.lunchMenu = lunchMenu;
    }
}

Breakfast Menu model:

public class BreakfastMenu {
    private List<MenuItem> menuItemList;
    public BreakfastMenu(List<MenuItem> menuItemList){
        this.menuItemList = menuItemList;
    }
}

Dinner Menu model:

public class DinnerMenu {
    public DinnerMenu(){}
    private List<MenuItem> menuItemList;

    public DinnerMenu(List<MenuItem> menuItemList){
        this.menuItemList = menuItemList;
    }
}

Lunch Menu model:

public class LunchMenu {
    private List<MenuItem> menuItemList;
    public LunchMenu(List<MenuItem> menuItemList){
        this.menuItemList = menuItemList;
    }
    public LunchMenu(){}
}

My Service( at first): Breakfast Menu services ( another two service Lunch and Dinner Menu are the same they only different in parameters):

public class BreakfastMenuServicesImpl implements BreakfastMenuServices {

    @Override
    public BreakfastMenu addMenuItemsToBreakfastMenu(BreakfastMenu breakfastMenu) {
        Scanner scanner = new Scanner(System.in);

        MenuItem menuItem = new MenuItem();

        List<MenuItem> menuItemList= breakfastMenu.getMenuItemList();

        if(menuItemList == null){
            menuItemList = new ArrayList<MenuItem>();
        }
        System.out.print("\nInsert food name: ");
        menuItem.setNames(scanner.nextLine());
        System.out.print("Insert food description: ");
        menuItem.setDescription(scanner.nextLine());
        System.out.print("Insert food image: ");
        menuItem.setImage(scanner.nextLine());
        System.out.print("Insert food price:");
        menuItem.setPrice(scanner.nextFloat());

        menuItemList.add(menuItem);
        breakfastMenu.setMenuItemList(menuItemList);
        return breakfastMenu;
    }

    @Override
    public void showBreakFastMenu(BreakfastMenu breakfastMenu) {
        System.out.println("\nBreakfast menu:");
        breakfastMenu.getMenuItemList().forEach(System.out::println);
    }

    @Override
    public void updateBreakfastMenu(BreakfastMenu breakfastMenu) {
        Scanner scanner = new Scanner(System.in);
        System.out.print("Insert food want to update:");
        String foodItem = scanner.nextLine();
        breakfastMenu.getMenuItemList().forEach((MenuItem menuItem)->{
            if(menuItem.getNames().equals(foodItem)){
                System.out.print("\nInsert food name: ");
                menuItem.setNames(scanner.nextLine());
                System.out.print("Insert food description: ");
                menuItem.setDescription(scanner.nextLine());
                System.out.print("Insert food image: ");
                menuItem.setImage(scanner.nextLine());
                System.out.print("Insert food price:");
                menuItem.setPrice(scanner.nextFloat());
            }
        });
    }

    @Override
    public void deleteBreakfastMenu(BreakfastMenu breakfastMenu) {
        Scanner scanner = new Scanner(System.in);
        System.out.print("Insert food want to delete:");
        String foodItem = scanner.nextLine();
        breakfastMenu.getMenuItemList().removeIf(menuItem ->
                                                    menuItem.getNames().equals(foodItem));
    }
}

After finishing service, I realize that all the services share the same CRUD process and I can write one father class for model and one father service class for three types of food menu services:

My new model ( after): Food Menu model:

public class FoodMenu{
    private BreakfastMenu breakfastMenu;
    private DinnerMenu dinnerMenu;
    private LunchMenu lunchMenu;

    public FoodMenu(BreakfastMenu breakfastMenu, DinnerMenu dinnerMenu, LunchMenu lunchMenu){
        this.breakfastMenu = breakfastMenu;
        this.dinnerMenu = dinnerMenu;
        this.lunchMenu = lunchMenu;
    }
}

AbstractMenu model:

public class AbstractMenu {
    private List<MenuItem> menuItemList;
    public AbstractMenu(List<MenuItem> menuItemList){
        this.menuItemList = menuItemList;
    }
    public AbstractMenu(){
    }
}

Breakfast Menu model:

public class BreakfastMenu extends AbstractMenu{
}

Lunch Menu model:

public class LunchMenu extends AbstractMenu{
}

Dinner Menu model:

public class DinnerDMenu extends AbstractMenu{
}

My new Service(After)

AbstractMenuService:

public interface AbstractMenuService {
    AbstractMenu addMenuItemsToMenu(AbstractMenu abstractMenu);
    void showMenu(AbstractMenu abstractMenu);
    void updateMenu(AbstractMenu abstractMenu);
    void deleteMenu(AbstractMenu abstractMenu);
}

BreakfastMenuService:

public interface BreakfastMenuServices extends AbstractMenuService{
}

DinnerMenuService:

public interface DinnerMenuServices extends AbstractMenuService{

}

LunchMenuSerivce:

public interface LunchServices extends AbstractMenuService{

}

So which one is easier to read and maintain, furthermore, as a result of extending service from father class, all methods in BreakfastMenuImpl requires the override but I don't need to write different methods so I left it's blank. Does this have any negative effects?

My new BreakfastMenuServiceImpl:

public class BreakfastMenuServicesImpl implements BreakfastMenuServices {


    @Override
    public AbstractMenu addMenuItemsToMenu(AbstractMenu abstractMenu) {
        return null;
    }

    @Override
    public void showMenu(AbstractMenu abstractMenu) {

    }

    @Override
    public void updateMenu(AbstractMenu abstractMenu) {

    }

    @Override
    public void deleteMenu(AbstractMenu abstractMenu) {

    }
}
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Welcome to Code Review! I changed the title so that it describes what the code does per site goals: "State what your code does in your title, not your main concerns about it.". Please check that I haven't misrepresented your code, and correct it if I have. \$\endgroup\$ Nov 21, 2022 at 7:46

1 Answer 1

1
\$\begingroup\$

If things really look the same, they probably are. You were right to add a class hierarchy to implement the common aspects only once, but you seem to have fallen into a few traps. All my comments are focused on a few methods and the breakfast-related classes but can be easily extended to all the methods for all meals.

Your new service does not do what you think it does, for different reasons.

This may be due to an incomplete post but in case it isn't, here they are.

AbstractMenu is not abstract

Nothing whatsoever prevents the instantiation of an AbstractMenu object. It is just weird to call abstract something that is not.

There is no behaviour implemented

In your first model, showBreakfastMenu does stuff. In your second model, you never actually implement its behaviour. Declaring a method signature in an interface does not magically implement the method, and that is why your compiler tells you that you must add the overriden methods in BreakfastMenuServicesImpl: it actually tells you that the behaviour is not defined and that you must do it. As your method return void, you can leave them blank but that only goes to say that they do nothing.

Type safety went out the window

Your first breakfast menu service included the method public BreakfastMenu addMenuItemsToBreakfastMenu(BreakfastMenu breakfastMenu). Your second one includes public AbstractMenu addMenuItemsToMenu(AbstractMenu abstractMenu). There are three differences, listed in increasing order of significance:

  1. The method name changed. That's probably a good thing as long as no one was expecting a addMenuItemsToBreakfastMenu in your code.
  2. The return type changed. If you are expected to output a BreakfastMenu, then outputting an AbstractMenu is only half the job and you expect the caller to do the other half, casting what you return to BreakfastMenu, and hope that nothing breaks doing so.
  3. The input type changed. That's the part where things may become weird. If you have a BreakfastMenuServicesImpl (and not just a MenuServicesImpl), you don't expect it to handle LunchMenu objects and yet, yours does. And with the previous point, if you try to cast to BreakfastMenu the output of a call to addMenuItemsToMenu with a LunchMenu argument then you're definitely getting in trouble.

How to fix that?

You made a generic interface to cover the services for the different types of menus, but you forgot to parameterize it. First, replace public interface AbstractMenuService { with public interface AbstractMenuService<T extends AbstractMenu> { and all subsequent uses of AbstractMenu in the interface with T. Then, replace public interface BreakfastMenuServices extends AbstractMenuService{ with public interface BreakfastMenuServices extends AbstractMenuService<BreakfastMenu>{ and voilà! A BreakfastMenuServices object only handles BreakfastMenu items.

That solves the second issue, but you still have the first one. One way to implement the showMenu behaviour only once would be to implementshowMenu as a default method in the interface, as follows:

default void showMenu(T menu){
    System.out.println(menu.getTitle());
    menu.getMenuItemList().forEach(System.out::println);
}

This requires to add a getTitle() method in AbstractMenu but that works out just fine: you now have a good reason to make AbstractMenu abstract! Add public abstract String getTitle() in AbstractMenu, and override it in each menu implementation to add the text you want.

There are a few other things you could improve

These comments are not as critical but they might still help you decide your two models.

Avoid null when possible

Because you do not initialise your menuItems at object initialisation, you push null-handling code into each service method, like the following block from addBreakfastMenuItem in your first model.

if(menuItemList == null){
    menuItemList = new ArrayList<MenuItem>();
}

Except that you don't: if you call any method other than addBreakfastMenuItem on a BreakfastMenu initialised with the no-argument constructor, you get a NullPointerException.

The two simplest ways to fix that are either to initialise the list at declaration time (private List<MenuItem> menuItemList = new ArrayList<>();) or inside the no-argument constructor.

Class names

I have learnt quite a lot from Robert C. Martin's Clean Code. One of them is that it is usually a poor idea to call something "Abstract": it does not actually tell anything useful about the class that is not already told by the presence of the "abstract" keyword. If you don't have constraints on your actual class names, I would make the following changes:

  1. FoodMenu -> DailyMenu
  2. AbstractMenu -> Menu
  3. AbstractMenuService -> MenuService

Remove useless classes

In your new model, BreakfastMenuServicesImpl implements BreakfastMenuServices that extends AbstractMenuServices. Do you expect BreakfastMenuServices to have different implementing classes? If not, then there does not seem to be any point in it existing: let BreakfastMenuServicesImpl directly implement AbstractMenuServices.

Let's go further: is there actually a point in having an object specialised in handling services for breakfast menus, and another for lunch menus? If all the behaviour is implemented in the interface, then those implementing classes have no purpose: delete them and let the AbstractMenuService be a class. Make all of its methods static and add a private no-arg constructor and you get a standard utility class.

Let's go further still: do you really need a utility class? Rather than having a separate class for the services, make all of the methods from AbstractMenuService instance methods of AbstractMenu. Then, when you want to add an item to a menu, you get to do menu.addItem(), which really is how you should expect an object-oriented code to behave.

Summary

I get the following model, with a few holes that you still have to fill mostly with bits from your first model. If I had done it from scratch, I might have gone with enums to differentiate the three types of meals but I could live with the code below.

DailyMenu

public class DailyMenu{
    private BreakfastMenu breakfastMenu;
    private DinnerMenu dinnerMenu;
    private LunchMenu lunchMenu;

    public DailyMenu(BreakfastMenu breakfastMenu, DinnerMenu dinnerMenu, LunchMenu lunchMenu){
        this.breakfastMenu = breakfastMenu;
        this.dinnerMenu = dinnerMenu;
        this.lunchMenu = lunchMenu;
    }
}

Menu

public abstract class Menu<T extends Menu>{
    private List<MenuItem> menuItems;

    public Menu(List<MenuItem> menuItems){
        this.menuItems = menuItems;
    }

    public AbstractMenu(){
        this.menuItems = new ArrayList<>();
    }

    protected abstract String getMenuName();

    public T addMenuItemsToMenu(){
        ...
    }
    public void showMenu(){
        System.out.println(getMenuName());
        menuItems.forEach(System.out::println);
    }
    public void updateMenu(){
        ...
    }
    public void deleteMenu(){
        ...
    }
}

BreakfastMenu

public BreakfastMenu extends Menu<BreakfastMenu>{
    public BreakfastMenu(){
        super();
    }

    public BreakfastMenu(List<MenuItem> menuItems){
        super(menuItems);
    }

    @Override
    protected String getMenuName(){
        return "Breakfast menu";
    }
}
```
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.