Sonatype

Previous: Chapter 3
Next: Chapter 4

A place for everything and everything in its place. -- Victorian Proverb

This chapter follows an example project. You may wish to download killerapp and follow along.

The POM

Maven, as a project management system, is based upon two major concepts. The first is the concept that all projects are best represented as objects, or things, or nouns. We speak of a project, or the project as a thing. Speach is a manifestation of existing conceptualizations, so we feel it best to actually represent a project as an "object", since the majority of organizations think of them in that way anyway. This is opposed to Ant which has no such project declaration, since all of its builds are limited strictly to only the second of the two concepts - actions - which it calls "tasks". The second concept is that everything else that is not objectifiable is a goal, or an action, or verb. You compile a project, or deploy a project. Since you already do things to a project, it makes sense that these should remain exclusively as actions. Moreover, those "things" you want done to the project are more than just normal actions, they are goals you wish to achieve. They are not simply tasks to be done, but some sort of transformation using some aspect of the project or build system to achieve. Thus, we call them "goals". This chapter focuses on the first of these two concepts: the object.

The POM is the conceptual project, reified.

A small model of the POM

Maven lumps all pieces of a project into a single conceptually convenient Project Object Model, or POM. Each project contains one and only one pom.xml file that represents the project in a declarative manner. It defines things like the project's name, the developers who work on the project, the url for the project on the web, the coordinates of the project, and everything else specific to the project. This is important to note, because there is more information in Maven than just the single POM, for example, the settings.xml files which define information for the build system as a whole - such as server authentication data. For now we will stay focused on the POM.

If you navigate to the killerapp-api file of the Killer App hierarchy you downloaded in the end of the previous chapter, you will notice a pom.xml file. Actually, you will notice many POM files throughout the directory structures. Each of these POMs represent a project. Maven 2.x is represented by POM version 4.0.0. Maven 1.x used version 3.0, which itself was incremented from two previous versions.

The killerapp-stores pom.xml file is as follows:

<project>
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>com.training.killerapp</groupId>
    <artifactId>killerapp</artifactId>
    <version>1.0-SNAPSHOT</version>
  </parent>
  <artifactId>killerapp-stores</artifactId>
  <name>Killer App Stores</name>
  <packaging>pom</packaging>

  <dependencies>
    <dependency>
      <groupId>com.training.killerapp</groupId>
      <artifactId>killerapp-api</artifactId>
    </dependency>
  </dependencies>

  <modules>
    <module>killerapp-store-memory</module>
    <module>killerapp-store-xstream</module>   
  </modules>

</project>

It is a fairly simple pom, only slightly more complex than the one created in the previous chapter via the archetype:create goal. You will notice that there are three different places where "artifactId" is used: under the "parent" element, under the "dependency" element, and the artifactId of this POM itself under the "project" element. There are also elements called "module"s that look suspiciously like artifactIds. This POM represents all of the major POM relationship types in Maven: Dependencies, multi-modules, and Inheritence. The glues that holds these methods of relating together is Maven's coordinate system.

Seperation of Concerns

