diff --git a/kmolecules-ddd/pom.xml b/kmolecules-ddd/pom.xml new file mode 100644 index 0000000..f422939 --- /dev/null +++ b/kmolecules-ddd/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + + + org.jmolecules + jmolecules + 1.5.0-SNAPSHOT + + + kmolecules-ddd + + kMolecules - DDD + Kotlin flavor of jMolecules DDD + + + org.kmolecules.ddd + + + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/test/kotlin + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + + compile + + compile + + + + + test-compile + + test-compile + + + + + + + + + diff --git a/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/annotation/AggregateRoot.kt b/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/annotation/AggregateRoot.kt new file mode 100644 index 0000000..724182e --- /dev/null +++ b/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/annotation/AggregateRoot.kt @@ -0,0 +1,22 @@ +package org.jmolecules.ddd.annotation + +/** + * Identifies an aggregate root, i.e. the root entity of an aggregate. An aggregate forms a cluster of consistent rules + * usually formed around a set of entities by defining invariants based on the properties of the aggregate that have to + * be met before and after operations on it. Aggregates usually refer to other aggregates by their identifier. + * References to aggregate internals should be avoided and at least not considered strongly consistent (i.e. a reference + * held could possibly have been gone or become invalid at any point in time). They also act as scope of consistency, + * i.e. changes on a single aggregate are expected to be strongly consistent while changes across multiple ones should + * only expect eventual consistency. + * + * Kotlin's counterpart of the Java's [AggregateRoot](https://github.com/xmolecules/jmolecules/blob/main/jmolecules-ddd/src/main/java/org/jmolecules/ddd/annotation/AggregateRoot.java) + * + * @author Jocelyn Ntakpe + * + * See also : [Domain-Driven Design Reference (Evans) - Aggregates](https://domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf) + */ +@Entity +@Retention +@MustBeDocumented +@Target(AnnotationTarget.CLASS) +annotation class AggregateRoot diff --git a/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/annotation/Entity.kt b/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/annotation/Entity.kt new file mode 100644 index 0000000..d2dbd89 --- /dev/null +++ b/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/annotation/Entity.kt @@ -0,0 +1,18 @@ +package org.jmolecules.ddd.annotation + +/** + * Identifies an [Entity]. Entities represent a thread of continuity and identity, going through a lifecycle, + * though their attributes may change. Means of identification may come from the outside, or it may be an arbitrary + * identifier created by and for the system, but it must correspond to the identity distinctions in the model. The model + * must define what it means to be the same thing. + * + * Kotlin's counterpart of the Java's [Entity](https://github.com/xmolecules/jmolecules/blob/main/jmolecules-ddd/src/main/java/org/jmolecules/ddd/annotation/Entity.java) + * + * @author Jocelyn Ntakpe + * + * See also : [Domain-Driven Design Reference (Evans) - Entities](https://domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf) + */ +@Retention +@MustBeDocumented +@Target(AnnotationTarget.CLASS) +annotation class Entity diff --git a/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/annotation/Factory.kt b/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/annotation/Factory.kt new file mode 100644 index 0000000..c9c1274 --- /dev/null +++ b/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/annotation/Factory.kt @@ -0,0 +1,17 @@ +package org.jmolecules.ddd.annotation + +/** + * Identifies a [Factory]. Factories encapsulate the responsibility of creating complex objects in general and + * Aggregates in particular. Objects returned by the factory methods are guaranteed to be in valid state. + * + * Kotlin's counterpart of the Java's [Factory](https://github.com/xmolecules/jmolecules/blob/main/jmolecules-ddd/src/main/java/org/jmolecules/ddd/annotation/Factory.java) + * + * @author Jocelyn Ntakpe + * + * See also : [AggregateRoot] + * See also : [Domain-Driven Design Reference (Evans) - Factories](https://domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf) + */ +@Retention +@MustBeDocumented +@Target(AnnotationTarget.CLASS) +annotation class Factory diff --git a/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/annotation/Repository.kt b/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/annotation/Repository.kt new file mode 100644 index 0000000..3bea7a6 --- /dev/null +++ b/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/annotation/Repository.kt @@ -0,0 +1,21 @@ +package org.jmolecules.ddd.annotation + +/** + * Identifies a [Repository]. Repositories simulate a collection of aggregates to which aggregate instances can be + * added and removed. They usually also expose API to select a subset of aggregates matching certain criteria. Access to + * projections of an aggregate might be provided as well but also via a dedicated separate abstraction. + * + * Implementations use a dedicated persistence mechanism appropriate to the data structure and query requirements at + * hand. However, they should make sure that no persistence mechanism specific APIs leak into client code. + * + * Kotlin's counterpart of the Java's [Repository](https://github.com/xmolecules/jmolecules/blob/main/jmolecules-ddd/src/main/java/org/jmolecules/ddd/annotation/Repository.java) + * + * @author Jocelyn Ntakpe + * + * See also : [AggregateRoot] + * See also : [Domain-Driven Design Reference (Evans) - Repositories](https://domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf) + */ +@Retention +@MustBeDocumented +@Target(AnnotationTarget.CLASS) +annotation class Repository diff --git a/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/annotation/Service.kt b/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/annotation/Service.kt new file mode 100644 index 0000000..f69e590 --- /dev/null +++ b/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/annotation/Service.kt @@ -0,0 +1,17 @@ +package org.jmolecules.ddd.annotation + +/** + * Identifies a domain [Service]. A service is a significant process or transformation in the domain that is not a + * natural responsibility of an entity or value object, add an operation to the model as a standalone interface declared + * as a service. Define a service contract, a set of assertions about interactions with the service. (See assertions.) + * State these assertions in the ubiquitous language of a specific bounded context. Give the service a name, which also + * becomes part of the ubiquitous language. + * + * Kotlin's counterpart of the Java's [Service](https://github.com/xmolecules/jmolecules/blob/main/jmolecules-ddd/src/main/java/org/jmolecules/ddd/annotation/Service.java) + * + * @author Jocelyn Ntakpe + */ +@Retention +@MustBeDocumented +@Target(AnnotationTarget.CLASS) +annotation class Service diff --git a/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/annotation/ValueObject.kt b/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/annotation/ValueObject.kt new file mode 100644 index 0000000..b6f1840 --- /dev/null +++ b/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/annotation/ValueObject.kt @@ -0,0 +1,16 @@ +package org.jmolecules.ddd.annotation + +/** + * Identifies a value object. Domain concepts that are modeled as value objects have no conceptual identity or + * lifecycle. Implementations should be immutable, operations on it are side effect free. + * + * Kotlin's counterpart of the Java's [ValueObject](https://github.com/xmolecules/jmolecules/blob/main/jmolecules-ddd/src/main/java/org/jmolecules/ddd/annotation/ValueObject.java) + * + * @author Jocelyn Ntakpe + * + * See also : [Domain-Driven Design Reference (Evans) - Value objects](https://domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf) + */ +@Retention +@MustBeDocumented +@Target(AnnotationTarget.CLASS) +annotation class ValueObject diff --git a/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/types/AggregateRoot.kt b/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/types/AggregateRoot.kt new file mode 100644 index 0000000..127c9b4 --- /dev/null +++ b/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/types/AggregateRoot.kt @@ -0,0 +1,20 @@ +package org.jmolecules.ddd.types + +/** + * Identifies an aggregate root, i.e. the root entity of an aggregate. An aggregate forms a cluster of consistent rules + * usually formed around a set of entities by defining invariants based on the properties of the aggregate that have to + * be met before and after operations on it. Aggregates usually refer to other aggregates by their identifier. + * References to aggregate internals should be avoided and at least not considered strongly consistent (i.e. a reference + * held could possibly have been gone or become invalid at any point in time). They also act as scope of consistency, + * i.e. changes on a single aggregate are expected to be strongly consistent while changes across multiple ones should + * only expect eventual consistency. + * + * Kotlin's counterpart of Java's [AggregateRoot](https://github.com/xmolecules/jmolecules/blob/main/jmolecules-ddd/src/main/java/org/jmolecules/ddd/types/AggregateRoot.java) + * + * @author Jocelyn Ntakpe + * @since 1.0 + * + * See also: [Domain-Driven Design Reference (Evans) - Aggregates](https://domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf) + * See also: [John Sullivan - Advancing Enterprise DDD - Reinstating the Aggregate](https://scabl.blogspot.com/2015/04/aeddd-9.html) + */ +interface AggregateRoot, ID : Identifier> : Entity diff --git a/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/types/Association.kt b/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/types/Association.kt new file mode 100644 index 0000000..ab1b171 --- /dev/null +++ b/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/types/Association.kt @@ -0,0 +1,55 @@ +package org.jmolecules.ddd.types + +/** + * An association to an [AggregateRoot]. + * + * Kotlin's counterpart of Java's [Association](https://github.com/xmolecules/jmolecules/blob/main/jmolecules-ddd/src/main/java/org/jmolecules/ddd/types/Association.java) + * + * @author Jocelyn Ntakpe + * @since 1.0 + * See also: [John Sullivan - Advancing Enterprise DDD - Reinstating the Aggregate](https://scabl.blogspot.com/2015/04/aeddd-9.html) + */ +interface Association, ID : Identifier> : Identifiable { + + companion object { + + /** + * Creates an [Association] pointing to the [Identifier] of the given [AggregateRoot]. + * + * @param T the concrete [AggregateRoot] type. + * @param ID the concrete [Identifier] type. + * @param aggregate used to create the association + * @return an [Association] pointing to the [Identifier] of the given [AggregateRoot]. + * @since 1.3 + */ + @JvmStatic + fun , ID : Identifier> forAggregate(aggregate: T): Association { + return SimpleAssociation { aggregate.id } + } + + /** + * Creates an [Association] pointing to the given [Identifier]. + * + * @param T the concrete [AggregateRoot] type. + * @param ID the concrete [Identifier] type. + * @param identifier used to create the association + * @return an [Association] pointing to the given [Identifier]. + * @since 1.3 + */ + @JvmStatic + fun , ID : Identifier> forId(identifier: ID): Association { + return SimpleAssociation { identifier } + } + } + + /** + * Returns whether the current [Association] points to the same [AggregateRoot] as the given one. Unlike + * [equals] and [hashCode] that also check for type equality of the [Association] + * itself, this only compares the target [Identifier] instances. + * + * @param other association compared with the current instance + * @return whether the current [Association] points to the same [AggregateRoot] as the given one. + * @since 1.3 + */ + fun pointsToSameAggregateAs(other: Association<*, ID>): Boolean = id == other.id +} diff --git a/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/types/Entity.kt b/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/types/Entity.kt new file mode 100644 index 0000000..fd8056a --- /dev/null +++ b/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/types/Entity.kt @@ -0,0 +1,17 @@ +package org.jmolecules.ddd.types + +/** + * Identifies an [Entity]. Entities represent a thread of continuity and identity, going through a lifecycle, + * though their attributes may change. Means of identification may come from the outside, or it may be an arbitrary + * identifier created by and for the system, but it must correspond to the identity distinctions in the model. The model + * must define what it means to be the same thing. + * + * Kotlin's counterpart of Java's [Entity](https://github.com/xmolecules/jmolecules/blob/main/jmolecules-ddd/src/main/java/org/jmolecules/ddd/types/Entity.java) + * + * @author Jocelyn Ntakpe + * @since 1.0 + * + * See also: [Domain-Driven Design Reference (Evans) - Entities](https://domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf) + * See also: [John Sullivan - Advancing Enterprise DDD - Reinstating the Aggregate](https://scabl.blogspot.com/2015/04/aeddd-9.html) + */ +interface Entity, ID> : Identifiable diff --git a/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/types/Identifiable.kt b/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/types/Identifiable.kt new file mode 100644 index 0000000..28392bc --- /dev/null +++ b/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/types/Identifiable.kt @@ -0,0 +1,17 @@ +package org.jmolecules.ddd.types + +/** + * An identifiable type, i.e. anything that exposes an [Identifier]. + * + * Kotlin's counterpart of Java's [Identifiable](https://github.com/xmolecules/jmolecules/blob/main/jmolecules-ddd/src/main/java/org/jmolecules/ddd/types/Identifiable.java) + * + * @author Jocelyn Ntakpe + * @since 1.0 + */ +interface Identifiable { + + /** + * Identifier + */ + val id: ID +} diff --git a/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/types/Identifier.kt b/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/types/Identifier.kt new file mode 100644 index 0000000..5ede48e --- /dev/null +++ b/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/types/Identifier.kt @@ -0,0 +1,13 @@ +package org.jmolecules.ddd.types + +/** + * Marker interface for identifiers. Exists primarily to easily identify types that are supposed to be identifiers + * within the code base and let the compiler verify the correctness of declared relationships. + * + * Kotlin's counterpart of Java's [Identifier](https://github.com/xmolecules/jmolecules/blob/main/jmolecules-ddd/src/main/java/org/jmolecules/ddd/types/Identifier.java) + * + * @author Jocelyn Ntakpe + * @since 1.0 + * @see Identifiable + */ +interface Identifier diff --git a/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/types/Repository.kt b/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/types/Repository.kt new file mode 100644 index 0000000..53d5570 --- /dev/null +++ b/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/types/Repository.kt @@ -0,0 +1,19 @@ +package org.jmolecules.ddd.types + +/** + * Identifies a [Repository]. Repositories simulate a collection of aggregates to which aggregate instances can be + * added and removed. They usually also expose API to select a subset of aggregates matching certain criteria. Access to + * projections of an aggregate might be provided as well but also via a dedicated separate abstraction. + * + * Implementations use a dedicated persistence mechanism appropriate to the data structure and query requirements at + * hand. However, they should make sure that no persistence mechanism specific APIs leak into client code. + * + * Kotlin's counterpart of Java's [Repository](https://github.com/xmolecules/jmolecules/blob/main/jmolecules-ddd/src/main/java/org/jmolecules/ddd/types/Repository.java) + * + * @author Jocelyn Ntakpe + * @since 1.0 + * @see [AggregateRoot] + * + * See also: [Domain-Driven Design Reference (Evans) - Repositories](https://domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf) + */ +interface Repository, ID : Identifier> diff --git a/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/types/SimpleAssociation.kt b/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/types/SimpleAssociation.kt new file mode 100644 index 0000000..2ef48df --- /dev/null +++ b/kmolecules-ddd/src/main/kotlin/org/jmolecules/ddd/types/SimpleAssociation.kt @@ -0,0 +1,34 @@ +package org.jmolecules.ddd.types + +/** + * Simple implementation of [Association] to effectively only define [equals] and + * [hashCode] on [Association]'s static factory methods. + * + * Kotlin's equivalent of Java's [SimpleAssociation](https://github.com/xmolecules/jmolecules/blob/main/jmolecules-ddd/src/main/java/org/jmolecules/ddd/types/SimpleAssociation.java) + * + * @author Jocelyn Ntakpe + * @since 1.0 + * @see Association.forId + * @see Association.forAggregate + */ +class SimpleAssociation, ID : Identifier>(private val identifier: () -> ID) : + Association { + + override val id: ID + get() = identifier() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SimpleAssociation<*, *> + + if (id != other.id) return false + + return true + } + + override fun hashCode(): Int = id.hashCode() + + override fun toString(): String = id.toString() +} diff --git a/kmolecules-ddd/src/test/kotlin/org/jmolecules/ddd/types/AssociationTest.kt b/kmolecules-ddd/src/test/kotlin/org/jmolecules/ddd/types/AssociationTest.kt new file mode 100644 index 0000000..1bda92e --- /dev/null +++ b/kmolecules-ddd/src/test/kotlin/org/jmolecules/ddd/types/AssociationTest.kt @@ -0,0 +1,56 @@ +package org.jmolecules.ddd.types + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +/** + * Unit tests for [Association] + * + * @author Jocelyn Ntakpe + */ +internal class AssociationTest { + + @Test + fun `creates association from identifier`() { + val id = SampleIdentifier() + assertThat(Association.forId(id).id).isEqualTo(id) + } + + @Test + fun `creates association from aggregate`() { + val id = SampleIdentifier() + val aggregate = SampleAggregate(id) + assertThat(Association.forAggregate(aggregate).id).isEqualTo(id) + } + + @Test + fun `associations equal if they point to the same id`() { + val id = SampleIdentifier() + val aggregate = SampleAggregate(id) + assertThat(Association.forId(id)).isEqualTo( + Association.forAggregate( + aggregate + ) + ) + assertThat(Association.forAggregate(aggregate)).isEqualTo( + Association.forId( + id + ) + ) + } + + @Test + fun `points to same aggregate`() { + val id = SampleIdentifier() + val simpleAssociation: Association = Association.forId(id) + val sampleAssociation = SampleAssociation(id) + assertThat(simpleAssociation.pointsToSameAggregateAs(sampleAssociation)).isTrue + assertThat(sampleAssociation.pointsToSameAggregateAs(simpleAssociation)).isTrue + } + + class SampleIdentifier : Identifier + + class SampleAggregate(override val id: SampleIdentifier) : AggregateRoot + + class SampleAssociation(override val id: SampleIdentifier) : Association +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 98e3ae6..812dbce 100644 --- a/pom.xml +++ b/pom.xml @@ -15,12 +15,14 @@ jmolecules-ddd jmolecules-events jmolecules-architecture + kmolecules-ddd UTF-8 3.20.2 5.7.2 + 1.6.21