ID: 1
1 Architectural Models
AquaLush is a Relaxed Layered system, as shown in the layer diagram in Figure B-10-1.
ID: 2
Figure B-10-1 AquaLush Layers Overview
ID: 3
Modules use only layers below them that they touch directly. A fielded system does not include the bottom Simulation layer, which is needed only as a stand-in for the hardware that would be present in a fielded system.
ID: 4
1.1 Layer Responsibilities
Layer
|
Responsibilities
|
Startup
|
Create and connect all runtime components based on a configuration specification and whether the program is a simulation or a fielded product. Restore the state of the system from persistent store.
|
User Interface
|
Implement a device-independent AquaLush text-based user interface.
|
Irrigation
|
Control both manual and automatic irrigation.
|
Device Interface
|
Implement virtual devices providing interfaces to all external entities, including hardware devices, a clock, and persistent storage.
|
Simulation
|
Implement components simulating hardware devices, simulate an irrigation site, and provide views and controls for manipulating the simulation.
|
ID: 25
1.2 Startup Layer Decomposition
The Startup layer contains the main program class, which may be either an application or an applet, assuming the program is written in Java. The main program uses a Configurer, which sets up the program based on the available hardware (real or simulated) and whether it is a simulation or a fielded program, and restores the previous program state. The Configurer reads a configuration specification to determine what sort of hardware (or simulated hardware) the program must use. It reads a persistent store to reset the program state. Figure B-10-3 shows the decomposition of the Startup layer.
ID: 876
Figure B-10-3 Startup Layer Structure
ID: 26
1.2.1 Startup Layer Module Responsibilities
Module
|
Responsibilities
|
AquaLushApplet
|
Ask the Simulation layer to create all GUI components and simulated entities, and create a Configurer that uses these simulation objects to configure the program. Obtain the configuration specification. Call the Configurer to configure the program.
|
AquaLush
|
Create a Configurer that uses real hardware. Obtain the configuration specifications. Call the Configurer to configure the program.
|
Configurer
|
Create and connect runtime components based on configuration specifications and whether the program is a simulation or a fielded product. Restore the state of the system from persistent store.
|
ID: 40
1.2.2 Startup Layer Interface Specifications
ID: 41
1.2.2.1 Services Provided
ID: 42
1.2.2.1.1 Applet initialization
Syntax:
|
init()
|
Pre:
|
None.
|
Post:
|
The applet simulating AquaLush is initialized.
|
ID: 53
1.2.2.1.2 Program execution
Syntax:
|
main( args : String[] )
|
Pre:
|
None.
|
Post:
|
The AquaLush program is set up according to its local configuration and its state is reset to what it was when it was last executing.
|
ID: 64
1.2.2.2 Services Required
ID: 65
Simulation.create()—Create the AquaLush simulation, including GUI components simulating the control panel display, keypad, and screen buttons; the simulated irrigation site with valves and sensors; and simulated time and persistent store objects.
ID: 66
DeviceFactory.createDeviceX()—Create various virtual devices already connected to real or simulated hardware.
ID: 67
DeviceX.setListener()—Add a listener to a keypad or screen button virtual device.
ID: 68
Irrigator.create()—Create irrigation control objects.
ID: 69
Irrigator.addX()—Configure the Irrigator with zones, sensors, and valves, and configure the virtual devices controlling valve and sensor hardware.
ID: 70
Irrigator.restoreState()—Restore the state of the program from persistent store.
ID: 71
UserInterface.create()—Create the user interface.
ID: 72
UserInterface.setDisplay()—Configure the user interface with the virtual display.
ID: 73
Reader.read()—Obtain an input character from the configuration specification.
ID: 74
1.2.3 Startup Layer Design Rationale
The configuration task could have been placed in the applet and application classes, but then there would have been two versions of very similar code. Separating this task into a separate component (the Configurer) that can be reused makes it easier to change and correct as well as faster to implement, though it takes more work to design originally. Note: This component will make use of the Abstract Factory pattern for virtual devices, which we discuss in the next section.
ID: 75
1.3 Simulation Layer Decomposition
The Simulation layer contains components that simulate the entire external environment during a simulation. Five of these are externally visible: a time simulator, a persistent storage simulator, a display device simulator, and sensor and valve simulators. In addition this layer has a façade class that hides other components (such as a simulated keypad, simulated screen buttons, the simulated site, and so forth). These components are pictured in Figure B-10-6.
ID: 877
Figure B-10-6 Simulation Layer Structure
ID: 76
1.3.1 Simulation Layer Module Responsibilities
Module
|
Responsibilities
|
Simulation
|
Simulate hardware devices and the real world, and display the simulation using GUI components.
|
SimStore
|
Simulate a persistent store. A real file will be used if possible; if not, no real persistence will be simulated.
|
SimDisplay
|
Simulate a 16-line-by-40-character monochrome textual display as a GUI component.
|
SimSensor
|
Simulate a moisture sensor.
|
SimValve
|
Simulate an irrigation valve.
|
SimTime
|
Simulate the passage of time. Notify other components of the passage of simulated time.
|
ID: 99
1.3.2 Simulation Layer Interface Specifications
ID: 311
1.3.2.1 Services Provided
All operations with preconditions on parameters throw IllegalArgumentExceptions if the preconditions are violated.
ID: 100
1.3.2.1.1 Create the simulation object
Syntax:
|
create()
|
Pre:
|
None.
|
Post:
|
All simulated entities are created.
|
ID: 113
1.3.2.1.2 Provide simulated persistent store
Syntax:
|
getStore() : SimStore
|
Pre:
|
None.
|
Post:
|
The simulated storage object is returned.
|
ID: 124
1.3.2.1.3 Provide a simulated valve
Syntax:
|
getValve( name : String ) : SimValve
|
Pre:
|
name is not null.
|
Post:
|
The simulated valve whose identifier is name is returned, or null is returned if no such valve exists.
|
ID: 135
1.3.2.1.4 Provide a simulated sensor
Syntax:
|
getSensor( name : String ) : SimSensor
|
Pre:
|
name is not null.
|
Post:
|
The simulated sensor whose identifier is name is returned, or null is returned if no such sensor exists.
|
ID: 146
1.3.2.1.5 Provide the simulated display
Syntax:
|
getDisplay() : SimDisplay
|
Pre:
|
None.
|
Post:
|
The simulated display object is returned.
|
ID: 157
1.3.2.1.6 Register a keypad listener
Syntax:
|
setKeypadListener( l : KeypadListener )
|
Pre:
|
None.
|
Post:
|
KeypadListener l will start to receive notifications of simulated keypad key presses.
|
ID: 168
1.3.2.1.7 Register a screen button listener
Syntax:
|
setScreenButtonListener( l : ScreenButtonListener )
|
Pre:
|
None.
|
Post:
|
ScreenButtonListener l will start to receive notifications of simulated screen button presses.
|
ID: 179
1.3.2.1.8 Record a value in simulated persistent store
Syntax:
|
setData( name : String, value : String )
|
Pre:
|
name and value are not null.
|
Post:
|
The name-value pair is recorded in persistent store. Throws DeviceFailureException if persistent store cannot be read.
|
ID: 190
1.3.2.1.9 Fetch a value from simulated persistent store
Syntax:
|
getData( name : String, default : String ) : String
|
Pre:
|
name is not null.
|
Post:
|
Returns the value associated with the name in persistent store. If no value is associated with name and default is not null, then associates the default with the name and returns it. If no value is associated with the name and the default is null, then returns null. Throws DeviceFailureException if persistent store cannot be read.
|
ID: 201
1.3.2.1.10 Remove data from simulated persistent store
Syntax:
|
removeData( name : String )
|
Pre:
|
name is not null.
|
Post:
|
The name and associated value are removed from persistent store. Throws DeviceFailureException if persistent store cannot be read.
|
ID: 212
1.3.2.1.11 Write to the simulated display
Syntax:
|
write( line : int, str : String )
|
Pre:
|
0 <= line < 16 and str is not null.
|
Post:
|
String str is written to line number line of the simulated display, starting at position 0.
|
ID: 223
1.3.2.1.12 Read a simulated sensor
Syntax:
|
getLevel() : int throws DeviceFailureException
|
Pre:
|
None.
|
Post:
|
The simulated moisture level for the sensor is returned. Throws DeviceFailureException if the sensor has a simulated failure.
|
ID: 234
1.3.2.1.13 Open a simulated valve
Syntax:
|
open() throws DeviceFailureException
|
Pre:
|
None.
|
Post:
|
Opens a simulated valve. Throws DeviceFailureException if the valve has a simulated failure.
|
ID: 245
1.3.2.1.14 Close a simulated valve
Syntax:
|
close() throws DeviceFailureException
|
Pre:
|
None.
|
Post:
|
Closes a simulated valve. Throws DeviceFailureException if the valve has a simulated failure.
|
ID: 256
1.3.2.1.15 Fetch the instance of SimTime
Syntax:
|
SimTime.instance() : SimTime
|
Pre:
|
None.
|
Post:
|
A single instance of the SimTime object will always be returned. The SimTime object always starts off with the current real time, but is stopped.
|
ID: 267
1.3.2.1.16 Get the simulated time
Syntax:
|
getDay() : Day getHour() : int getMinute() : int getSecond() : int
|
Pre:
|
None.
|
Post:
|
Returns the requested component of the simulated time.
|
ID: 278
1.3.2.1.17 Set the simulated time
Syntax:
|
setDay( d : Day ) setHour( h : int ) setMinute( h : int ) setSecond( s : int )
|
Pre:
|
0 <= h < 24 and 0 <= m, s < 60.
|
Post:
|
The simulated time is adjusted as indicated.
|
ID: 289
1.3.2.1.18 Start and stop the simulated time
Syntax:
|
start() stop()
|
Pre:
|
None.
|
Post:
|
The simulated time is paused or resumed. Starting simulated time when it is already going or stopping it when it is already stopped has no effect.
|
ID: 300
1.3.2.1.19 Time notification
Syntax:
|
addObserver( o : Observer )
|
Pre:
|
None.
|
Post:
|
Observer o will be notified when simulated time is not stopped and a simulated second has passed.
|
ID: 312
1.3.2.2 Services Required
The Simulation layer requires the services of a GUI toolkit (such as Java Swing). It requires a Day enumeration type. It also requires a mechanism for regular notification of the passage of time occurring at least once per second and ideally much faster so that simulated time can be sped up.
ID: 313
1.3.2.3 Usage Guide
The Simulation layer is needed only when AquaLush is being simulated. Create it first because it is used in every other layer and in constructing the objects in almost every other layer.
ID: 314
1.3.3 Simulation Layer Design Rationale
Many alternatives considered for the Simulation layer lacked various components or had functionality present in other layers. For example, the persistent store was not initially in this layer until it was realized that an applet may not be allowed to access a host operating system because of security concerns. Eventually it was realized that this layer needs to simulate everything in the environment of the software. As another example, in one alternative the simulation GUI construction process was left partly up to the applet, so that the applet requested a simulated control panel and a simulated environment from the Simulation layer, then combined them to form the applet. The present alternative is more cohesive.
The SimTime class is a singleton class. This was deemed necessary because having more than one simulated time object would certainly cause the program to fail. Once SimTime is made a singleton there is no need for an operation to retrieve it from the Simulation layer façade, the role played by the Simulation class.
SimTime is also a subject in the Observer pattern. If it were an invoker in the Command pattern, then it could have only a single reactor. However, several components in the simulation as well as the ClockDevice in the Device Interface layer need to be notified of the passage of simulated time.
ID: 315
1.4 Device Interface Layer Decomposition
The Device Interface layer provides virtual devices to hide the real or simulated external entities manipulated by the program. Writing the program to interact only with virtual devices makes AquaLush highly configurable. The virtual devices provided in this layer are pictured in Figure B-10-9.
Note that these entities are all interface types except for the Clock and the KeyPress enumeration. This layer also includes private realizations of these interfaces that actually do the work, but these are hidden at the architectural level of detail.
ID: 878
Figure B-10-9 Device Interface Layer Structure
ID: 316
1.4.1 Device Interface Layer Module Responsibilities
Module
|
Responsibilities
|
ClockDevice Interface
|
Provide virtual clock hardware that keeps track of the day of the week and the time of the day, accurate to the minute.
|
TickListener Interface
|
Guarantee that a module listening to the ClockDevice implements a tick() operation, which is needed for the Command pattern.
|
StoreDevice Interface
|
Provide virtual persistent storage of name-value pairs.
|
DisplayDevice Interface
|
Provide a virtual 16-line-by-40-character monochrome textual display device.
|
SensorDevice Interface
|
Provide virtual moisture sensor hardware.
|
ValveDevice Interface
|
Provide virtual irrigation valve hardware.
|
KeypadDevice Interface
|
Provide a virtual 12-key keypad hardware device.
|
KeypadListener Interface
|
Guarantee that a module registered as a Keypad listener implements the keyPress() operation, which is needed for the Command pattern.
|
ScreenButtonDevice Interface
|
Provide a virtual hardware device with eight push buttons.
|
ScreenButtonListener Interface
|
Guarantee that a module registered as a ScreenButton listener implements the screenButtonPress() operation, which is needed for the Command pattern.
|
Clock
|
Provide the day of the week and the time of day, accurate to one minute. Notify observers of the passage of time every minute. The Clock should be used by the rest of the system; it relies on the ClockDevice.
|
DeviceFactory Interface
|
Provide abstract factory methods for creating all virtual devices. This interface is needed as part of the Factory Method pattern.
|
ID: 357
1.4.2 Device Interface Layer Interface Specifications
ID: 358
1.4.2.1 Services Provided
All operations with preconditions on parameters throw IllegalArgumentExceptions if the precondition is violated.
ID: 359
1.4.2.1.1 Set the time of the day (Clock and ClockDevice)
Syntax:
|
setTime( milTime : int )
|
Pre:
|
milTime is a legitimate military time specification.
|
Post:
|
The clock device is reset to milTime.
|
ID: 370
1.4.2.1.2 Get the time of the day (Clock and ClockDevice)
Syntax:
|
getTime() : int
|
Pre:
|
None.
|
Post:
|
The current time is returned, accurate to the minute, in military time format.
|
ID: 381
1.4.2.1.3 Set the day of the week (Clock and ClockDevice)
Syntax:
|
setDay( d : Day )
|
Pre:
|
None.
|
Post:
|
The clock device is reset to day d.
|
ID: 392
1.4.2.1.4 Get the day of the week (Clock and ClockDevice)
Syntax:
|
getDay() : Day
|
Pre:
|
None.
|
Post:
|
The current day of the week is returned.
|
ID: 403
1.4.2.1.5 Set the ClockDevice listener
Syntax:
|
setListener( l : TickListener )
|
Pre:
|
None.
|
Post:
|
TickListener l will start to receive notifications of the passage of time every minute. Any previous TickListener is replaced.
|
ID: 414
1.4.2.1.6 Register a Clock observer
Syntax:
|
addObserver( o : Observer )
|
Pre:
|
o is not null.
|
Post:
|
Observer o will start to receive notifications every minute. Throws an exception if the precondition is violated.
|
ID: 425
1.4.2.1.7 Register a ScreenButton listener
Syntax:
|
setScreenButtonListener( l : ScreenButtonListener )
|
Pre:
|
None.
|
Post:
|
ScreenButtonListener l will start to receive notifications of screen button presses.
|
ID: 436
1.4.2.1.8 Register a Keypad listener
Syntax:
|
setKeypadListener( l : KeypadListener )
|
Pre:
|
None.
|
Post:
|
KeypadListener l will start to receive notifications of keypad button presses.
|
ID: 447
1.4.2.1.9 Record a value in persistent store
Syntax:
|
setData( n : String, v : String )
|
Pre:
|
n and v are not null.
|
Post:
|
The name-value pair is recorded in persistent store.
|
ID: 458
1.4.2.1.10 Fetch a value from persistent store
Syntax:
|
getData( n : String, v : String ) : String
|
Pre:
|
n is not null.
|
Post:
|
Return the value associated with n in persistent store. If no value is associated with n and v is not null, then associate v with n and return v. If no value is associated with n and v is null, return null.
|
ID: 469
1.4.2.1.11 Remove data from persistent store
Syntax:
|
removeData( n : String )
|
Pre:
|
n is not null.
|
Post:
|
Remove the association between n and a value (if any) from persistent store.
|
ID: 480
1.4.2.1.12 Clear the display
Syntax:
|
clear()
|
Pre:
|
None.
|
Post:
|
The display is blanked.
|
ID: 491
1.4.2.1.13 Clear all display highlighting
Syntax:
|
clearHighlight()
|
Pre:
|
None.
|
Post:
|
All highlighted lines are returned to regular display; the text is unaltered.
|
ID: 502
1.4.2.1.14 Clear a line of the display
Syntax:
|
clearLine( line : int )
|
Pre:
|
0 <= line < 16.
|
Post:
|
The text (and highlighting) are removed from the designated line.
|
ID: 513
1.4.2.1.15 Highlight a display line
Syntax:
|
highlight( line : int, col : int, length : int )
|
Pre:
|
0 <= line < 16 and 0 <= col < 40 and 0 <= length.
|
Post:
|
The portion of line starting at the column and extending for the length of the characters (or until the end of the line) is highlighted.
|
ID: 524
1.4.2.1.16 Write a string to the display
Syntax:
|
write( line : int, col : int, str : String )
|
Pre:
|
0 <= line < 16 and 0 <= col <= 40 and str is not null.
|
Post:
|
String str is written to line number line and column col of the simulated display. If str is too long to fit on the line it is truncated to fit. The line is not highlighted.
|
ID: 535
1.4.2.1.17 Write a line to the display
Syntax:
|
writeLine( line : int, str : String )
|
Pre:
|
0 <= line < 16 and str is not null.
|
Post:
|
String str is written to line number line of the simulated display, starting at position 0. If str is more than 40 characters it is truncated to 40 characters. If it is less than 40 characters, then it is padded with blanks until it is 40 characters. The line is not highlighted.
|
ID: 546
1.4.2.1.18 Read a sensor
Syntax:
|
read() : int throws DeviceFailureException
|
Pre:
|
None.
|
Post:
|
Returns the moisture level. Throws DeviceFailureException if the sensor fails.
|
ID: 557
1.4.2.1.19 Open a valve
Syntax:
|
open() throws DeviceFailureException
|
Pre:
|
None.
|
Post:
|
Opens a valve. Throws DeviceFailureException if the valve fails.
|
ID: 568
1.4.2.1.20 Close a valve
Syntax:
|
close() throws DeviceFailureException
|
Pre:
|
None.
|
Post:
|
Closes a valve. Throws DeviceFailureException if the valve fails.
|
ID: 579
1.4.2.1.21 Create virtual devices
Syntax:
|
createSensorDevice( id : String ) : SensorDevice createValveDevice( id : String, rate ) : ValveDevice createDisplayDevice() : DisplayDevice createKeypadDevice() : KeypadDevice createScreenButtonDevice() : ScreenButtonDevice createClockDevice() : ClockDevice createStorageDevice() : StorageDevice
|
Pre:
|
id is not null.
|
Post:
|
The desired virtual device is created. The virtual device is fully configured and connected to real or simulated hardware.
|
ID: 590
1.4.2.2 Services Required
The Device Interface layer uses actual or simulated hardware. The details of these services vary: That is the point of having a device interface layer. This layer also requires a Day enumeration type and a DeviceFailureException class. The ClockDevice requires some sort of real or simulated device that notifies it when one minute has passed.
ID: 591
1.4.2.3 Usage Guide
The Clock, not the ClockDevice, is the main time and time notification service provider. The Clock is a singleton, so it is globally visible. It is also an observable, so it can be accessed from any layer and arbitrarily many observers can register with it for time notifications.
The extra parameter in getData() is there to make it easy to initialize the persistent store when the program is run for the first time. Default persistent values are supplied, and they will be written to the persistent store even if the store did not previously exist.
All sensors, valves, and zones are identified across layers using String identifiers. Hence these must match up and be hooked into whatever mechanism is used to associate virtual devices with real or simulated hardware.
ID: 592
1.4.3 Device Interface Layer Design Rationale
Other versions of this layer did not include the DeviceFactory. The DeviceFactory is part of the Abstract Factory pattern, which allows the details of virtual device configuration to be hidden inside concrete factories and makes it very easy to change the configuration. Design alternatives that include the Abstract Factory pattern are clearly superior.
A question that was revisited several times was how the clock should be handled. Does there need to be a Clock distinct from a ClockDevice? On the one hand, the ClockDevice is supposed to provide a virtual device as an interface to real or simulated hardware, while a Clock provides time and notification services to clients. These are distinct responsibilities, so it appears that there ought to be two entities. On the other hand, in practice the services provided by a Clock are almost identical to those of a ClockDevice, and it is not good to multiply entities beyond necessity. In the end the alternative with a distinct Clock and ClockDevice was chosen to maintain uniformity with the rest of the modules in the Device Interface layer.
Having resolved the issue of whether the Clock is distinct from the ClockDevice, the question of where the Clock should reside arises. On the one hand, the Clock is responsible for providing time and notification services rather than a uniform interface to hardware, suggesting that it should not be in the Device Interface layer. On the other hand, a Clock is a kind of device and it does not fit particularly well in any other layer, so perhaps the best home for it is the Device Interface layer. The latter argument seems slightly more persuasive, so the Clock is in the Device Interface layer. It could be moved easily, however.
ID: 593
1.5 Irrigation Layer Decomposition
The Irrigation layer is the core of AquaLush in the sense that it realizes the program’s main function of controlling irrigation. Its architectural structure is quite simple: It contains a façade module that provides operations for configuration, setting irrigation parameters, and controlling manual irrigation. These operations depend on an enumeration type and several classes that merely package data about the state of the program for return from query functions. The class diagram in Figure B-10-12 illustrates this structure. The classes with the «constant» stereotype are immutable data containers that simply hold values returned by query operations.
ID: 879
Figure B-10-12 Irrigation Layer Structure
ID: 594
1.5.1 Irrigation Layer Module Responsibilities
Module
|
Responsibilities
|
Irrigator
|
Hold irrigation parameters, oversee irrigation cycles, and provide a façade to the rest of the irrigation-layer facilities.
|
Mode
|
Enumeration values for program modes.
|
ZoneReport
|
Record providing data about irrigation zones.
|
ValveReport
|
Record providing data about the state of a valve.
|
SensorReport
|
Record providing data about the state of a sensor.
|
IrrigationReport
|
Record providing data about the state of an irrigation cycle.
|
FailureReport
|
Record containing ValveReport and SensorReport instances for failed valves and sensors.
|
ID: 620
1.5.2 Irrigation Layer Interface Specifications
ID: 621
1.5.2.1 Services Provided
All operations with preconditions on parameters throw IllegalArgumentExceptions if the preconditions are violated.
ID: 622
1.5.2.1.1 Create irrigation objects
Syntax:
|
create( s : StorageDevice )
|
Pre:
|
s is not null.
|
Post:
|
Irrigation-layer objects are created and initialized.
|
ID: 633
1.5.2.1.2 Set and get irrigation parameters
Syntax:
|
setMode( m : Mode ) getMode() : Mode setAllocation( a : int ) getAllocation() : int setIrrigationTime( t : int ) getIrrigationTime() : t setIrrigationDays( s : Set ) getIrrigationDays() : Set
|
Pre:
|
0 < a and t is a valid military time specification and s is a set of Day.
|
Post:
|
The irrigation parameters are set or fetched. Changes are recorded in the persistent store.
|
ID: 644
1.5.2.1.3 Add a zone
Syntax:
|
addZone( id : String, loc : String )
|
Pre:
|
id and loc are not null; id is unique.
|
Post:
|
The layer is configured with a new zone.
|
ID: 655
1.5.2.1.4 Add a sensor
Syntax:
|
addSensor( zid: String, id : String, loc : String, d : SensorDevice )
|
Pre:
|
No parameter is null and the designated zone does not have a sensor.
|
Post:
|
Adds a sensor to the designated zone. Throws an IllegalStateException if the zone already has a sensor.
|
ID: 666
1.5.2.1.5 Add a valve
Syntax:
|
addValve( zid: String, id : String, loc : String, rate : int, d : ValveDevice )
|
Pre:
|
No parameter is null and 0 < rate and the designated zone does not have 32 valves.
|
Post:
|
Adds a sensor to the designated zone. Throws an IllegalStateException if the zone already has 32 valves.
|
ID: 677
1.5.2.1.6 Restore program state
Syntax:
|
restoreState()
|
Pre:
|
Layer configuration is complete.
|
Post:
|
Persistent store is read and the previous state of the program is restored.
|
ID: 688
1.5.2.1.7 Set or get a zone’s critical moisture level
Syntax:
|
setCriticalMoistureLevel( zid : String, level : int ) getCriticalMoistureLevel( zid : String ) : int
|
Pre:
|
zid is not null and names a registered zone and 0 <= level <= 100.
|
Post:
|
The designated zone’s critical moisture level is set or returned and 0 <= result <= 100. The change is recorded in persistent store.
|
ID: 699
1.5.2.1.8 Start and end manual irrigation cycles
Syntax:
|
startManualCycle() endManualCycle()
|
Pre:
|
mode is MANUAL
|
Post:
|
A manual irrigation cycle is started or ended and waterUsed is set to zero when a cycle starts. Throws an IllegalStateException if mode is not MANUAL.
|
ID: 710
1.5.2.1.9 Manually open or close a valve
Syntax:
|
openValve( id : String ) closeValve( id : String )
|
Pre:
|
id is not null and id names a registered valve and mode is MANUAL.
|
Post:
|
A valve is opened or closed. Attempts to open or close failed valves do nothing. Throws an IllegalStateException if mode is not MANUAL.
|
ID: 721
1.5.2.1.10 Obtain irrigation cycle data
Syntax:
|
getIrrigationReport() : IrrigationReport
|
Pre:
|
None.
|
Post:
|
The IrrigationReport is populated and returned.
|
ID: 732
1.5.2.1.11 Get zone data
Syntax:
|
getZoneReports() : Collection getZoneReport( id : String ) : ZoneReport
|
Pre:
|
id is not null and names a registered zone.
|
Post:
|
If no zone is identified, a collection of ZoneReports, one for each registered zone, is populated and returned. If a zone is identified, a report for that zone is populated and returned.
|
ID: 743
1.5.2.1.12 Get sensor data
Syntax:
|
getSensorReports() : Collection getSensorReport( id : String ) : SensorReport
|
Pre:
|
id is not null and names a registered sensor.
|
Post:
|
If no sensor is identified, a collection of SensorReports, one for each registered sensor, is populated and returned. If a sensor is identified, a report for that sensor is populated and returned.
|
ID: 754
1.5.2.1.13 Get valve data
Syntax:
|
getValveReports() : Collection getValveReport( id : String ) : ValveReport
|
Pre:
|
id is not null and names a registered valve.
|
Post:
|
If no valve is identified, a collection of ValveReports, one for each registered valve, is populated and returned. If a valve is identified, a report for that valve is populated and returned.
|
ID: 765
1.5.2.1.14 Get a failed hardware report
Syntax:
|
getFailureReport() : FailureReport
|
Pre:
|
None.
|
Post:
|
A new FailureReport is created and populated. It will contain ValveReports and SensorReports only for failed devices.
|
ID: 776
1.5.2.1.15 Mark a device as repaired
Syntax:
|
repairDevice( id : String )
|
Pre:
|
id is not null and names a registered valve or sensor.
|
Post:
|
The indicated valve or sensor is marked as repaired. The change is recorded in persistent store.
|
ID: 787
1.5.2.2 Services Required
The Irrigation layer uses the following operations from the Device Interface layer: ValveDevice.open(), ValveDevice.close(), SensorDevice.read(), StorageDevice.setData(), StorageDevice.getData(), Clock.getTime(), Clock.setTime(), Clock.getDay(), Clock.setDay(), Clock.addObserver(). The Irrigator is the Clock observer.
ID: 788
1.5.2.3 Usage Guide
The Irrigation layer should be completely configured using the addX() operations before the restoreState() operation is called. The restoreState() operation uses the Irrigation layer configuration to obtain system state values such as device failure states; if the configuration is not complete, these state values will not be set correctly.
The Irrigator registers itself with the Clock, so a client should not do this.
The various report classes are merely containers for groups of values. They are intended to be used by the User Interface layer to populate screens with data about the state of the system, especially during manual irrigation. They are supposed to provide a wide range of information so they will not have to be changed even if the user interface is changed. Note that because the system is changing during irrigation, the values in the report become stale, so they need be retrieved anew every minute or after every change.
ID: 789
1.5.3 Irrigation Layer Design Rationale
There are many design alternatives that expose the internals of the Irrigation layer by making the Zone, Sensor, and Valve classes part of the layer interface. This makes the Irrigator class much simpler and obviates the need for the many report classes used to return data about the configuration, the state of irrigation, and device states. Such alternatives have two problems: By exposing the internal structure of this layer they make it much harder to change in the future because they do not hide information, and they make it much harder to enforce constraints on irrigation behavior. To illustrate this last point, the alternative previously documented does not allow valves to be manually opened or closed unless the mode is automatic. Allowing direct access to valves would make it harder to enforce this constraint. It seems clear that the previous alternative is the best choice because it favors information hiding and control over simplicity.
ID: 790
1.6 User Interface Layer Decomposition
The User Interface layer is responsible for coordinating user interaction with output on a monochrome textual display and input from a 12-key keypad and 8 screen buttons. This layer has a UIController that executes a state machine derived from the user interface dialog map. The controller dispatches input from the keypad, screen buttons, and the clock to the currently active screen.
ID: 880
Figure B-10-15 User Interface Layer Structure
ID: 791
1.6.1 User Interface Layer Module Responsibilities
Module
|
Responsibilities
|
UIController
|
Execute a state machine whose states are screens from the dialog map. Dispatch user input and clock notifications to the current screen.
|
ID: 799
1.6.2 User Interface Layer Interface Specifications
ID: 800
1.6.2.1 Services Provided
All operations with preconditions on parameters throw IllegalArgumentExceptions if the preconditions are violated.
ID: 801
1.6.2.1.1 Create the user interface
Syntax:
|
create( d : DisplayDevice, w : Irrigator )
|
Pre:
|
d and w are not null.
|
Post:
|
The user interface controller is created and the initial screen in the program is activated.
|
ID: 812
1.6.2.2 Services Required
The User Interface layer uses almost all the operations from the Irrigation layer. From the Device Interface layer, it uses all the services provided by the DisplayDevice and the Clock, which it observes. This layer also uses the KeyPress enumeration from the Device Interface layer.
ID: 813
1.6.2.3 Usage Guide
The UIController must be registered as the listener for the KeypadDevice and the ScreenButtonDevice to receive notifications from them. It registers itself with the Clock.
ID: 814
1.6.3 User Interface Layer Design Rationale
Although the intent is for this layer to implement a state machine realizing the user interface dialog map, the architecture description is so general that any sort of implementation is possible: The UIController can be treated as a façade hiding all sorts of alternative implementations. This is perhaps the best reason to prefer this architectural alternative. Other alternatives would provide more detail that would constrain the detailed design alternatives.
ID: 815
1.7 Runtime Components
Figure B-10-18 depicts the runtime configuration of AquaLush.
The Startup component executes first and configures the remaining parts of the program using the data it reads from the AquaLush Configuration data store. The User interacts with the AquaLush Control Panel, which interacts with the User Interface. The User Interface translates the User’s desires into interactions with the Irrigation Control component to set the mode of the program, set program parameters, and control manual irrigation. The Irrigation Control component maintains a copy of its state in the AquaLush State data store and uses it to recover its state when it starts up after loss of power. The Irrigation Control component interacts with the Valves/Sensors component, which reads hardware (or simulated) Sensors and controls hardware (or simulated) Valves.
ID: 816
Figure B-10-18 AquaLush Runtime Structure
ID: 817
2 Mapping Between Models
Table B-10-19 indicates the uses relations between layers and their constituents. The layers in the rows use the classes in the columns.
  |
