-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ Add Superhero Domain and DDD Patterns (#277)
* Started on new hero domain model * Tidied up base classes * Updated readme * Added migration for new Domain models * Added DDD adr * Tidied up current mission * Updated domain model * Added DB diagram * Domain is easier to click * Added descriptions to common base classes and interfaces * Temporarily remove test * Fix arch tests * Removed unneeded interface * Capture properties when event is raised * Tidied up aggregate root interface * Fixed arch tests * Update src/Infrastructure/Persistence/Configuration/HeroConfiguration.cs Co-authored-by: Chris Clement [SSW] <[email protected]> * Configuration: Remove TODOs * Renamed Strength to PowerLevel * Update ADR * Add TODO: to DB Context Configurations' marker * Update database.png with new Heroes PowerLevel name --------- Co-authored-by: Chris Clement [SSW] <[email protected]> Co-authored-by: Chris <[email protected]>
- Loading branch information
1 parent
64aaca4
commit 23fa27e
Showing
37 changed files
with
1,149 additions
and
55 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
43 changes: 43 additions & 0 deletions
43
docs/adr/20240404-use-domain-driven-design-tactical-patterns.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
# Use Domain-Driven Design Tactical Patterns | ||
|
||
- Status: Accepted | ||
- Deciders: Daniel Mackay, Matt Goldman, Matt Wicks, Luke Parker, Chris Clement | ||
- Date: 2024-04-04 | ||
- Tags: ddd | ||
|
||
Technical Story: https://github.com/SSWConsulting/SSW.CleanArchitecture/issues/283 | ||
|
||
|
||
## Context and Problem Statement | ||
|
||
The current Clean Architecture framework relies on an anemic domain model, which simplifies initial development but increasingly hampers our ability to handle the complex interactions and business logic inherent in our domain. By incorporating Domain-Driven Design (DDD), projects with non-trivial logic can better accommodate complex workflows and business rule integrations without compromising maintainability or scalability. | ||
|
||
We would like to default to using DDD in the template and provide a good example of building applications in that manner. | ||
|
||
|
||
## Considered Options | ||
|
||
1. Anemic Domain Model | ||
2. Rich Domain Model with DDD | ||
|
||
## Decision Outcome | ||
|
||
Chosen option: "Option 2 - Rich Domain Model with DDD", because it helps set developers up for success when building complex applications. It's easier to go from a rich domain model to an anemic domain model than the other way around. | ||
|
||
### Consequences <!-- optional --> | ||
|
||
- Need to create a new Domain model to show the usefulness of DDD. This will require most layers to be rebuilt. | ||
|
||
## Pros and Cons of the Options <!-- optional --> | ||
|
||
### Option 1 - Anemic Domain Model | ||
|
||
- ✅ Simplier for trivial applications | ||
- ❌ Difficult to upgrade to use DDD patterns | ||
|
||
### Option 2 - Rich Domain Model with DDD | ||
|
||
- ✅ Easy to migrate to an anemic domain model if needed | ||
- ✅ More flexible for complex applications | ||
- ❌ Overkill for trivial applications | ||
- ❌ More complex to understand |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
# SuperHero Domain Model | ||
|
||
- `Hero` - Aggregate | ||
- `Team` - Aggregate | ||
- `Power` - ValueObject | ||
- `Mission` - Entity | ||
- `HeroPowerUpdated` - Domain Event (updates the TotalPowerLevel on the SuperHeroTeam) | ||
|
||
```mermaid | ||
classDiagram | ||
class Hero { | ||
string Name | ||
string Alias | ||
int Strength | ||
Power[] Powers | ||
void AddPower() | ||
void RemovePower() | ||
} | ||
class Power { | ||
string Name | ||
string Strength | ||
} | ||
class Team { | ||
string Name | ||
int TotalStrength | ||
enum TeamStatus | ||
Mission[] Missions | ||
void AddHero() | ||
void RemoveHero() | ||
void ExecuteMission() | ||
void CompleteCurrentMission() | ||
} | ||
class Mission { | ||
int MissionId | ||
string Description | ||
enum MissionStatus | ||
void Complete() | ||
} | ||
Hero --> Power: has many | ||
Team --> Hero: has many | ||
Team --> Mission: has many | ||
``` | ||
## Database Schema | ||
|
||
![SuperHero Database Schema](./database.png) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
using SSW.CleanArchitecture.Domain.Common.Interfaces; | ||
using System.ComponentModel.DataAnnotations.Schema; | ||
|
||
namespace SSW.CleanArchitecture.Domain.Common.Base; | ||
|
||
/// <summary> | ||
/// Cluster of objects treated as a single unit. | ||
/// Can contain entities, value objects, and other aggregates. | ||
/// Enforce business rules (i.e. invariants) | ||
/// Can be created externally. | ||
/// Can raise domain events. | ||
/// Represent a transactional boundary (i.e. all changes are saved or none are saved) | ||
/// </summary> | ||
public abstract class AggregateRoot<TId> : Entity<TId>, IAggregateRoot | ||
{ | ||
private readonly List<DomainEvent> _domainEvents = []; | ||
|
||
[NotMapped] | ||
public IReadOnlyList<DomainEvent> DomainEvents => _domainEvents.AsReadOnly(); | ||
|
||
public void AddDomainEvent(DomainEvent domainEvent) => _domainEvents.Add(domainEvent); | ||
|
||
public void RemoveDomainEvent(DomainEvent domainEvent) => _domainEvents.Remove(domainEvent); | ||
|
||
public void ClearDomainEvents() => _domainEvents.Clear(); | ||
} | ||
|
||
// TODO: Delete this once TodoItems are removed | ||
public abstract class BaseEntity<TId> : Entity<TId> | ||
{ | ||
private readonly List<DomainEvent> _domainEvents = []; | ||
|
||
[NotMapped] | ||
public IReadOnlyList<DomainEvent> DomainEvents => _domainEvents.AsReadOnly(); | ||
|
||
public void AddDomainEvent(DomainEvent domainEvent) => _domainEvents.Add(domainEvent); | ||
|
||
public void RemoveDomainEvent(DomainEvent domainEvent) => _domainEvents.Remove(domainEvent); | ||
|
||
public void ClearDomainEvents() => _domainEvents.Clear(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
namespace SSW.CleanArchitecture.Domain.Common.Base; | ||
|
||
/// <summary> | ||
/// Entities have an ID and a lifecycle (i.e. created, modified, and deleted) | ||
/// They can be created within the domain, but not externally. | ||
/// Enforce business rules (i.e. invariants) | ||
/// </summary> | ||
public abstract class Entity<TId> : AuditableEntity | ||
{ | ||
public TId Id { get; set; } = default!; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
namespace SSW.CleanArchitecture.Domain.Common; | ||
|
||
public static class Constants | ||
{ | ||
public const int DefaultNameMaxLength = 100; | ||
public const int DefaultDescriptionMaxLength = 500; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
using SSW.CleanArchitecture.Domain.Common.Base; | ||
|
||
namespace SSW.CleanArchitecture.Domain.Common.Interfaces; | ||
|
||
public interface IAggregateRoot | ||
{ | ||
public IReadOnlyList<DomainEvent> DomainEvents { get; } | ||
|
||
public void AddDomainEvent(DomainEvent domainEvent); | ||
|
||
public void RemoveDomainEvent(DomainEvent domainEvent); | ||
|
||
public void ClearDomainEvents(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
namespace SSW.CleanArchitecture.Domain.Common.Interfaces; | ||
|
||
/// <summary> | ||
/// Marker interface. | ||
/// Value objects do not have identity. | ||
/// They are immutable. | ||
/// Compared by using their attributes or properties. | ||
/// Generally need context perhaps from a parent. | ||
/// Improve ubiquitous language. | ||
/// Help to eliminate primitive obsession. | ||
/// </summary> | ||
public interface IValueObject; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
using Ardalis.GuardClauses; | ||
using SSW.CleanArchitecture.Domain.Common; | ||
using SSW.CleanArchitecture.Domain.Common.Base; | ||
|
||
namespace SSW.CleanArchitecture.Domain.Heroes; | ||
|
||
// For strongly typed IDs, check out the rule: https://www.ssw.com.au/rules/do-you-use-strongly-typed-ids/ | ||
public readonly record struct HeroId(Guid Value); | ||
|
||
public class Hero : AggregateRoot<HeroId> | ||
{ | ||
private readonly List<Power> _powers = []; | ||
public string Name { get; private set; } = null!; | ||
public string Alias { get; private set; } = null!; | ||
public int PowerLevel { get; private set; } | ||
public IEnumerable<Power> Powers => _powers.AsReadOnly(); | ||
|
||
public static Hero Create(string name, string alias) | ||
{ | ||
Guard.Against.NullOrWhiteSpace(name); | ||
Guard.Against.StringTooLong(name, Constants.DefaultNameMaxLength); | ||
|
||
Guard.Against.NullOrWhiteSpace(alias); | ||
Guard.Against.StringTooLong(alias, Constants.DefaultNameMaxLength); | ||
|
||
var hero = new Hero { Id = new HeroId(Guid.NewGuid()), Name = name, Alias = alias, }; | ||
|
||
return hero; | ||
} | ||
|
||
public void AddPower(Power power) | ||
{ | ||
Guard.Against.Null(power); | ||
|
||
if (!_powers.Contains(power)) | ||
{ | ||
_powers.Add(power); | ||
} | ||
|
||
PowerLevel += power.PowerLevel; | ||
AddDomainEvent(new PowerLevelUpdatedEvent(this)); | ||
} | ||
|
||
public void RemovePower(string powerName) | ||
{ | ||
Guard.Against.NullOrWhiteSpace(powerName, nameof(powerName)); | ||
|
||
var power = Powers.FirstOrDefault(p => p.Name == powerName); | ||
if (power is null) | ||
{ | ||
return; | ||
} | ||
|
||
if (_powers.Contains(power)) | ||
{ | ||
_powers.Remove(power); | ||
} | ||
|
||
PowerLevel -= power.PowerLevel; | ||
AddDomainEvent(new PowerLevelUpdatedEvent(this)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
using Ardalis.GuardClauses; | ||
using SSW.CleanArchitecture.Domain.Common; | ||
using SSW.CleanArchitecture.Domain.Common.Interfaces; | ||
|
||
namespace SSW.CleanArchitecture.Domain.Heroes; | ||
|
||
public record Power : IValueObject | ||
{ | ||
// Private setters needed for EF | ||
public string Name { get; private set; } | ||
|
||
// Private setters needed for EF | ||
public int PowerLevel { get; private set; } | ||
|
||
public Power(string name, int powerLevel) | ||
{ | ||
Name = Guard.Against.StringTooLong(name, Constants.DefaultNameMaxLength); | ||
PowerLevel = Guard.Against.OutOfRange(powerLevel, nameof(PowerLevel), 1, 10); | ||
} | ||
} |
Oops, something went wrong.