An Example of Analysis Patterns: Observation


In this post, I will create an application that implements Martin Fowler’s observation analysis pattern as described in his book, Analysis Patterns: Reusable Object Models.   The book is a great practical book that reflects its author experience in domain modeling.   The only problem of this book is it uses its own custom diagram rather than UML diagram (by that time, UML hasn’t been standardized yet).   It will be great if there is a second edition of the book which uses UML class diagram or object diagram to explain domain models.   I will definitely read it😉

What is analysis pattern?   The book describes analysis patterns as “groups of concepts that represent a common construction in business modeling”.   It is something that helps me to understand business world from software engineering perspective.   Why do I care about analysis pattern?  In small team,  software developers are not only creating source code;  sometimes,  they need to be (or work closely with) domain experts.

For example, I will need to implement this simple requirement:

“The program should records motorcycle’s measurements: mileage (the distance travelled in kilometers), oil level (over-full, ok, or low), etc. Program will also store the date of measurement. Not all measurements will be performed on the same day. User should be able to add new measurement types later (for example, he may want to measure motorcycle’s emission in the future).”

The requirement above can be solved by using observation pattern.   In observation analysis pattern, a quantitative value (such as mileage) is a ‘Measurement’ and a qualitative value (such as oil level) is a ‘Category Observation’.   Both of them have an association with a ‘Phenomenon Type’.

Here is the class diagram that I’m going to implement:

Domain Classes

Domain Classes

Below is a UML object diagram that depicts an example run time state of the class diagram:

Object diagrams

Object diagrams

I will use Griffon and simple-jpa to quickly create an implementation for the design above.

This is the content of Motorcycle.groovy:

package domain

import ...

@DomainModel @Entity @Canonical
class Motorcycle {

    @Size(min=6, max=10)
    String plateNumber

    @OneToMany(cascade=CascadeType.ALL, orphanRemoval=true)
    List<Observation> observations = []

}

Observation class has a DateTime attribute to store the date and time when the observation is performed. If the method by which the observation was performed is required, then a protocol attribute can be added to this class. The following is the content of Observation.groovy:

package domain

import ...

@DomainModel @Entity @Canonical
abstract class Observation {

    @NotNull @Type(type="org.jadira.usertype.dateandtime.joda.PersistentDateTime")
    DateTime dateTime

}

The following is the content of Measurement.groovy:


package domain

import ...

@DomainModel @Entity @Canonical
class Measurement extends Observation {

    @NotNull
    Double quantity

    @NotNull @ManyToOne
    PhenomenonType type

    @Override
    String toString() {
        "${type.name}: ${NumberFormat.getNumberInstance().format(quantity)}"
    }
}

This is the content of CategoryObservation.groovy:

package domain

import ...

@DomainModel @Entity @Canonical
class CategoryObservation extends Observation {

    @NotNull @ManyToOne
    Phenomenon phenomenon

    @Override
    String toString() {
        "${phenomenon.type.name} - ${phenomenon.name}"
    }
}

This is the content of PhenomenonType.groovy:

package domain

import ...

@DomainModel @Entity @Canonical
class PhenomenonType {

    @NotEmpty @Size(min=2, max=50)
    String name

}

The following is the content of Phenomenon.groovy:

package domain

import ...

@DomainModel @Entity @Canonical
class Phenomenon {

    @NotEmpty @Size(min=2, max=50)
    String name

    @NotNull @ManyToOne
    PhenomenonType type

}

The next step is generating MVC based on those domain classes.   simple-jpa will create individual menu per domain class, but user will not need to access all of them.   There should be only menus for Motorcycle, Phenomenon, and PhenomenonType.   I will need to remove unneeded menus from MainGroupView.groovy; their corresponding MVCGroups can also be safely deleted.

Main Menu

Main Menu

Apart from template renderer configuration, “Phenomenon Type” screen is already working.   User can add, edit, or delete a PhenomenonType from this screen.

PhenomenonType Screen

PhenomenonType Screen

“Phenomenon” screen can be considered done too. User can add, edit, or delete a Phenomenon from this screen. Whenever they create a new Phenomenon, they should select one of the PhenomenonType objects.

Phenomenon Screen

Phenomenon Screen

Now, let see “Motorcycle” screen:

Motorcycle Screen

Motorcycle Screen

This screen consists of two MVCGroups: Motorcycle and ObservationAsChild.   Motorcycle is the one shown in the picture above.   ObservationAsChild will be displayed when the user click on the ‘Observations’ button.   Observation is an abstract class whose children are Measurement and CategoryObservation, so user will intuitively expect a form to input both of kinds in ObservationAsChild.   Unfortunately, simple-jpa is not smart enough to create MVCGroup that will allow entering both Measurement and CategoryObservation.   I will need to manually change the code for ObservationAsChild MVCGroup.

