I'm trying to implement vending machine using state design pattern. The problem is that the state interface contain some methods which might not be relevant to some concrete implementations of the state interface. As a result it violates the interface segregation principle. So how can we implement it without violating it? How will the project structure and implementation of state context will look like?
My state interface :
public interface State {
public void clickOnInsertCoinButton(VendingMachine machine) throws Exception;
public void clickOnStartProductSelectionButton(VendingMachine machine) throws Exception;
public void insertCoin(VendingMachine machine , Coin coin) throws Exception;
public void chooseProduct(VendingMachine machine, int codeNumber) throws Exception;
public int getChange(int returnChangeMoney) throws Exception;
public Item dispenseProduct(VendingMachine machine, int codeNumber) throws Exception;
public List<Coin> refundFullMoney(VendingMachine machine) throws Exception;
public void updateInventory(VendingMachine machine, Item item, int codeNumber) throws Exception; }
Concrete state implementations:
public class IdleState implements State {
public IdleState(){
System.out.println("Currently Vending machine is in IdleState");
}
public IdleState(VendingMachine machine){
System.out.println("Currently Vending machine is in IdleState");
machine.setCoinList(new ArrayList<>());
}
@Override
public void clickOnInsertCoinButton(VendingMachine machine) throws Exception{
machine.setVendingMachineState(new HasMoneyState());
}
@Override
public void clickOnStartProductSelectionButton(VendingMachine machine) throws Exception {
throw new Exception("first you need to click on insert coin button");
}
@Override
public void insertCoin(VendingMachine machine, Coin coin) throws Exception{
throw new Exception("you can not insert Coin in idle state");
}
@Override
public void chooseProduct(VendingMachine machine, int codeNumber) throws Exception{
throw new Exception("you can not choose Product in idle state");
}
@Override
public int getChange(int returnChangeMoney) throws Exception{
throw new Exception("you can not get change in idle state");
}
@Override
public List<Coin> refundFullMoney(VendingMachine machine) throws Exception{
throw new Exception("you can not get refunded in idle state");
}
@Override
public Item dispenseProduct(VendingMachine machine, int codeNumber) throws Exception{
throw new Exception("proeduct can not be dispensed idle state");
}
@Override
public void updateInventory(VendingMachine machine, Item item, int codeNumber) throws Exception {
machine.getInventory().addItem(item, codeNumber);
}
}
3 more concrete state implementations are there with some not all methods overridden.
State Context:
public class VendingMachine {
private State vendingMachineState;
private Inventory inventory;
private List<Coin> coinList;
public VendingMachine(){
vendingMachineState = new IdleState();
inventory = new Inventory(10);
coinList = new ArrayList<>();
}
public State getVendingMachineState() {
return vendingMachineState;
}
public void setVendingMachineState(State vendingMachineState) {
this.vendingMachineState = vendingMachineState;
}
public Inventory getInventory() {
return inventory;
}
public void setInventory(Inventory inventory) {
this.inventory = inventory;
}
public List<Coin> getCoinList() {
return coinList;
}
public void setCoinList(List<Coin> coinList) {
this.coinList = coinList;
}
}
CodePudding user response:
The interface segregation principle is about the interface supplied to clients of a service. The idea is that if the caller of an interface only uses a subset of methods on the interface, it suggests that the interface has too many responsibilities: the ones used by this caller, and the ones that are only used by other callers can be separated.
On the implementation side, implementations of all interface methods need to be provided. There's no way for the caller to know which methods are valid for the current state without violating encapsulation.
In this specific example, you can think of the interface methods as the physical controls on a mechanical vending machine, and the caller as the person standing in front of it. In a real vending machine, the physical controls can't rearrange themselves whenever the state changes. There's no way to prevent a person from pressing the "wrong" button for the current state, so there needs to be a defined behaviour for when that does happen. In practice, the machine may choose to show an error on a display, flash an LED, play a sound, or do nothing at all. These are all valid implementations of these methods!
An alternative design might be to have each state implement a different interface, and have each method on a state return the new state. This option can be good when there really is a different interface to present to the caller. To stretch this example, maybe the vending machine uses a touch screen display instead of mechanical controls.
One drawback of this alternative is that there is no way to guarantee that a caller won't retain a reference to an outdated state and call methods on it. You can turn this into a runtime error by ensuring that each state instance changes to throw exceptions from every method after it has been "used". In some cases, you may be able to allow calls on the old state to continue working, as if time-travelling back to the earlier state. Whether this makes sense for a particular use case is application dependent — you wouldn't want it for a vending machine, for example.
CodePudding user response:
There is a super book about design patterns "Head First Design Patterns, 2nd Edition". And this book has a chapter about "State pattern". Its code has the following methods of State pattern:
public interface State
{
void InsertQuarter();
void EjectQuarter();
void TurnCrank();
void Dispense();
}
In my view, these list of methods should be enough for your requirements:
public interface State
{
void InsertCoins(List<Coin> coins);
List<Coin> EjectCoins();
void ChooseProduct(Product product);
void Dispense();
}
and other code will look like this. Let me show an example via C#. C# has almost the same API with Java and I have not used any special feature of C# which cannot be converted to Java.
Model of Coin
:
public class Coin
{
public string Name { get; set; }
public string Count { get; set; }
}
Model of Product
:
public class Product
{
public int Code { get; set; }
public string Name { get; set; }
public string Count { get; set; }
}
Model of Inventory
:
public class Inventory
{
List<Product> products = new List<Product>();
Product selectedProduct;
public void Add(Product product) { }
public void ReturnProduct()
{
if (selectedProduct != null)
{
products.Remove(selectedProduct);
}
}
public void ChooseProduct(Product product)
{
selectedProduct = product;
}
}
Model of VendingMoney
:
class VendingMoney
{
List<Coin> coins = new List<Coin>();
List<Coin> insertedCoins;
public void Add()
{
if (insertedCoins != null)
coins.AddRange(insertedCoins);
}
public void Minus(List<Coin> coins) { }
public void Insert(List<Coin> coins)
{
insertedCoins = coins;
}
public List<Coin> Eject()
{
List<Coin> tempCoins = new List<Coin>(insertedCoins);
insertedCoins = null;
return tempCoins;
}
}
And VendingMachine
:
public class VendingMachine
{
Inventory inventory = new Inventory();
VendingMoney vendingMoney = new VendingMoney();
State _state;
public void ReturnProductIntoInventory()
{
inventory.ReturnProduct();
}
public void AddProduct(Product product)
{
inventory.Add(product);
}
// Take money and then give a product
public void AddCoins()
{
vendingMoney.Add();
}
public void ChooseProduct(Product product)
{
_state.ChooseProduct(product);
}
public List<Coin> EjectCoins()
{
return _state.EjectCoins();
}
// Just put money to Vending machine without buying a product
public void InsertCoins(List<Coin> coins)
{
_state.InsertCoins(coins);
}
// give a product, take money
public void Dispense()
{
_state.Dispense();
}
public void SetState(State state)
{
_state = state;
}
}
And one othe implementations of State
:
public class HasCoinsState : State
{
VendingMachine _vendingMachine;
public HasCoinsState(VendingMachine vendingMachine)
{
_vendingMachine = vendingMachine;
}
public void ChooseProduct(Product product)
{
_vendingMachine.ChooseProduct(product);
}
public void Dispense()
{
_vendingMachine.ReturnProductIntoInventory();
_vendingMachine.AddCoins();
}
public List<Coin> EjectCoins()
{
_vendingMachine.ReturnProductIntoInventory();
return _vendingMachine.EjectCoins();
}
public void InsertCoins(List<Coin> coins)
{
_vendingMachine.InsertCoins(coins);
}
}