A good practice to follow for maintaining system integrity, which is the the true role of an architect (don't let anyone tell you differently). Maven helps guide your project design into a good best-practice seperation of concerns (SoC).

More on Coordinates

We mentioned coordinates in Chapter 2. They are mentioned again here because of their central importance to Maven's ability to manage project relationships. Coordinates are Maven's way of tracking a specific project, like an address, but with a temporal aspect. More than pinpointing a specific project, Maven can pinpoint a specific project in time by its "version" element. Coordinates are used by dependency, plugin and extension elements, as well as for defining inheritence, and anywhere else the project or goal configuration may need to know information about including - or excluding - a specific project.

If we go up to the parent pom.xml file of the Killer App project you will see the coordinates peppered throughout the file. Most obvious is the project's own required coordinate definition:

<project>
  <groupId>com.training.killerapp</groupId>
  <artifactId>killerapp</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>pom</packaging>
  ...
</project>

The version element is always required in some capacity. If not specifically specified where you see it, it is likely specified in a parent project, or some sort of management meta-element, or may be inferred through a plugin. The other element you will notice is the packaging element. This tells Maven that the artifact created by this project is a single file: the POM - albeit a possibly annotated version of it. The default packaging type is a jar, and so the default coordinate assumes this is the case.

Whereas the my-app project of the previous chapter had the coordinates mavenbook:my-app:jar:1.0-SNAPSHOT this project is represented as com.training.killerapp:killerapp:pom:1.0-SNAPSHOT.

How does Maven use these coordinates? In multiple ways. Firstly, it uses them to retrieve the specific projects from remote and local repositories. It also uses coordinates to decifer how project hierarchies fit together, and plugins use them for a wide array of things, such as the maven-war-plugin, that uses coordinates to decide what projects to package into the war.

If you have run mvn install on this project, you may wonder what it means to "install" a project. In Maven, installing a project means placing a copy of the project's POM and the generated artifact (in jar packaging type projects, the artifact is a JAR; in pom projects, the artifact is only the POM, etc.) to a local directory structure mirroring the groupId, artifactId and version of the coordinate. The default location for your local repository is .m2/repository under your base user directory (retrieved from Java's "user.home" property) represented as ${user.home}. Each artifact is under its groupId - with dot-notation seperated into directory hierarchy. This is similar to Java's packaging structure. The groupId is followed by the artifactId, followed by version. The POM file can be found under this directory, named as the artifactId-version.pom.

In general, the project's packaged artifact lives with its pom, resulting in the following two files (not including optional checksum files, like md5):

groupId/artifactId/version/artifactId-version.pom
groupId/artifactId/version/artifactId-version.packaging

For our example, the "killerapp" project you have installed above will reside under:

${user.home}/.m2/repository/com/training/killerapp/killerapp/1.0-SNAPSHOT/killerapp-1.0-SNAPSHOT.pom

Since it is a pom type project, the pom.xml file is its own artifact. The killerapp-api project, on the other hand, it a jar project, thus yeilding the following files installed into the local repository:

${user.home}/.m2/repository/com/training/killerapp/killerapp-api/1.0-SNAPSHOT/killerapp-api-1.0-SNAPSHOT.jar
${user.home}/.m2/repository/com/training/killerapp/killerapp-api/1.0-SNAPSHOT/killerapp-api-1.0-SNAPSHOT.pom

We cover local and remote repositories with more depth in Chapter 13. For now simply note the relationship between project coordinates and where they are installed.

Project Relationships

One powerful aspect of Maven is in its handling of project relationships; that includes dependencies (and transitive dependencies), inheritance, and multi-module projects. Dependency management has a long tradition of being a complicated mess for anything but the most trivial of projects. "Jarmageddon" quickly ensues as the dependency tree becomes large and complicated. "Jar Hell" follows, where versions of dependencies on one system are not equivalent to versions as those developed with, either by the wrong version given, or conflicting versions between similarly named jars. Maven solves both problems through a common local repository from which to link projects correctly, versions and all.

Dependencies

The most immediately useful piece of Maven's project relationship mechanism is project dependencies. A dependency is any project that is required by this project to perform some function. That function could be compilation, testing or simply runtime.

Before Maven most build systems would require the build user to provide the dependencies of a project on their own. A common method of dealing with this problem was to check in required binaries to version control, which has problems of its own, wasting a significant amount of bandwidth and disk space. Typically you would want the jars to be contained by your tree so that they can be versioned along with it. This may mean that each development branch on a machine has a copy of those jars. It also means they are checked for diffs each time a commit is done. Since they rarely change, another common pattern is to locate the jars in a tree separate from the source. This works around the problems mentioned above, but now you lose reproducibility over time. Another common problem is that the jars aren't versioned in their file name so it's difficult to determine what versions are actually present. These /lib folders tend to become dumping grounds for jars over time that are difficult to keep in sync with the actual dependencies used.

A Chain of Project Dependencies

Most of the projects in the killerapp have dependencies. The dependency list of the parent killerapp project is as follows:

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