The following is the new content of ObservationAsChildView.groovy after modification:

package project

import ...

application(title: 'Observation',
        preferredSize: [520, 340],
        pack: true,
        locationByPlatform: true,
        iconImage: imageIcon('/griffon-icon-48x48.png').image,
        iconImages: [imageIcon('/griffon-icon-48x48.png').image,
                imageIcon('/griffon-icon-32x32.png').image,
                imageIcon('/griffon-icon-16x16.png').image]) {

    panel(id: 'mainPanel') {
        borderLayout()

        panel(constraints: PAGE_START) {
            flowLayout(alignment: FlowLayout.LEADING)
            label("Type: ")
            JComboBox cboTypeFilter = comboBox(model: model.typeFilter, templateRenderer: '${(value instanceof String)?value:value.name}')
            cboTypeFilter.addItemListener(model.observationTypeMatcherEditor)
        }

        panel(constraints: CENTER) {
            borderLayout()
            panel(constraints: PAGE_START, layout: new FlowLayout(FlowLayout.LEADING)) {
                label(text: bind('searchMessage', source: model))
            }
            scrollPane(constraints: CENTER) {
                table(rowSelectionAllowed: true, id: 'table') {
                    eventTableModel(list: model.observationFilterList,
                        columnNames: ['Date Time', 'Description'],
                        columnValues: ["\${value.dateTime.toString('dd-MM-yyyy hh:mm')}",'${value}'])
                    table.selectionModel = model.observationSelection
                }
            }
        }

        panel(id: "form", layout: new MigLayout('hidemode 2', '[right][left][left,grow]',''), constraints: PAGE_END, focusCycleRoot: true) {

            group = buttonGroup()
            label('Type:')
            radioButton(text: "Measurement", buttonGroup: group, constraints: 'split 2',
                selected: bind('measurementType', target: model, mutual: true))
            radioButton(text: "Category Observation", buttonGroup: group, constraints: 'skip, wrap',
                selected: bind('categoryObservationType', target: model, mutual: true))

            label('Date Time:')
	    dateTimePicker(id: 'dateTime', dateTime: bind('dateTime', target: model, mutual: true), errorPath: 'dateTime')
	    errorLabel(path: 'dateTime', constraints: 'wrap')

            label('Quantity:', visible: bind('measurementType', source: model))
            numberTextField(id: 'quantity', columns: 20, bindTo: 'quantity', errorPath: 'quantity',
                visible: bind('measurementType', source: model))
            errorLabel(path: 'quantity', constraints: 'wrap', visible: bind('measurementType', source: model))

            label('Type:', visible: bind('measurementType', source: model))
            comboBox(model: model.type, templateRenderer: '${value.name}', errorPath: 'type',
                visible: bind('measurementType', source: model))
            errorLabel(path: 'type', constraints: 'wrap', visible: bind('measurementType', source: model))

            label('Phenomenon:', visible: bind('categoryObservationType', source: model))
            comboBox(model: model.phenomenon, templateRenderer: '${value.type.name} - ${value.name}', errorPath: 'phenomenon',
                visible: bind('categoryObservationType', source: model))
            errorLabel(path: 'phenomenon', constraints: 'wrap',
                visible: bind('categoryObservationType', source: model))

            panel(constraints: 'span, growx, wrap') {
                flowLayout(alignment: FlowLayout.LEADING)
                button(app.getMessage("simplejpa.dialog.update.button"), actionPerformed: {
                    if (!model.observationSelection.selectionEmpty) {
                        if (JOptionPane.showConfirmDialog(mainPanel, app.getMessage("simplejpa.dialog.update.message"),
                            app.getMessage("simplejpa.dialog.update.title"), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) != JOptionPane.YES_OPTION) {
                                return
                        }
                    }
                    controller.save()
                    form.getFocusTraversalPolicy().getFirstComponent(form).requestFocusInWindow()
                })
                button(app.getMessage("simplejpa.dialog.cancel.button"), visible: bind (source: model.observationSelection,
                    sourceEvent: 'valueChanged', sourceValue: {!model.observationSelection.selectionEmpty}),actionPerformed: model.clear)
                button(app.getMessage("simplejpa.dialog.delete.button"), visible: bind (source: model.observationSelection,
                    sourceEvent: 'valueChanged', sourceValue: {!model.observationSelection.selectionEmpty}), actionPerformed: {
                        if (JOptionPane.showConfirmDialog(mainPanel, app.getMessage("simplejpa.dialog.delete.message"),
                            app.getMessage("simplejpa.dialog.delete.title"), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) == JOptionPane.YES_OPTION) {
                                controller.delete()
                        }
                })
                button(app.getMessage("simplejpa.dialog.close.button"), actionPerformed: {
                    SwingUtilities.getWindowAncestor(mainPanel)?.dispose()
                })
            }
        }
    }
}

