This page describes the architectural concepts for the business object layer. The business object layer:
The business object layer provides an explicit business-oriented domain model as an API. In a picture that shows the typical architecture layers of a business application, it can be located on top of the persistence layer (or data layer), and below the application layer that forms the actual application from a reusable domain layer - the business object layer.
It forms a new level of abstraction over the persistent state of objects. It comes with concepts to change and extend the behavior of business objects without breaking the CAPI barrier. It hides the underlying internal implementation, which can still be based on the existing ORM model, or on any other back end. It also provides a more object-oriented view on the available business functionality, which, for example, makes user interface development much easier.
A business object API should contain methods, which exists in the "real" world. This way a business user easily understands the relation between the object in the application and the real world commerce object.
This section describes a minimal set of functions, which a business object and repository should provide. It is always helpful to think about the real world objects to find proper method and business object names. For example: I would add a product to my cart instead of create a product line item.
Each repository should provide following methods or functions:
These functions can be provided directly or indirectly (like BusinessObject.remove()), with more or less parameters. The externalID can be replaced by other unique key-names inside of the repository like "name", "sku" ....
Methods to reference to external business objects can be created via the business object itself or artifical external key providers. E.g: think about a product rating/review repository.
The functionalities of business objects are very different. It is not possible to provide a general set of methods. At least, each business object (root entity - accessable via repository) should have an identifier to resolve that business object. An external identifier is necessary also, so the user or external system can identify the object.
A business object, which acts as aggregate, must not act as a repository for included business object entities. E.g.:
The BasketBO does not need to provide "find" or "getByID" methods.
Note
Intershop highly recommends that each method call ends in a valid state of the business object. So it is not necessary to call other methods to correct the state of the business object.
As explained business objects matches real world objects. So the business object API must avoid a close binding to the implementation or used technology. E.g., avoid references to:
The behavior of the business object layer can be changed dynamically with respect to the application (or channel) that uses the business object.
An aggregate represents a certain business functionality that "belongs together". It can be seen as the equivalent for the term "engine" that is used for technical features.
In the business object layer, an aggregate consists of entities, value objects, repositories, extensions and context objects. It is managed in its own cartridge. Aggregates must be built to be self-contained in order to be usable in different kinds of applications.
An entity is a business object that has its own identity. It can reference other business objects within the aggregate or in other aggregates, and it can hold attributes (e.g., it has a state). Entities can have methods for executing business operations, which usually will manipulate the object graph. Usually, entities are mapped to some underlying persistence layer, so they only represent a wrapper around some delegate object.
An aggregate comes with a root entity object, which serves as the entry point and which manages the inner entities. The inner objects (e.g., entities) cannot be created directly from the outside, but they can only be accessed via the methods of the root object (or indirectly via other entities, starting from root).
The root entity can be retrieved from a repository by its ID. Only the root entity type is accessible from a repository, but no inner entities. The ID of an entity has a local scope within the aggregate / repository.
// BasketBO.java package com.intershop.component.basket.capi; import com.intershop.beehive.businessobject.capi.BusinessObject; public interface BasketBO extends BusinessObject { public BasketLineItemBO addProductToBasket(ProductBO product); public Collection<? extends BasketLineItemBO> getLineItems(); public BigDecimal getTotal(); public BasketBORepository getRepository(); }
Value objects are defined by the values of their attributes. Their identity is unimportant. They can be set as the values of attributes at entities, or they can represent intermediate results, for example, during calculations.
A repository is responsible for the life cycle management of business objects. It handles the creation of new objects, their persistence in the underlying persistence layer, their look-up by some object IDs or other search criteria, and their deletion.
There may be multiple different repository implementations for the same type of business object. For example, the BasketBORepository
interface could be implemented by an implementation that holds baskets in memory only. Another implementation could store them in the database via the ORM engine.
For each aggregate, there is only one repository which manages the root entity. All other entities can be accessed via the root entity methods only.
// BasketBORepository.java package com.intershop.component.basket.capi; import com.intershop.beehive.businessobject.capi.BusinessObjectRepository; public interface BasketBORepository extends BusinessObjectRepository { public BasketBO createBasket(); public BasketBO getBasketByID(String id); public Collection<? extends BasketBO> getAllBaskets(); public void deleteBasket(String id); }
The business object layer is designed for optimum usability by application programmers. It comes with a nicely designed API which reflects all important domain concepts as Java interfaces, methods and so on. The business object layer API serves as the primary API for programmers in customer projects or Intershop solutions and hides the internal implementation details (like the mapping to the persistence layer) from them. It has to remain stable for a long time.
In contrast, the underlying persistence layer for a business object is designed for maximum performance (read, write). The beauty and stability of the API is secondary, as it will not be exposed to any application programmers. The persistence layer implementation is subject to optimization and therefore to permanent change. It may be redesigned for a better performance without breaking the business object API.
The underlying persistence layer will therefore have a different object structure than the business object layer. Which structure to choose cannot be defined here, as it depends on the actual requirements for the business object.
For now, we will only provide an implementation of the business objects API that directly operates on their underlying persistence representation. If there are multiple different implementations of the business object interfaces (e.g., multiple repository implementations), this means that all business methods in an object will have to be re-implemented again in each different implementation. This can be avoided by defining an additional "storage" API, which abstracts the persistence layer from the business API. However, due to the increased implementation efforts we will not provide such an API for now. If necessary, it can be introduced later without breaking the business object API.
In order to support the customization and extension of functionality of business objects, business objects can be enhanced by attaching an extension object to them. Extensions are instantiated by extension factories. An extension factory can decide whether it is applicable for a given business object and can create an extension instance for this object. The decision may include checks of the interface type, the implementation type or even the attributes (state) of the business object.
The list of available extension factories is the key to achieve an app-specific behavior: by attaching different lists to different apps, a business object can behave differently, depending on the app from which it is accessed.
//RepositoryBOBasketExtensionImpl.java public class RepositoryBOBasketExtensionFactory extends AbstractDomainRepositoryBOExtensionFactory { /** * The ID of the created extensions which can be used to get them from the business object later. */ public static final String EXTENSION_ID = "BasketBORepository"; @Override public BusinessObjectExtension<RepositoryBO> createExtension(RepositoryBO repository) { return new RepositoryBOBasketExtensionImpl(EXTENSION_ID, repository); } }
Context objects are used to provide the run-time context for business objects and their methods. Therefore, every business object lives in a business object context. The context object holds the list of available extension factories. Additionally, it also handles the caching of business object instances.
Every entity implementation will get a reference to the context in which it was instantiated.
In order to support the development of business objects, a number of super classes and interfaces are provided, which must be subclassed or implemented. The framework classes are located in the cartridge businessobject.
The image shows the conceptual structure of the framework.
A business object repository can be implemented in several ways:
RepositoryBO
that is internally mapped to a Domain, and a BasketBORepository
is implemented by an extension that can be attached to the RepositoryBO
and internally manages {{BasketPO}}s (which require a domain).
It may be necessary for an extension to store custom data in the host object. For this purpose, the framework defines an API that can be used to set or get custom attributes and localized custom attributes at a business object. An extension can be implemented that provides access to such custom attributes. How this API is mapped to an underlying implementation is up to the implementer of a business object.
In Enfinity, there is an implementation of such an extension that is applicable to all business objects that internally delegate to an ExtensibleObject
. The business object attributes API is mapped to the extensible object attribute values.
The whole concept has been evaluated using a reference implementation. This implementation is completely autonomous and mimics several core concepts of Enfinity. It serves as a showcase how several issues can be solved and how certain things must be implemented when developing or working with business objects.
We must distinguish between two different life cycles:
The Java instance is bound to the context, so it is valid as long as the context is in use. In Enfinity, such contexts will have a lifetime of one request, so after the request the BO instance will become invalid and cannot be used anymore.
The persistent entity has a different life cycle, since such objects can exist for a long time (e.g., as entries in the database).
Business objects can notify interested listeners about important life cycle events, like object creation, object changes and object deletion. Such events refer to the persistent life cycle of the object, not the Java life cycle.
Using this notification mechanism, extensions can register themselves at their host object in order to perform necessary cleanup or initialization actions of their underlying persistent state.
The business object layer does not define its own transaction handling. Instead, it relies on the transaction handling of the underlying persistence layer (if applicable).
In Enfinity, transactions are controlled by the pipeline. This will not change with a new business object layer. The business object implementation should not handle its own transactions, as it may be necessary to synchronize the commits of multiple different business objects with each other.
Ideally, the business object developer does not have to care about transactions at all. They will be handled by the pipeline engine and the underlying ORM engine.
Business objects (e.g., root entities) must have an ID that can be used to look them up later. It can be retrieved by the method getID()
, and it is generated by the repository that creates the business object. In the business object layer, such IDs are simply defined to be a string attribute. Its purpose is an internal one - it is used to look up objects within a repository, where the repository can also be an entity within an aggregate object (see below). The possible values are not specified here, this is up to the repository implementation. For repositories that map the objects to ORM objects, we recommend to use the UUID of the mapped PO as ID.
Depending entities, e.g., objects that are reachable via the root entity only, may also have an ID. This ID can be used within the scope of the aggregate to identify the object. For example, such entities could be looked up by using some method in the root entity (or the entity being the repository of the BO in question). How the IDs of internal entities map to the underlying persistence layer is also left open here. Some implementations may prefer to use UUIDs, some other implementations may rely on auto-generated sequence numbers in the datastore, some others may need to use compound keys, and so on. Preferably, the ID of non-root BOs contains the ID of their parent BO. This allows looking them up from the outside repository.
The ID is for internal purposes only and should not leave Enfinity (the only exception is the user's browser, so the ID may take part in web forms).
Typically, business objects are also referred by a so-called "semantic key". This is the ID made available to the business side. It is used when communication outside of Enfinity takes place, e.g., for exports and imports. There is no defined structure or naming for this kind of ID; it is up to each BO to define it. It must be documented in the documentation of the BO API. As far as it exists, it needs to contain the name of the repository where the BO is located, and it must be globally unique. The business ID can be provided by the repository, but it can also be set from the outside (e.g., when importing data). For aggregate objects, the "inner" objects also need a semantic key, that must contain the semantic key of their parent object. If no such key can be derived from the existing business data, it needs to be created artificially.
Business objects are valid as long as their context is valid. Since the context is usually bound to a single request, the lifetime of an object is the time until the request is processed and finished. On the next request, a new context object will be created, so the business object must be retrieved from the repository again.
The repository may implement a caching strategy that returns the same business object instance again in case an object for a given ID is looked up multiple times with the same context.
Additionally, the underlying persistence layer (like the ORM engine) is free to implement its own caching strategy to allow caching across multiple requests or even threads.
Some methods in a business object may be very expensive, so their result should be cached. For example, operations like calculating the total of a basket or converting some XML CLOB from the persistent object back into a Java representation are very expensive. The result of such an operation can be set as an instance variable at the business object.
Since the business object depends on the context, and the context is different for each concurrent request, the transactional isolation between objects is guaranteed.
If a business object holds its own internal cached state (for performance reasons), it may be necessary to notify it regarding changes that affect the validity of the cached state. For example, when a basket caches the total of all its line items, this total becomes invalid if the quantity of a line item is changed.
There are two possible ways to implement such a cache invalidation:
The invalidation might have two scopes:
However, since each of the implementation of the objects in an object graph may be exchanged independently, the first case cannot really happen - there is always the chance that someone out there is relying on the changed data. This means that, when the exposed data is changed, all interested parties must be invalidated (when data that is never exposed changes, there are no subsequent objects that need to be notified).
For doing this, a central BusinessObjectInvalidationHandler
is provided. This handler knows about all objects that need invalidation, and propagates the invalidation event to them. So the process consists of 2 steps
Members of an aggregate can pass the InvalidationHandler
as a constructor argument. The handler is needed for
Note that the invalidate() call to the handler is sufficient - the caller will be called back from the handler when it is registered. There is no need for the caller to invalidate its data directly before or after the call to the handler (though it does not harm).
When registering as a listener, an anonymous inner class should be used. This avoids exposing the Listener interface on the business object itself, so these methods cannot be called from the outside. This ensures that they are not called by accident.