The 20th chapter on the revelation of coffee
This example has many advantages for teaching. It is short, easy to understand, and shows how to apply object-oriented design principles to manage dependencies and classification concerns. But on the other hand, its short also means that the benefits of this separation may not be worth the cost. Look at it as a design idea.
20.1 Mark IV Specialty Coffee machine
20.1.1 Spec Sheet
The Mark IV-type coffee machine can produce 12 cups of coffee at a time. The user places the filter on the stand, where it is filled with ground coffee and pushes the bracket into its container. The user then pours 12 cups of water into the water filter and presses the Brew button. The water has been heated to boiling. The constant steam pressure causes water to spill over the coffee powder, resulting in droplets flowing through the filter into the coffee pot. The coffee pot is insulated by an insulating plate for long periods of time, and only when there is coffee in the pot does the insulation plate work. If the coffee pot is removed from the insulation tray while the water is still spraying on the coffee powder, the water will stop so that the brewed coffee will not splash on the insulation plate. The following are the hardware devices that need to be monitored.
- Heating elements of the heater. Can be turned on and off.
- Heating elements of the heating plate. Can be turned on and off.
- Insulation Disc sensor. It has 3 states: Warmerempty, Potempty, and Potnotempty.
- A heater sensor is used to determine if there is water. It has two states: Boilerempty, Boilernotempty.
- Brew button. This momentary button initiates the brewing process. It has an indicator light that is lit when the brewing process finishes, indicating that the coffee has been cooked.
- The pressure relief valve, when opened, can reduce the stress in the heater. Lower pressure will prevent the flow of water to the filter. The valve can be turned on and off.
The hardware of the Mark IV dedicated coffee machine has been designed, and the hardware engineer has provided us with a low-level API, as follows:
namespacecoffeemaker{ Public enumwarmerplatestatus {warmer_empty, pot_empty, pot_not_empty}; Public enumboilerstatus {EMPTY, not_empty}; Public enumbrewbuttonstatus {pushed, not_pushed}; Public enumboilerstate {on, OFF}; Public enumwarmerstate {on, OFF}; Public enumindicatorstate {on, OFF}; Public enumreliefvalvestate {OPEN, CLOSED}; Public InterfaceCoffeemakerapi {/** This function returns the status of the warmer-plate * sensor. This sensor detects the presence of the pot * and whether it have coffee in it. */warmerplatestatus getwarmerplatestatus (); /** This function returns the status of the boiler switch. * The boiler switch is a float switch this detects if * there is more than-a-cup of water in the boiler. */boilerstatus getboilerstatus (); /** This function returns the status of the Brew button. * The Brew button is a momentary switch this remembers * its state. Each call to this function returns the * remembered state, and then resets, state to * not_pushed. * * Thus, even if this function was polled at a very slow * rate, it would still detect when the brew button is * pushed. */brewbuttonstatus getbrewbuttonstatus (); /** This function turns the heating element with the boiler * on or off. */ voidsetboilerstate (boilerstate s); /** This function turns the heating element with the warmer * plate on or off. */ voidsetwarmerstate (warmerstate s); /** This function turns the indicator light on or off. * The indicator light should is turned on at the end * of the brewing cycle. It should is turned off when * The user presses the Brew button. */ voidsetindicatorstate (indicatorstate s); /** This function opens and closes the pressure-relief * valve. When this valve was closed, steam pressure in * the boiler would force hot water to spray out over * the Coffe E filter. When the valve was open, the steam * in the boiler escapes to the environment, and the * water in the Boile R would not be spray out over the filter. */ voidsetreliefvalvestate (reliefvalvestate s); }}
We are designing software for a simple embedded real-time system. I expect to be able to give a set of class diagrams, sequence diagrams, and state diagrams.
20.1.2 Common Ugly Solution
Shows the most common ugly scenarios:
It's hard for beginners to recognize how ugly this design is. There are some very serious errors hidden in this picture. Many of them will only be noticed when you start writing code for this design, and then you will find out how ridiculous the code is written.
Let's take a look at some of the problems with how this UML diagram is created.
Missing method
When designers create views without methods, they may not divide the software according to behavior. No behavior-based partitioning is basically a serious error. It is the behavior of the system that gives us the first clue on how to divide the system.
Steam type
If you consider the method that the light class should have, you will find out how bad the design is divided. Obviously, the light object should be able to be opened or turned off. Therefore, we will put the on () and Off () methods into the light class. What does it look like when these functions are implemented? As follows:
Public class Light { publicvoid on () { CoffeeMaker.api.SetIndicatorState (indicatorstate.on ); } Public void Off () { CoffeeMaker.api.SetIndicatorState (indicatorstate.off); } }
The light class has several strange places. First, he doesn't have any member variables. Some are unusual, because objects usually have some kind of state to manipulate. In addition, the on () and off () methods simply delegate the work to the Coffeemakerapi Setindicatorstate method. Obviously, the light class is just a call converter that doesn't do anything useful.
The button class, the boiler class, and the Warmerplate class have the same problem. They are all just adapters that convert one invocation format into another format. In fact, it is entirely possible to remove them from the design without causing any change in the logic of the Coffeemaker class. The Coffermaker class can simply call Coffeemakerapi instead of using these adapters.
By studying the methods and code of these classes, we have relegated those important positions in the graph to the purely placeholder that there is not much that is necessary. Therefore, we call them water vapor class.
20.1.3 The imaginary abstraction
The sensor and heater classes are not used in all classes in the system. They have up to a few abstract methods. such as the heater interface. A class that contains only abstract methods and does not have any users is a useless class.
Public Interface Heater { void TurnOn (); void turnoff (); }
At first, there seemed to be a lot of classes with meaningful functions. But when we start writing code that implements the western Zhejiang class, we find that only one of the Coffermaker classes has some meaningful behavior, and all the other classes are either fictitious abstractions or steam classes.
20.1.4 Improvement Program
The trick to solving this problem (or any problem) is to step back and separate the nature and details of the problem. Forget all the small details of heaters, valves, sensors, etc., and focus on the fundamental issues. What is the underlying problem? is how to cook coffee.
How to cook coffee? The simplest and most common method is to pour hot water on the ground coffee and collect the good coffee liquid in a container. Where does the hot water come from? From the Hotwatersource class. Where do you store your coffee? stored in the Containmentvessel.
Considering the parts of Mark IV, you can imagine that heaters, valves, and Heating Sensors act as hotwatersource roles. Hotwatersource is responsible for the water heating and spraying on the ground coffee, forming a solution into the Containmentvessel. We can also imagine the role of the insulation disc and its sensors acting as containmentvessel. He is responsible for maintaining the temperature of the coffee and letting us know if there is any coffee in the container.
How do I use UML to describe the discussion above? Shows a possible approach. Hotwatersource and Containmentvessel are classes that are linked by coffee flow.
This association is a common mistake made by beginners. This association is based on some of the physical associations in the problem rather than the software control behavior. Coffee flows from Hotwatersource to Containmentvessel and the associations between these two classes are completely unrelated.
What happens if the start and stop of the hot water flow leading to the vessel is Containmentvessel notified Hotwatersource? As shown below, note that Containmentvessel sent the start message to Hotwatersource. This means that the association relationship is in the opposite direction. Containmentvessel relies on Hotwatersource.
Associations are the paths through which messages are sent between objects. There is no relationship between the association and the flow direction of the physical entity.
There is no way to make it possible for people to exchange with our systems. The system must be able to report its work status to the owner. We have added a userinterface class to the coffee machine model.
Let's look at a few use cases to see if we can figure out the behavior of these classes.
Use Case 1: User presses the Brew button
When both Hotwatersource and Containmentvessel are ready, the UserInterface object should send the start message to Hotwatersource, and Hotwatersource will begin to work.
Use Case 2: The receiving vessel is not ready.
When the receiving vessel is not ready, Containmentvessel notifies Hotwatersource to stop transmitting hot water, when ready, then notifies Hotwatersource to turn on the hot water again, the end and recovery of hot water flow as follows:
Use Case 3: Brew complete
Both Hotwatersource and Containmentvessel can send the done message.
Use case 4: coffee is finished
Mark IV turns off the indicator when the brew is finished and an empty coffee pot is placed on the insulation plate.
According to this picture, we can draw a class diagram with the same relationship. As follows:
20.1.5 Implementing an abstract model
None of the 3 classes we created can know anything about Mark IV. This is the dependency inversion principle (DIP). We do not allow high-level coffee-making strategies in the system to be dependent on lower levels of implementation.
User presses the Brew button
How did UserInterface know that the brew button was pressed? It has to call the Coffeemakerapi.geebrewbuttonstatus () function. We decided that the UserInterface class could not know Coffeemakerapi. According to the dip, this call is placed in a derived class of userinterface.
The code is as follows:
Public classM4userinterface:userinterface {Private voidCheckbutton () {brewbuttonstatus status=CoffeeMaker.api.GetBrewButtonStatus (); if(Status = =brewbuttonstatus.pushed) {startbrewing (); } } } Public classUserInterface {PrivateHotwatersource HWS; PrivateContainmentvessel CV; Public voidDone () {} Public voidComplete () {}protected voidstartbrewing () {if(HWS. IsReady () &&CV. IsReady ()) {HWS. Start (); Cv. Start (); } } }
Why create a protected startbrewing () method? Maintenance no longer m4userinterface call the start () function directly? The reason is simple, but it's important. The IsReady () test and subsequent calls to the start () method of Hotwatersource and Containmentvessel are both high-level policies and should be attributed to the UserInterface class.
Implementing the IsReady () method
Public classM4hotwatersource:hotwatersource { Public Override BOOLIsReady () {boilerstatus status=CoffeeMaker.api.GetBoilerStatus (); returnStatus = =Boilerstatus.not_empty; } } Public classM4containmentvessel:containmentvessel { Public Override BOOLIsReady () {warmerplatestatus status=CoffeeMaker.api.GetWarmerPlateStatus (); returnStatus = =Warmerplatestatus.pot_empty; } }
Implementing the Start () method
The start () method of the Hotwatersource is only an abstract method, and M4hotwatersource implements the function of closing the valve and opening the heater in the method call Coffeemakerapi. As I was writing these functions, I began to get bored of writing structures like CoffeeMaker.api.XXX, so I did some refactoring at the same time.
Public classM4hotwatersource:hotwatersource {PrivateCOFFEEMAKERAPI API; PublicM4hotwatersource (Coffeemakerapi API) { This. API =API; } Public Override BOOLIsReady () {boilerstatus status=API. Getboilerstatus (); returnStatus = =Boilerstatus.not_empty; } Public Override voidStart () {API. Setreliefvalvestate (reliefvalvestate.closed); Api. Setboilerstate (Boilerstate.on); } } Public classM4containmentvessel:containmentvessel {PrivateCOFFEEMAKERAPI API; Private BOOLIsbrewing =false; PublicM4containmentvessel (Coffeemakerapi API) { This. API =API; } Public Override BOOLIsReady () {warmerplatestatus status=API. Getwarmerplatestatus (); returnStatus = =Warmerplatestatus.pot_empty; } Public Override voidStart () {isbrewing=true; } }
Call M4userinterface.checkbutton
How does the control flow of the system call the Coffeemakerapi.getbrewbuttonstatus () function? Select a thread or poll? This decision can be made at the last minute. This has no effect on the design. It is best to always assume that messages can be sent asynchronously, as if there were separate threads.
Let's say we use polling:
Public Interface pollable{ void Poll ();}
Public Static voidMain (string[] args) {COFFEEMAKERAPI API=NewM4coffeemakerapi (); M4userinterface UI=NewM4userinterface (API); M4hotwatersource HWS=NewM4hotwatersource (API); M4containmentvessel CV=NewM4containmentvessel (API); Ui. Init (HWS, CV); Hws. Init (UI, CV); Cv. Init (HWS, UI); while(true) {UI. Poll (); Hws. Poll (); Cv. Poll (); } }
public class m4userinterface:userinterface,pollable { private COFFEEMAKERAPI API; public M4userinterface (Coffeemakerapi API) { this . API = API; public void Poll () {brewbuttonstatus status = API. Getbrewbuttonstatus (); if (status == brewbuttonstatus.pushed) {startbrewing (); } } }
Finish the coffee machine practice
20.1.6 the benefits of this design
The lines circle the 3 abstract classes. The class in the circle is not dependent on any class outside the loop. Therefore, the abstraction is completely separated from the details.
20.2 Object-oriented over-design
This example has many advantages for teaching. It is short, easy to understand, and shows how to apply object-oriented design principles to manage dependencies and classification concerns. But on the other hand, its short also means that the benefits of this separation may not be worth the cost.
If the Mark IV coffee machine is implemented as a finite state machine, we will find that it has 7 states and 18 migrations. We can use the 18-line SMC (the state machine Compiler, status machines compiler) code to represent state machines. Polling the sensor's simple main loop is also more than 10 lines of code, the limited state of the call action function is also around dozens of lines of code. In short, we can implement the entire program within one page of code.
If the test code is not counted, the object-oriented implementation of the coffee machine has 5 pages of code. We cannot make a reasonable explanation for this disparity. In large applications, the benefits of dependency management and separation of concerns can significantly outweigh the cost of object-oriented design. But in this case, we may draw the opposite conclusion.
Excerpt from: "Agile Software Development: principles, patterns and Practices (C # Edition)" Robert C.martin Micah Martin
Reprint please specify the source:
Jesselzj
Source: http://jesselzj.cnblogs.com
Agile Software Development: principles, patterns and practices--the 20th chapter on the revelation of coffee