The following is the new content of ObservationAsChildController.groovy after modification:

package project

import ...

@SimpleJpaTransaction
class ObservationAsChildController {

    def model
    def view

    void mvcGroupInit(Map args) {
        args.'parentList'.each { model.observationList << it }
        listAll()
    }

    void mvcGroupDestroy() {
        destroyEntityManager()
    }

    def listAll = {
        execInsideUIAsync {
            model.typeList.clear()
            model.typeFilterList.clear()
            model.phenomenonList.clear()
        }

        List typeResult = findAllPhenomenonType()
        List phenomenonResult = findAllPhenomenon()

        execInsideUIAsync {
            model.phenomenonList.addAll(phenomenonResult)
            model.typeList.addAll(typeResult)
            model.typeFilterList.add("All")
            model.typeFilterList.addAll(typeResult)
            model.typeFilter.setSelectedItem("All")
        }
    }

    def save = {
        Observation observation
        if (model.measurementType) {
            observation = new Measurement(dateTime: model.dateTime, quantity: model.quantity, type: model.type.selectedItem)
        } else if (model.categoryObservationType) {
            observation = new CategoryObservation(dateTime: model.dateTime, phenomenon: model.phenomenon.selectedItem)
        } else {
            throw new Exception("Unknown observation!")
        }
        if (!validate(observation)) return_failed()

        if (model.observationSelection.selectionEmpty) {
            // Insert operation
            execInsideUIAsync { model.observationList << observation }
        } else {
            // Update operation
            execInsideUISync { model.observationList.set(model.observationSelection.minSelectionIndex, observation) }
        }
        execInsideUIAsync { model.clear() }
    }

    def delete = {
        Observation observation = model.observationSelection.selected[0]
        execInsideUIAsync { model.observationList.remove(observation) }
    }

} 

The following is the new content of ObservationAsChildModel.groovy after modification:

package project

import ...

class ObservationAsChildModel {

    def view

    @Bindable Long id
    @Bindable DateTime dateTime
    @Bindable boolean measurementType
    @Bindable Double quantity
    @Bindable boolean categoryObservationType

    TypeMatcher observationTypeMatcher = new TypeMatcher()
    MatcherEditor observationTypeMatcherEditor = new TypeMatcherEditor()

    BasicEventList<Observation> observationList = new BasicEventList<>()
    FilterList<Observation> observationFilterList = new FilterList<>(observationList, observationTypeMatcherEditor)
    DefaultEventSelectionModel<Observation> observationSelection =
        GlazedListsSwing.eventSelectionModelWithThreadProxyList(observationFilterList)

    BasicEventList<PhenomenonType> typeList = new BasicEventList<>()
    @Bindable DefaultEventComboBoxModel<PhenomenonType> type =
        GlazedListsSwing.eventComboBoxModelWithThreadProxyList(typeList)

    BasicEventList<PhenomenonType> typeFilterList = new BasicEventList<>()
    @Bindable DefaultEventComboBoxModel<PhenomenonType> typeFilter =
        GlazedListsSwing.eventComboBoxModelWithThreadProxyList(typeFilterList)


    BasicEventList<Phenomenon> phenomenonList = new BasicEventList<>()
    @Bindable DefaultEventComboBoxModel<Phenomenon> phenomenon =
        GlazedListsSwing.eventComboBoxModelWithThreadProxyList(phenomenonList)

    public ObservationAsChildModel() {

        observationSelection.valueChanged = { ListSelectionEvent event ->
            if (observationSelection.isSelectionEmpty()) {
                clear()
            } else {

                Observation selected = observationSelection.selected[0]
                errors.clear()
                id = selected.id
		dateTime = selected.dateTime

                if (selected instanceof Measurement) {
                    measurementType = true
                    quantity = selected.quantity
                    type.selectedItem = selected.type
                } else if (selected instanceof CategoryObservation) {
                    categoryObservationType = true
                    phenomenon.selectedItem = selected.phenomenon
                }

            }
        }
    }

    def clear = {
        id = null
        dateTime = null

        errors.clear()
        quantity = null
        type.selectedItem = null
        phenomenon.selectedItem = null
        observationSelection.clearSelection()
        measurementType = false
        categoryObservationType = false
        view.group.clearSelection()
    }