User Interface
|
Irrigation
|
Device Interface
|
Simulation
|
Startup
|
UIController
|
Irrigator
|
DeviceFactory KeypadDevice ScreenButtonDevice
|
Simulation
|
User Interface
|
  |
Irrigator
|
Clock DisplayDevice
|
  |
Irrigation
|
  |
  |
Clock ValveDevice SensorDevice StorageDevice
|
  |
Device Interface
|
  |
  |
  |
SimTime SimDisplay SimValve SimSensor SimStore
|
ID: 849
The runtime components shown in Figure B-10-18 are comprised of the modules and data stores shown in Table B-10-20.
Runtime Component
|
Modules
|
Startup
|
Startup layer configuration module(s), plus DeviceInterface.DeviceFactory
|
AquaLush Configuration
|
Configuration specification (file or applet parameters)
|
Control Panel
|
DeviceInterface.DisplayDevice, DeviceInterface.KeypadDevice, and DeviceInterface.ScreenButtonDevice; display, keypad, and screen button hardware or Simulation.SimDisplay and a simulated keypad
|
User Interface
|
UserInterface.UIController
|
Irrigation Control
|
The objects in the Irrigation layer, plus DeviceInterface.StorageDevice, DeviceInterface.Clock, DeviceInterface.ClockDevice, Simulation.SimStore, or a configuration file, and Simulation.SimTime or real clock hardware
|
Valves/Sensors
|
DeviceInterface.ValveDevice, DeviceInterface.SensorDevice
|
Valves and Sensors
|
Simulation.SimValve, Simulation.SimSensor, or actual hardware sensors and valves
|
ID: 875
3 Architectural Design Rationale
The layers in this program are rather unusual, but they are arranged as they are to satisfy one of the most important AquaLush quality attributes: configurability. Because the core software, principally the contents of the Irrigation layer, must run unchanged in a simulation or in a fielded product, the program must be configurable so that all interactions with its environment can be changed. This includes things that are usually not considered part of the environment, such as time (because simulated time may be faster, slower, or even stop), and things that are usually taken for granted (such as the persistent store, which may not be accessible in an applet).
There is no reasonable alternative except to have a Device Interface layer. It uses the Simulation layer to provide the entire external environment during a simulation and hardware devices, a file system, and the system clock in a fielded program. In either case, the Device Interface layer shields the rest of the program from having to deal with the differences occasioned by these radically different environments.
User interfaces are usually coded using the user interface toolkit provided by a programming environment. However, since the AquaLush simulation uses a GUI and the fielded program uses ATM-like hardware, the user interface must either be coded in several versions or coded once in a device-independent component that uses virtual devices. The latter alternative is clearly preferable. Furthermore, it makes sense to separate the User Interface layer from the Irrigation layer so that the latter can be unchanged even if the user interface is altered.
Finally, the complexity of the configuration problem suggests the need for a layer responsible for configuring the program at startup. Isolating this task in one place makes it easier to code, test, and modify. The Startup layer is responsible for this task.