Simple! This specifies the coordinates for a project that the killerapp depends upon, as well as the scope of the dependency. Notice that the packaging type is not specified. That is because, like the POM definition itself, the type defaults to "jar" when not specified. Note that junit is required for testing only. That means that for the project compilation or normal runtime, junit is not necessary. However, the project build will only succeed with junit available in the repository, since that would cause the test to fail. The valid scope types are:

  • compile - this is the default scope, used if none is specified. Compile dependencies are available in all classpaths.
  • provided - this is much like compile, but indicates you expect the JDK or a container to provide it. It is only available on the compilation classpath (not runtime), is not transitive, nor is it packaged.
  • runtime - this scope indicates that the dependency is not required for compilation, but is for execution. It is in the runtime and test classpaths, but not the compile classpath.
  • test - this scope indicates that the dependency is not required for normal use of the application, and is only available for the test compilation and execution phases.
  • system - this scope is similar to provided except that you have to provide the JAR which contains it explicitly. The artifact is always available and is not looked up in a repository. If you declare the scope to be system you must also provide the systemPath element. Note that this scope is not recommended, and will likely be removed in future versions of Maven (it is always desired that libraries are in the Maven repository).

Transitive Dependencies

Each of the scope options in the above list affects more than just the scope of the dependency in the declaring project, but also how it acts as a transitive dependency (when this project is itself a dependency of another project). The easiest way to convey this information is through a table. Scopes in the top row represent a dependency-of-a-dependency's scope (transative dependency), while the scopes in the left represent the direct dependency's scope (non-transative) in ths current project. Where they intersect is the scope given to the transative dependency in the current project. Blank on the table means the dependency will be omitted.

compile provided runtime test
compile compile - runtime -
provided provided provided provided -
runtime runtime - runtime -
test test - test -

Conflict Resolution

If you open the killerapp-cli you will notice a more complex dependency definition.

    <dependency>
      <groupId>com.training.killerapp</groupId>
      <artifactId>killerapp-core</artifactId>
      <version>${project.version}</version>
      <classifier>memory</classifier>
      <exclusions>
        <exclusion>
          <groupId>com.training.killerapp</groupId>
          <artifactId>killerapp-store-xstream</artifactId>
        </exclusion>
      </exclusions>
    </dependency>

This dependency contains an exclusion. An exclusion is a transitive dependency that this project does not wish to require.

By default - unless that project defines its dependencies as "optional" - transative dependencies resolve to the scope following the table above. This element supresses that behavior. In the case of the above definition, the killerapp-core project depends upon the killerapp-store-xstream project (check the killerapp-core POM if you like). The killerapp-cli project, however, does not wish to require that dependency, so it is excluded.

Here are a few other reasons you may wish to exclude transitive dependencies.

  1. The groupId or artifactId of the artifact has changed, where the current project requires an alternately named version from a dependency's version - resulting in 2 copies of a differently versioned project in the classpath.
  2. An artifact that is unused (although the dependency should have declared this optional in it's own POM).
  3. An artifact which is provided by your runtime container thus should not be included with your build
  4. An API which might have multiple vendors -- ie Sun API which requires click-wrap licensing and manual install into repo vs CDDL/GNU licensed version of the same API which is freely available in the repo.

Versions

Versions are more than just a sequence of numbers, but codifiers of information. They describe an evolving software project through time. In Maven the version number are parsed in a simple manner:

major.minor.bug_fix-qualifier-build_number

for example:

1.3.5-alpha-1

When comparing verion numbers: major is resolved before minor, then bug_fix. The qualifier is merely compared as a string (luckly for us, alpha is before beta, which is before releasecandidate or RC). Finally if all previous versions match, the build_number is compared numerically. Finally, if the version cannot be parsed, they are compared entirely as strings.

SNAPSHOT

SNAPSHOT is a special quality that can be appended to the end of a version. It describes to both the user and the Maven system that this project is currently in development, and should be treated as such. You may deploy a specific version of a SNAPSHOT to a remote repository, but users must specifically enable the ability to download snapshots to their projects. This helps keep in development SNAPSHOTs seperate from released versions. Read more about it in The Maven Repository chapter.

1.3.5-alpha-1-SNAPSHOT

One last note: when releasing a project you must resolve all -SNAPSHOT versions. The practical reason for this is that you do not wish your users to require on development versions of a project if they are expecting a release-quality item. But functionally, for the reason given above, by default Maven does not download SNAPSHOT versions from repositories - so having a release version depending upon SNAPSHOT versioned dependencies will make those dependencies inaccessible by default.

Ranges

Versions can be matched by more than a scalar values. They can be given a range of possible values - even with no end. You can do this by using version ranges.

  • (, ) - exclusive quantifiers
  • [, ] - inclusive quantifiers

For example, if you wished to access any JUnit version greater than or equal to 3.8, but less than 4.0, your dependency would be thus:

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>[3.8,4.0)</version>
      <scope>test</scope>
    </dependency>

Note that a version before or after the comma is no required, and means +/- infinity, respectively. [4.0,) means any version greater than or equal to 4.0. (,2.0) is any version less than 2.0. [1.2] means only version 1.2, and nothing else.

Note on Properties

The other thing you will notice is the version is not specifically given, but instead given as a Maven property. Maven properties occur frequently in advanced Maven usage, and are similar to properties in other systems such as Ant. They are simply variables delimited by ${...}. Just to break on a short tangent, let us quickly cover the various types of properties available in the Maven POMs:

  • env.X: Prefixing a variable with "env." will return the shell’s environment variable. For example, ${env.PATH} contains the $PATH environment variable - or %PATH% in Windows.
  • project.x: A dot (.) notated path in the POM will contain the corresponding element's value. For example: <project><groupId>org.apache.maven</groupId></project> is accessed as ${project.groupId}.
  • settings.x: A dot (.) notated path in the settings.xml will contain the corresponding element'€™s value. For example: <settings><offline>false</offline></settings> is accessible via ${settings.offline}. More on the setttings file in a later chapter.
  • Java System Properties: All properties accessible via java.lang.System.getProperties() are available as POM properties, such as ${java.home}.
  • x: Set within a <properties \/> element or an external files, the value may be used as ${x}. This is the simplest and most straight-forward property.

You can find more about properties in Appendix 3.

Finally we mention the classifier element. We will cover how to create artifacts with classifiers later in this book, however, for now just note that those elements which have classifiers may be accessed by specifying this element.

Multi-module

Multi-module projects are pom type projects that contain a list of modules to build. Simple, elegant, and filled with vast potential, multi-module projects are always of packaging type "pom". This is because they do not (normally) produce artifacts of their own, but exist merely to group together other projects - and sometimes other multi-modules. The killerapp:killerapp:pom:1.0-SNAPSHOT project is a multi-module project since it contains a list of modules that can be managed as a group. We have already touched on the power of these projects when we ran "mvn install" under the base directory. When the Maven commandline printed out the following list:

[INFO] Scanning for projects...
[INFO] Reactor build order:
[INFO]   Killer App
[INFO]   Killer App Model
[INFO]   Killer App API
[INFO]   Killer App Stores
[INFO]   Killer App Memory Store
[INFO]   Killer App XStream Store
[INFO]   Killer App Core
[INFO]   Killer App Commandline Interface
[INFO]   Killer App Web WAR

It discovered the list because they were added as modules, and those modules were the names of directories immediately under the current POM ${basedir} (the current project's base directory). The Killer App Stores project is also a multi-module - manages Memory and XStream Stores.

A Multi-Module Project and its Modules

Note that we call the projects under the multi-module projects "modules" and not "children" or "child projects". This is purposeful, so as not to confuse projects grouped by multi-module project with another type of project relationship: inheritence.

Inheritence

Under the killerapp-api project type the following command:

 mvn help:effective-pom

Comparing that pom.xml to the contents of the killerapp-api's pom.xml should yield surprising results. Mailing lists? JUnit and killerapp-model version in the dependency list? And other information we have not encountered before, such as the build element and child elements. Were did all of this data come from? Peeking at the parent killerapp project should yield some answers. The killerapp-model (and, indeed, all of the projects in the Killer App) inherit from the com.training.killerapp:killerapp project, declared in each POM via the parent element.

  <parent>
    <groupId>com.training.killerapp</groupId>
    <artifactId>killerapp</artifactId>
    <version>1.0-SNAPSHOT</version>
  </parent>
A Parent Project that is Inherited From

Remember above we mentioned that groupId/artifactId/versionId are required in some capacity? The killerapp-api POM does not specify a groupId or version. It inherits those values from the parent. Maven assumes that the parent is either installed into the local repository, or available in the parent directory (../pom.xml) of the current project. If neither is true (for example, some organizations prefer to put the parent project in its own directory) this default may be overridden via the relativePath - for example: ../parent-project/pom.xml.

  • dependencies
  • developers and contributors
  • plugin lists
  • reports lists
  • plugin executions with matching ids
  • plugin configuration

Oftentimes a set of projects will require similar values for the set. Just like the inheritence of any other object oriented system (of which Maven is one... it is the Project Object Model afterall) child projects will inherit many of a parent projects values.

The Ultimate Parent

All POMs ultimately inherit from the SuperPOM. The SuperPOM plays a similar role to Java's java.lang.Object class. It is a set of pre-defined values from which all other POMs minimally inherit. This is Convention over Configuration at work. Since there are several values required for proper POM operation, and most of these values are similar for many projects, they are set as the default values of all POMs. The specific values of the SuperPOM can be found in Appendix A. Some important values to note from the SuperPOM are under the "build" element.

The build element defines information directly concerning a project's build settings (as opposed to, say, the project's name - like "Killer App API"). Up until now we have built the projects without concern for how Maven knows where to look for a project's files, and how it knows where to build and place its artifact. This information is configurable by any Maven project, but inherits certain defaults from the Maven SuperPOM. Any directories which are not absolute (begins with a / in unix environment, a driver letter like C: in windows) are assumed to be relative to the project's base directory (or ${basedir}.

<project>
  ...
  <build>
    <!-- the base directory where build artifacts are placed -->
    <directory>target</directory>

    <!-- where compiled files are placed -->
    <outputDirectory>target/classes</outputDirectory>

    <!-- The name of the final artifact sans extension (or possible classifiers) -->
    <finalName>${artifactId}-${version}</finalName>

    <!-- where compiled test files are placed -->
    <testOutputDirectory>target/test-classes</testOutputDirectory>

    <!-- Where the Java source files are expected to be -->
    <sourceDirectory>src/main/java</sourceDirectory>

    <!-- Where non-Java script source files are expected to be -->
    <scriptSourceDirectory>src/main/scripts</scriptSourceDirectory>

    <!-- Where the test Java source files are expected to be -->
    <testSourceDirectory>src/test/java</testSourceDirectory>
    <resources>
      <resource>
        <!-- Where non-compiled files reside (such as .properties file). They are put into the outputDirectory -->
        <directory>src/main/resources</directory>
      </resource>
    </resources>
    <testResources>
      <testResource>
        <!-- Where non-compiled files reside. They are put into the testOutputDirectory -->
        <directory>src/test/resources</directory>
      </testResource>
    </testResources>
  </build>
  ...
</project>
The SuperPOM is always the base Parent

There is more to being a parent than just defining defaults for all of its children to inherit. Sometimes the parent needs to be able to manage a child's settings if it requires them at all. Enter the dependencyManagement element.

Dependency Management

You may notice in the com.training.killerapp:killerapp pom.xml file the dependencyManagement definition contains its own dependencies element. This is not redundency on the part of the POM schema, but a mechanism for managing child the dependencies of any child project that require them. Like good parents, parent POMs sometimes needs to manage details for its children. The following is a piece of the dependencyManagement element from the pom.xml file.

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>com.training.killerapp</groupId>
        <artifactId>killerapp-model</artifactId>
        <version>${project.version}</version>
      </dependency>
      ...
    <dependencies>
  </dependencyManagement>

Unlike the junit dependency defined in the "Dependencies" section in this chapter, this definition does not actually add the killerapp-model dependency to the killerapp project and its children. Meaning, that any children that inherit from this project will not require the killerapp-model project. Instead, this merely "manages" any matching dependencies that this project or its children may have. Look at the killerapp-api's pom file and you will notice that it does not contain a version - although as mentioned above, version is required. Thanks to the dependencyManagement definition the version has already been specified for all of its children.

  • dependencyManagement - is often used by parent POMs to help manage dependency information across all of its children. If the my-parent project uses dependencyManagement to define a dependency on junit:junit:4.0, then POMs inheriting from this one can set their dependency giving the groupId=junit and artifactId=junit only, then Maven will fill in the version set by the parent. The benefits of this method are obvious. Dependency details can be set in one central location, which will propagate to all inheriting child POMs.

Multi-module versus Inheritance

To re-tread on the previous field, there is a difference between inheriting from a parent project, and being managed by an multi-module project. A parent project is one that passes its values to its children. This is different from a multi-module project, where it merely manages a group of other sub-project or module - however no values are actually passed to the modules it manages. Because of this disconnect - and it is commonly desired that a single project both manage a group of projects as well as set values they they can each utilize - a parent project will commonly contain its children as modules (such as killerapp and killerapp-stores). This is not to believe that inheritence and multi-modules are related - so do not be confused - they are merely complimentary - yet a best practice to follow for simplicity and cohesion (only one top-level "master" project).

All Together

Maven's goal is to make project management conceptually simpler. When we put the projects above together we get the following.

Project Relationships Overall

There are more complex pieces for certain, for example inheriting dependencies, or dependency scope. However, understanding the above graph will take you a long way to understanding Maven's project relationship combinations.

Tips and Tricks

Comprehensive Overview

You can find more details about the POM 4.0.0 structure in Appendix 1.

Properties

Properties are an excellent way of helping to "future-proof" your POM - especially in combination with a Parent POM. Check out the Tips and Tricks section in Appendix 3: Properties: Synchronize versions/groups with an Inherited Property.

Dependency Groupers

This is a simple trick used to keep a set of dependencies grouped together. For example, the Microsoft SQLServer JDBC implementation which consists of three jars: msbase.jar, msutil.jar, and mssqlserver.jar. The following example assumes that you have a repository containing these jars, grouped under "com.microsoft":

<project>
  <groupId>com.mycompany</groupId>
  <artifactId>jdbc</artifactId>
  <version>1.0</version>
  <packaging>pom</packaging>
  <dependencies>
    <dependency>
      <groupId>com.microsoft</groupId>
      <artifactId>msbase</artifactId>
      <version>${msJdbcVersion}</version>
    </dependency>
    <dependency>
      <groupId>com.microsoft</groupId>
      <artifactId>msutil</artifactId>
      <version>${msJdbcVersion}</version>
    </dependency>
    <dependency>
      <groupId>com.microsoft</groupId>
      <artifactId>mssqlserver</artifactId>
      <version>${msJdbcVersion}</version>
    </dependency>
  </dependencies>
  <properties>
    <msJdbcVersion>(1.0,)</msJdbcVersion>
  </properties>
</project>

Install the above project - since it's packaging type is "pom" rather than something like "jar", this file alone is placed into the repository. You can now add this project as a dependency (do not forget to add the "pom" type) and all of it's dependencies will be added to your project - the power of transitive dependencies at work!

<project>
  <description>This is a project requiring JDBC</description>
  ...
  <dependencies>
    ...
    <dependency>
      <groupId>com.mycompany</groupId>
      <artifactId>jdbc</artifactId>
      <version>1.0</version>
      <type>pom</type>
    </dependency>
  </dependencies>
</project>

If you later decide to switch to a different JDBC driver, for example, JTDS, just replace the dependencies in the com.mycompany:jdbc project to use net.sourceforge.jtds:jtds and update the version. All of your dependant projects will use jtds if they decide to update to the newer version. It is an easy way to roll out mass changes without breaking anyone unexpectedly.

<project>
  <groupId>com.mycompany</groupId>
  <artifactId>jdbc</artifactId>
  <version>1.0</version>
  <packaging>pom</packaging>
  <dependencies>
    <dependency>
      <groupId>net.sourceforge.jtds</groupId>
      <artifactId>jtds</artifactId>
      <version>1.2</version>
    </dependency>
  </dependencies>
</project>

Summary

This chapter we covered the different ways in which Maven projects relate to each other: as dependencies, or as hierarchies via inheritence or multi-modules, and followed how the Killer App uses these concepts to create a complex project hierarchy with little configuration. The com.training.killerapp:killerapp project pulls double-duty as both a multi-module project and a parent project. So far we have learned that with a single execution (mvn install) we can build an entire hierarchy of projects, test them and install them for future use by other projects into our local repository. Maven's objectification of projects allows it to easily manage project relationships with simple definitions, and propogate actions and relationships to other projects. We have covered the concept of project as object - in the next chapter we explore the concept of object-base actions, in the form of goals and phases, and how they too have a framework for relating to each other.


Previous: Chapter 3
Next: Chapter 4