    class TypeMatcher implements Matcher {

        @Override
        boolean matches(Object o) {
            if (typeFilter.selectedItem == "All") return true
            if (!o) return false
            if (observationList.isEmpty()) return true
            if (!typeFilter.selectedItem) return true

            if (o instanceof Measurement) {
                return ((Measurement) o).type == typeFilter.selectedItem
            } else if (o instanceof CategoryObservation) {
                return ((CategoryObservation) o).phenomenon.type == typeFilter.selectedItem
            }
        }

    }

    class TypeMatcherEditor extends AbstractMatcherEditor implements ItemListener {

        @Override
        void itemStateChanged(ItemEvent e) {
            if (e.stateChange==ItemEvent.SELECTED) fireChanged(observationTypeMatcher)
        }

    }

}

ObservationAsChildView now has JRadioButton to indicate what kind of observation that will be saved.  If user clicks on “Measurement” JRadioButton, he will be able to add a quantity and select a PhenomenonType.   If user clicks on “Category Observation” JRadioButton, he will be able to select a Phenomenon (qualitative value).

Add Observations With JRadioButton

Add Observations With JRadioButton

Clicking a JRadioButton will display correspondings components

Clicking a JRadioButton will display correspondings components

In a real world application, observations will often be performed periodically, so there will be a lot of observations for a Motorcycle.   On some occasions, user may want to quickly display certain PhenomenonType for a Motorcycle.   That’s the reason why I use FilterList (from GlazedLists) for JTable in ObservationAsChildView.   By using FilterList, I can filter table’s contents based on a Matcher.

PhenomenonType filter

PhenomenonType filter

Selecting "Mileage" will only display mileage's observation

Selecting “Mileage” will only display mileage’s observation

Selecting "Oil Level" will only display oil level's observations

Selecting “Oil Level” will only display oil level’s observations

Phenomenon and PhenomenonType are knowledge level objects which are used by the others (operational level objects).   This design will allow user to easily add new measurement types later.   For example, I will add a new PhenomenonType called emission as shown in the picture below:

Add new PhenomenonType

Add new PhenomenonType

This implementation of observation analysis pattern allows me to add the new emission measurement to all motorcycles without changing any source code:

New PhenomenonType is ready to use

New PhenomenonType is ready to use

Perihal Solid Snake
I'm nothing...

4 Responses to An Example of Analysis Patterns: Observation

  1. Komang Hendra Santosa mengatakan:

    mas, yg ini sudah saya coba, tp kenapa ya ga bs abstract classnya dipakai? saya pakai simple-jpa-0.4.1 tp kok ga mau ya?

    • Solid Snake mengatakan:

      Abstract class adalah class yang tidak boleh dibuat instance-nya secara langsung, melainkan harus melalui turunannya. Pada post ini, Observation adalah abstract class. Kita harus memilih memakai salah satu turunannya: Measurement atau CategoryObservation. Karena hanya developer yang tahu mana yang dipakai, maka hasil scaffolding dari simple-jpa yang berhubungan dengan abstract class tersebut wajib diubah secara manual. Hal ini terlihat di kode program di atas untuk ObservationAsChildController.groovy di baris 40-46:

              Observation observation
              if (model.measurementType) {
                  observation = new Measurement(dateTime: model.dateTime, quantity: model.quantity, type: model.type.selectedItem)
              } else if (model.categoryObservationType) {
                  observation = new CategoryObservation(dateTime: model.dateTime, phenomenon: model.phenomenon.selectedItem)
              } else {
                  throw new Exception("Unknown observation!")
              }
      

      Bila telah memakai kode program di atas tetapi masih salah, coba post kode program dan Exception yang terjadi.

  2. Komang Hendra Santosa mengatakan:

    saya udah pakai yg sperti diatas mas, tp muncul exception spt ini:

    [griffonc] org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
    [griffonc] D:\latihan\observation\griffon-app\controllers\project\ObservationController.groovy: 48: You cannot create an instance from the abstract class ‘domain.Observation’.
    [griffonc] @ line 48, column 35.
    [griffonc] Observation observation = new Observation(‘dateTime’: model.dateTime)
    [griffonc] ^
    [griffonc]
    [griffonc] 1 error
    Compilation error: Compilation Failed

    Process finished with exit code 1

  3. Komang Hendra Santosa mengatakan:

    Ah, ternyata tinggal tambahin skrip diatas di ObservationController save methodnya, hehe..

Apa komentar Anda?

Please log in using one of these methods to post your comment:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s

%d blogger menyukai ini: