SlideShare a Scribd company logo
DDDesignChallenges
by YvesReynhout
Half stackdeveloper
Guess whichpart
AboutMe
A journeylist with a passion for well-designed,
properly decomposed, working software.
Professional experience: 20+ years, mainly
building products, in domains such as Patient
logistics, Real estate, Manufacturing,
Construction, Transportation, Oil & gas
Community experience: DDDBE member -
Occasional speaker, blogger and discusser of
(D)DDD, CQRS & ES and Messaging and author
of AggregateSource and Projac
ContactMe
Online: Twitter: @yreynhout or @bittacklr,
Skype: yves.reynhout, Mail:
yves.reynhout@gmail.com or info@bittacklr.be
Meet the Company: BitTacklr BVBA based in
Belgium
Est-ce que tu DDD?
Design, you say?
Why do we fail to design?
Because ...{reasons}
Not a habit (inexperience)
No (real) domain expertise
No (or too little) communication/collaboration
No time to learn the problem/solution space
No (or too little) design up front ( coz Agile ;-) )
No documentation/visualization
Because ...{reasons}
Wrong team or size (consensus)
Second system syndrome in brown‹eld
Option paralysis in green‹eld
...
Let'sassumewe overcome
Design in the small vs the large
Designinthe small
PracticalExamples
Groups
1: public class Group {
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
Guid Id { get; set; }
string Name { get; set; }
string Objective { get; set; }
GroupType Type { get; set; }
public
public
public
public
//...
}
//Usage
var @group = new Group {
Id = message.GroupId,
Name = message.Name,
Objective = message.Objective,
Type = GroupType.Private
};
1: public class Group {
2: public Group(
3: Guid id,
4:
string name,
string objective,
5: GroupType type)
6: {
7: //...
8: }
}
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
//Usage
var @group = new Group(
message.GroupId,
message.Name,
message.Objective,
GroupType.Private
);
1: public class PrivateGroup {
2: private PrivateGroup(/* ... */)
3: {
4:
/* ... */
}
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
public static PrivateGroup Start(
GroupId id,
GroupName name,
GroupObjective objective)
{
//...
}
}
//Usage
var @group = PrivateGroup.Start(
new GroupId(message.GroupId),
new GroupName(message.Name),
GroupObjective.InferFrom(message.Objective
)
);
"id");
"name");
1: public class Group {
2: public Group(
3: Guid id,
4:
string name,
string objective,
5: GroupType type)
6: {
7: if(id == Guid.Empty)
8: throw new ArgumentException("...",
if(string.IsNullOrEmpty(name))
9: throw new ArgumentException("...",
10: if(name.Length > 255)
11: throw new ArgumentException("...",
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
"name");
if(objective != null && objective.Length == 0)
throw new ArgumentException("...", "objective");
if(objective != null && objective.Length > 500)
new ArgumentException("...",
!= GroupType.Private && type
new ArgumentException("...",
"objective");
!= GroupType.Public)
"type");
throw
if(type
throw
//...
}
}
1: public class PrivateGroup {
2: public static PrivateGroup Start(
3: GroupId id,
4:
GroupName name,
GroupObjective objective)
5: {
6: //...
7: }
8: }
9:
1: public class GroupId {
2: private readonly Guid value;
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
public GroupId(Guid value)
{
if(value == Guid.Empty)
throw new ArgumentException("...", "value");
this.value = value;
}
return false;
public override bool Equals(object other)
{
if(other == null || other.GetType() != this.GetType())
return ((GroupId)other).value.Equals(this.value);
}
public override int GetHashCode()
{
return this.value.GetHashCode();
}
//...
}
1: public class GroupName {
2: private readonly string value;
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
public GroupName(string value)
{
if(string.IsNullOrEmpty(value))
throw new ArgumentException("...", "value");
if(value.Length > Metadata.MaximumGroupNameLength)
throw new ArgumentException("...", "value");
this.value = value;
}
//...
}
//Usage
@group.Rename(new GroupName(message.Name));
1: public class GroupObjective {
2: private static readonly GroupObjective NotSpecified =
3: new GroupObjective(null);
4:
private readonly string value;
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
private GroupObjective(string value)
{
this.value = value;
}
public static GroupObjective InferFrom(string value)
{
if(value == null) return NotSpecified;
if(value.Length == 0)
throw new ArgumentException("...", "value");
if(value.Length > Metadata.MaximumGroupObjectiveLength)
throw new ArgumentException("...", "value");
return new GroupObjective(value);
}
}
1: public class GroupObjective {
2: //...
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
public static GroupObjective InferFromMarkdown(string value)
{
if(value == null) return NotSpecified;
MarkDownDocument parsedDocument;
if(!MarkDownParser.TryParse(value, out parsedDocument))
throw new ArgumentException("...", "value");
var document = parsedDocument.Sanitize();
if(document.Text.Length == 0)
throw new ArgumentException("...", "value");
if(document.Text.Length > Metadata.MaximumGroupObjectiveLength)
throw new ArgumentException("...", "value");
return new GroupObjective(document.ToString());
}
}
1: public class GroupObjective {
2: //...
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
public static GroupObjective InferFromMarkdown(MarkDownDocument value)
{
if(value == null) return NotSpecified;
var document = value.Sanitize();
if(document.Text.Length == 0)
throw new ArgumentException("...", "value");
if(document.Text.Length > Metadata.MaximumGroupObjectiveLength)
throw new ArgumentException("...", "value");
return new GroupObjective(document.ToString());
}
}
Scheduling
1: public class FillPercentage {
2: private readonly double value;
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
public FillPercentage(double value)
{
if(value < 0)
throw new ArgumentOutOfRangeException("value",
"The fill percentage must be greater than or
value,
equal to 0.");
this.value = value;
}
public static FillPercentage FromPercentNotation(double value)
{
return new FillPercentage(value / 100d);
}
instance)public static implicit operator double(FillPercentage
{
return instance.value;
}
}
1: public class MaximumFillPercentage {
2: private readonly double value;
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
public MaximumFillPercentage(double value)
{
if(value < 1)
throw new ArgumentOutOfRangeException("value", value,
"The maximum fill percentage must be greater than or equal to 1.
this.value = value;
}
public static MaximumFillPercentage FromPercentNotation(double value)
{
return new MaximumFillPercentage(value / 100d);
}
instance)public static implicit operator double(MaximumFillPercentage
{
return instance.value;
}
}
1: public class MaximumFillPercentage : IComparable<FillPercentage> {
2: //...
3:
4:
5:
6:
7:
8:
public int CompareTo(FillPercentage other)
{
return this.value.Compare(other);
}
}
1: public class MaximumFillPercentage : IComparable<FillPercentage> {
2: //...
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
public static bool operator <
(MaximumFillPercentage left, FillPercentage right) {
return left.CompareTo(right) == 1;
}
right) {FillPercentage
<= 0;
public static bool operator <=
(MaximumFillPercentage left,
return left.CompareTo(right)
}
right) {FillPercentage
== 1;
public static bool operator >
(MaximumFillPercentage left,
return left.CompareTo(right)
}
right) {FillPercentage
>= 0;
public static bool operator >=
(MaximumFillPercentage left,
return left.CompareTo(right)
}
}
1: public class FillPercentage {
2: //...
3:
4:
5:
6:
7:
public bool Exceeds(MaximumFillPercentage maximum) {
return maximum < this;
}
}
1: var tryoutSession = session.Add(timeslot);
2:
3:
4:
5:
6:
7:
if (tryoutSession
.CurrentFillPercentage
.Exceeds(session.MaximumFillPercentage))
{
throw new MaximumFillPercentageExceededException(/* ... */);
}
Designinthe small
Value Objects act as an attractor for behaviour,
such as validation, security, arithmetic,
composition, ...
They are the primitives of your model.
They keep the repetition out of the wrong places
and stimulate the use of acceptable values.
They promote domain specific language in
both their name and behaviors.
They compose well and move us away from the
procedural landscape.
Still, don't go overboard (e.g. message obsession).
Groups
1: public class PrivateGroupBehavior : BehaviorModule {
2: public PrivateGroupBehavior(
3: ITenantRepository tenantRepository,
4:
IPrivateGroupRepository groupRepository)
{
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
Receive<StartPrivateGroup>(async message =>
{
var tenant
.Get(new
= await tenantRepository
TenantId(message.TenantId))
;
var @group = tenant.StartPrivateGroup(
new GroupId(message.GroupId),
new GroupName(message.GroupName);
GroupObjective.InferFrom(message.GroupObjective))
;
groupRepository.Add(@group);
});
}
}
1: public class PrivateGroupScenarios {
2: [Fact] public async Task when_starting_a_private_group() {
3: using(var context = new GroupContext()) {
4:
//Arrange
context.Tenants.Add(
5: new TenantData { TenantId = 123, Subdomain = "nespresso" });
6: var module = new PrivateGroupBehavior(
7: new TenantRepository(context),
8: new PrivateGroupRepository(context)
); //Act
9: await module.Send(new StartPrivateGroup {
10: TenantId = 123,
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
GroupId = new Guid("a9a620cac8c241b7b7c0fc03881ed00d"),
Name = "Prototype Retail Machines"
}); //Assert
Assert.Equal(1, context.PrivateGroups.Count());
GroupData actual = context.PrivateGroups.Single();
Assert.Equal(123, actual.TenantId);
Assert.Equal(new Guid("a9a620cac8c241b7b7c0fc03881ed00d"),
actual.GroupId);
Assert.Equal("Prototype Retail Machines", actual.Name);
}
}
}
1: public class PrivateGroupScenarios {
2: [Fact] public Task when_starting_a_private_group() {
3: var tenantId = new Random().Next(1, 100);
4:
var groupId = Guid.NewGuid();
return new Scenario()
5: .Given(tenantId, new TenantSnapshot {
6: TenantId = tenantId,
7: Subdomain = "nespresso"
8: })
.When(new StartPrivateGroup {
9: TenantId = tenantId,
10: GroupId = groupId,
11: Name = "Prototype Retail Machines"
12: })
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
.Then(groupId, new PrivateGroupSnapshot {
TenantId = tenantId,
GroupId = groupId,
Machines"Name = "Prototype Retail
})
.Assert();
} //statebased
}
1: public class PrivateGroupScenarios {
2: [Fact] public Task when_starting_a_private_group() {
3: var tenantId = new Random().Next(1, 100);
4:
var groupId = Guid.NewGuid();
return new Scenario()
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
.Given(tenantId, new RegisteredAsTenant {
TenantId = tenantId,
Subdomain = "nespresso"
})
.When(new StartPrivateGroup {
TenantId = tenantId,
GroupId = groupId,
Name = "Prototype Retail Machines"
})
.Then(groupId, new PrivateGroupStarted {
TenantId = tenantId,
GroupId = groupId,
Machines"Name = "Prototype Retail
})
.Assert();
} //eventdriven
}
1: public class PrivateGroupScenarios {
2: [Fact] public Task when_starting_a_private_group() {
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
var tenantId = new TenantId(new Random().Next(1, 100));
var
var
var
subdomain
groupId =
groupName
= new Subdomain("nespresso");
new GroupId(Guid.NewGuid());
= new GroupName("Prototype Retail Machines");
return new Scenario()
.Given(tenantId, new RegisteredAsTenant {
TenantId = tenantId,
Subdomain = subdomain
})
.When(new StartPrivateGroup {
TenantId = tenantId,
GroupId = groupId,
Name = groupName
})
.Then(groupId, new PrivateGroupStarted {
TenantId = tenantId,
GroupId = groupId,
Name = groupName
})
.Assert();
} //use of value objects
}
1: public class PrivateGroupScenarios {
2: [Fact] public Task when_starting_a_private_group() {
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
var fixture = new ScenarioFixture();
var tenantId = fixture.Create<TenantId>();
var groupId = fixture.Create<GroupId>();
var groupName = fixture.Create<GroupName>();
return new Scenario()
.Given(tenantId, new RegisteredAsTenant {
TenantId = tenantId, Subdomain = fixture.Create<Subdomain>()
})
.When(new StartPrivateGroup {
TenantId = tenantId,
GroupId = groupId,
Name = groupName
})
.Then(groupId, new PrivateGroupStarted {
TenantId = tenantId,
GroupId = groupId,
Name = groupName
})
.Assert();
} //anonymous, random values
}
1: public class PrivateGroupScenarios {
2: [Fact] public Task when_starting_a_private_group() {
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
var fixture = new ScenarioFixture();
var tenantId = fixture.Create<TenantId>();
var otherGroupId = fixture.Create<GroupId>();
var groupId = fixture.Create<GroupId>();
var groupName = fixture.Create<GroupName>();
return new Scenario()
.Given(tenantId, new RegisteredAsTenant {
TenantId = tenantId, Subdomain = fixture.Create<Subdomain>()
})
.Given(otherGroupId, new PrivateGroupStarted {
TenantId = tenantId, GroupId = otherGroupId, Name = groupName
})
.When(new StartPrivateGroup {
TenantId = tenantId,
GroupId = groupId,
Name = groupName
})
groupName)).Throws(new GroupNameIsAlreadyTakenException(tenantId,
.Assert();
} // expected failure
}
Designinthe small
When it comes to testing, try to find the right
level of abstraction.
For scenario driven tests that means being able to
change your implementation without having to
touch your tests.
Try to assert that only what you expected occurred
AND NOTHING ELSE.
Keep the boilerplate/distraction out of your tests.
DDDesign Challenges
Q&A

More Related Content

PDF
Socrates BE - Projections Explained
PDF
Projections explained
PDF
ES3-2020-06 Test Driven Development (TDD)
PDF
Google App Engine in 40 minutes (the absolute essentials)
PPTX
Domain Driven Design - DDDSydney 2011
PDF
Domain Driven Design Up And Running
KEY
Event sourcing
PDF
Event based modeling - eng
Socrates BE - Projections Explained
Projections explained
ES3-2020-06 Test Driven Development (TDD)
Google App Engine in 40 minutes (the absolute essentials)
Domain Driven Design - DDDSydney 2011
Domain Driven Design Up And Running
Event sourcing
Event based modeling - eng

Viewers also liked (8)

PPTX
Unleash Your Domain With Greg Young @ DDD-Day
PDF
DDD Dirty Harry style
KEY
Loosely Coupled Complexity - Unleash the power of your Domain Model with Comm...
PPTX
Introdução ao Domain-Driven Design
PDF
Model storming
PPTX
Greg Young on Architectural Innovation: Eventing, Event Sourcing
PPTX
A Practical Guide to Domain Driven Design: Presentation Slides
PDF
Event storming recipes
Unleash Your Domain With Greg Young @ DDD-Day
DDD Dirty Harry style
Loosely Coupled Complexity - Unleash the power of your Domain Model with Comm...
Introdução ao Domain-Driven Design
Model storming
Greg Young on Architectural Innovation: Eventing, Event Sourcing
A Practical Guide to Domain Driven Design: Presentation Slides
Event storming recipes
Ad

Similar to DDDesign Challenges (20)

PDF
Design in the small
KEY
Solid principles
PPTX
Object Oriented Analysis and Design UNIT II
PPTX
Functional DDD
PDF
Epic.NET: Processes, patterns and architectures
PDF
Avoiding Over Design and Under Design
PPTX
Object Oriented System Design
PDF
Design Principles Behind PATAGONIA
PDF
Structured Software Design
PDF
Game Programming 04 - Style & Design Principles
PPTX
introofUML.pptx
PDF
Aggregates, Entities and Value objects - Devnology 2010 community day
PPTX
Software System Architecture-Lecture 6.pptx
PPTX
Domain Driven Design
PPTX
Designing DDD Aggregates
PPT
Object Oriented Analysis and Design with UML2 part2
PPT
08 class and sequence diagrams
PPTX
Code Quality
PPTX
Code quality
PDF
Responsibility Driven Design
Design in the small
Solid principles
Object Oriented Analysis and Design UNIT II
Functional DDD
Epic.NET: Processes, patterns and architectures
Avoiding Over Design and Under Design
Object Oriented System Design
Design Principles Behind PATAGONIA
Structured Software Design
Game Programming 04 - Style & Design Principles
introofUML.pptx
Aggregates, Entities and Value objects - Devnology 2010 community day
Software System Architecture-Lecture 6.pptx
Domain Driven Design
Designing DDD Aggregates
Object Oriented Analysis and Design with UML2 part2
08 class and sequence diagrams
Code Quality
Code quality
Responsibility Driven Design
Ad

Recently uploaded (20)

PPTX
Transform Your Business with a Software ERP System
PPT
Introduction Database Management System for Course Database
PDF
Addressing The Cult of Project Management Tools-Why Disconnected Work is Hold...
PDF
Softaken Excel to vCard Converter Software.pdf
PDF
PTS Company Brochure 2025 (1).pdf.......
PDF
Adobe Illustrator 28.6 Crack My Vision of Vector Design
PDF
Odoo Companies in India – Driving Business Transformation.pdf
PDF
How to Choose the Right IT Partner for Your Business in Malaysia
PPTX
Introduction to Artificial Intelligence
PPTX
Computer Software and OS of computer science of grade 11.pptx
PPTX
CHAPTER 2 - PM Management and IT Context
PDF
System and Network Administration Chapter 2
PDF
top salesforce developer skills in 2025.pdf
PDF
Wondershare Filmora 15 Crack With Activation Key [2025
PDF
T3DD25 TYPO3 Content Blocks - Deep Dive by André Kraus
PDF
Design an Analysis of Algorithms II-SECS-1021-03
PDF
Design an Analysis of Algorithms I-SECS-1021-03
PDF
Internet Downloader Manager (IDM) Crack 6.42 Build 42 Updates Latest 2025
PDF
Designing Intelligence for the Shop Floor.pdf
PDF
How to Migrate SBCGlobal Email to Yahoo Easily
Transform Your Business with a Software ERP System
Introduction Database Management System for Course Database
Addressing The Cult of Project Management Tools-Why Disconnected Work is Hold...
Softaken Excel to vCard Converter Software.pdf
PTS Company Brochure 2025 (1).pdf.......
Adobe Illustrator 28.6 Crack My Vision of Vector Design
Odoo Companies in India – Driving Business Transformation.pdf
How to Choose the Right IT Partner for Your Business in Malaysia
Introduction to Artificial Intelligence
Computer Software and OS of computer science of grade 11.pptx
CHAPTER 2 - PM Management and IT Context
System and Network Administration Chapter 2
top salesforce developer skills in 2025.pdf
Wondershare Filmora 15 Crack With Activation Key [2025
T3DD25 TYPO3 Content Blocks - Deep Dive by André Kraus
Design an Analysis of Algorithms II-SECS-1021-03
Design an Analysis of Algorithms I-SECS-1021-03
Internet Downloader Manager (IDM) Crack 6.42 Build 42 Updates Latest 2025
Designing Intelligence for the Shop Floor.pdf
How to Migrate SBCGlobal Email to Yahoo Easily

DDDesign Challenges

  • 3. AboutMe A journeylist with a passion for well-designed, properly decomposed, working software. Professional experience: 20+ years, mainly building products, in domains such as Patient logistics, Real estate, Manufacturing, Construction, Transportation, Oil & gas Community experience: DDDBE member - Occasional speaker, blogger and discusser of (D)DDD, CQRS & ES and Messaging and author of AggregateSource and Projac
  • 4. ContactMe Online: Twitter: @yreynhout or @bittacklr, Skype: yves.reynhout, Mail: yves.reynhout@gmail.com or info@bittacklr.be Meet the Company: BitTacklr BVBA based in Belgium
  • 7. Why do we fail to design?
  • 8. Because ...{reasons} Not a habit (inexperience) No (real) domain expertise No (or too little) communication/collaboration No time to learn the problem/solution space No (or too little) design up front ( coz Agile ;-) ) No documentation/visualization
  • 9. Because ...{reasons} Wrong team or size (consensus) Second system syndrome in brown‹eld Option paralysis in green‹eld ...
  • 11. Design in the small vs the large
  • 14. 1: public class Group { 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: Guid Id { get; set; } string Name { get; set; } string Objective { get; set; } GroupType Type { get; set; } public public public public //... } //Usage var @group = new Group { Id = message.GroupId, Name = message.Name, Objective = message.Objective, Type = GroupType.Private };
  • 15. 1: public class Group { 2: public Group( 3: Guid id, 4: string name, string objective, 5: GroupType type) 6: { 7: //... 8: } } 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: //Usage var @group = new Group( message.GroupId, message.Name, message.Objective, GroupType.Private );
  • 16. 1: public class PrivateGroup { 2: private PrivateGroup(/* ... */) 3: { 4: /* ... */ } 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: public static PrivateGroup Start( GroupId id, GroupName name, GroupObjective objective) { //... } } //Usage var @group = PrivateGroup.Start( new GroupId(message.GroupId), new GroupName(message.Name), GroupObjective.InferFrom(message.Objective ) );
  • 17. "id"); "name"); 1: public class Group { 2: public Group( 3: Guid id, 4: string name, string objective, 5: GroupType type) 6: { 7: if(id == Guid.Empty) 8: throw new ArgumentException("...", if(string.IsNullOrEmpty(name)) 9: throw new ArgumentException("...", 10: if(name.Length > 255) 11: throw new ArgumentException("...", 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: "name"); if(objective != null && objective.Length == 0) throw new ArgumentException("...", "objective"); if(objective != null && objective.Length > 500) new ArgumentException("...", != GroupType.Private && type new ArgumentException("...", "objective"); != GroupType.Public) "type"); throw if(type throw //... } }
  • 18. 1: public class PrivateGroup { 2: public static PrivateGroup Start( 3: GroupId id, 4: GroupName name, GroupObjective objective) 5: { 6: //... 7: } 8: } 9:
  • 19. 1: public class GroupId { 2: private readonly Guid value; 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: public GroupId(Guid value) { if(value == Guid.Empty) throw new ArgumentException("...", "value"); this.value = value; } return false; public override bool Equals(object other) { if(other == null || other.GetType() != this.GetType()) return ((GroupId)other).value.Equals(this.value); } public override int GetHashCode() { return this.value.GetHashCode(); } //... }
  • 20. 1: public class GroupName { 2: private readonly string value; 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: public GroupName(string value) { if(string.IsNullOrEmpty(value)) throw new ArgumentException("...", "value"); if(value.Length > Metadata.MaximumGroupNameLength) throw new ArgumentException("...", "value"); this.value = value; } //... } //Usage @group.Rename(new GroupName(message.Name));
  • 21. 1: public class GroupObjective { 2: private static readonly GroupObjective NotSpecified = 3: new GroupObjective(null); 4: private readonly string value; 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: private GroupObjective(string value) { this.value = value; } public static GroupObjective InferFrom(string value) { if(value == null) return NotSpecified; if(value.Length == 0) throw new ArgumentException("...", "value"); if(value.Length > Metadata.MaximumGroupObjectiveLength) throw new ArgumentException("...", "value"); return new GroupObjective(value); } }
  • 22. 1: public class GroupObjective { 2: //... 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: public static GroupObjective InferFromMarkdown(string value) { if(value == null) return NotSpecified; MarkDownDocument parsedDocument; if(!MarkDownParser.TryParse(value, out parsedDocument)) throw new ArgumentException("...", "value"); var document = parsedDocument.Sanitize(); if(document.Text.Length == 0) throw new ArgumentException("...", "value"); if(document.Text.Length > Metadata.MaximumGroupObjectiveLength) throw new ArgumentException("...", "value"); return new GroupObjective(document.ToString()); } }
  • 23. 1: public class GroupObjective { 2: //... 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: public static GroupObjective InferFromMarkdown(MarkDownDocument value) { if(value == null) return NotSpecified; var document = value.Sanitize(); if(document.Text.Length == 0) throw new ArgumentException("...", "value"); if(document.Text.Length > Metadata.MaximumGroupObjectiveLength) throw new ArgumentException("...", "value"); return new GroupObjective(document.ToString()); } }
  • 25. 1: public class FillPercentage { 2: private readonly double value; 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: public FillPercentage(double value) { if(value < 0) throw new ArgumentOutOfRangeException("value", "The fill percentage must be greater than or value, equal to 0."); this.value = value; } public static FillPercentage FromPercentNotation(double value) { return new FillPercentage(value / 100d); } instance)public static implicit operator double(FillPercentage { return instance.value; } }
  • 26. 1: public class MaximumFillPercentage { 2: private readonly double value; 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: public MaximumFillPercentage(double value) { if(value < 1) throw new ArgumentOutOfRangeException("value", value, "The maximum fill percentage must be greater than or equal to 1. this.value = value; } public static MaximumFillPercentage FromPercentNotation(double value) { return new MaximumFillPercentage(value / 100d); } instance)public static implicit operator double(MaximumFillPercentage { return instance.value; } }
  • 27. 1: public class MaximumFillPercentage : IComparable<FillPercentage> { 2: //... 3: 4: 5: 6: 7: 8: public int CompareTo(FillPercentage other) { return this.value.Compare(other); } }
  • 28. 1: public class MaximumFillPercentage : IComparable<FillPercentage> { 2: //... 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: public static bool operator < (MaximumFillPercentage left, FillPercentage right) { return left.CompareTo(right) == 1; } right) {FillPercentage <= 0; public static bool operator <= (MaximumFillPercentage left, return left.CompareTo(right) } right) {FillPercentage == 1; public static bool operator > (MaximumFillPercentage left, return left.CompareTo(right) } right) {FillPercentage >= 0; public static bool operator >= (MaximumFillPercentage left, return left.CompareTo(right) } }
  • 29. 1: public class FillPercentage { 2: //... 3: 4: 5: 6: 7: public bool Exceeds(MaximumFillPercentage maximum) { return maximum < this; } }
  • 30. 1: var tryoutSession = session.Add(timeslot); 2: 3: 4: 5: 6: 7: if (tryoutSession .CurrentFillPercentage .Exceeds(session.MaximumFillPercentage)) { throw new MaximumFillPercentageExceededException(/* ... */); }
  • 31. Designinthe small Value Objects act as an attractor for behaviour, such as validation, security, arithmetic, composition, ... They are the primitives of your model. They keep the repetition out of the wrong places and stimulate the use of acceptable values. They promote domain specific language in both their name and behaviors. They compose well and move us away from the procedural landscape. Still, don't go overboard (e.g. message obsession).
  • 33. 1: public class PrivateGroupBehavior : BehaviorModule { 2: public PrivateGroupBehavior( 3: ITenantRepository tenantRepository, 4: IPrivateGroupRepository groupRepository) { 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: Receive<StartPrivateGroup>(async message => { var tenant .Get(new = await tenantRepository TenantId(message.TenantId)) ; var @group = tenant.StartPrivateGroup( new GroupId(message.GroupId), new GroupName(message.GroupName); GroupObjective.InferFrom(message.GroupObjective)) ; groupRepository.Add(@group); }); } }
  • 34. 1: public class PrivateGroupScenarios { 2: [Fact] public async Task when_starting_a_private_group() { 3: using(var context = new GroupContext()) { 4: //Arrange context.Tenants.Add( 5: new TenantData { TenantId = 123, Subdomain = "nespresso" }); 6: var module = new PrivateGroupBehavior( 7: new TenantRepository(context), 8: new PrivateGroupRepository(context) ); //Act 9: await module.Send(new StartPrivateGroup { 10: TenantId = 123, 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: GroupId = new Guid("a9a620cac8c241b7b7c0fc03881ed00d"), Name = "Prototype Retail Machines" }); //Assert Assert.Equal(1, context.PrivateGroups.Count()); GroupData actual = context.PrivateGroups.Single(); Assert.Equal(123, actual.TenantId); Assert.Equal(new Guid("a9a620cac8c241b7b7c0fc03881ed00d"), actual.GroupId); Assert.Equal("Prototype Retail Machines", actual.Name); } } }
  • 35. 1: public class PrivateGroupScenarios { 2: [Fact] public Task when_starting_a_private_group() { 3: var tenantId = new Random().Next(1, 100); 4: var groupId = Guid.NewGuid(); return new Scenario() 5: .Given(tenantId, new TenantSnapshot { 6: TenantId = tenantId, 7: Subdomain = "nespresso" 8: }) .When(new StartPrivateGroup { 9: TenantId = tenantId, 10: GroupId = groupId, 11: Name = "Prototype Retail Machines" 12: }) 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: .Then(groupId, new PrivateGroupSnapshot { TenantId = tenantId, GroupId = groupId, Machines"Name = "Prototype Retail }) .Assert(); } //statebased }
  • 36. 1: public class PrivateGroupScenarios { 2: [Fact] public Task when_starting_a_private_group() { 3: var tenantId = new Random().Next(1, 100); 4: var groupId = Guid.NewGuid(); return new Scenario() 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: .Given(tenantId, new RegisteredAsTenant { TenantId = tenantId, Subdomain = "nespresso" }) .When(new StartPrivateGroup { TenantId = tenantId, GroupId = groupId, Name = "Prototype Retail Machines" }) .Then(groupId, new PrivateGroupStarted { TenantId = tenantId, GroupId = groupId, Machines"Name = "Prototype Retail }) .Assert(); } //eventdriven }
  • 37. 1: public class PrivateGroupScenarios { 2: [Fact] public Task when_starting_a_private_group() { 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: var tenantId = new TenantId(new Random().Next(1, 100)); var var var subdomain groupId = groupName = new Subdomain("nespresso"); new GroupId(Guid.NewGuid()); = new GroupName("Prototype Retail Machines"); return new Scenario() .Given(tenantId, new RegisteredAsTenant { TenantId = tenantId, Subdomain = subdomain }) .When(new StartPrivateGroup { TenantId = tenantId, GroupId = groupId, Name = groupName }) .Then(groupId, new PrivateGroupStarted { TenantId = tenantId, GroupId = groupId, Name = groupName }) .Assert(); } //use of value objects }
  • 38. 1: public class PrivateGroupScenarios { 2: [Fact] public Task when_starting_a_private_group() { 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: var fixture = new ScenarioFixture(); var tenantId = fixture.Create<TenantId>(); var groupId = fixture.Create<GroupId>(); var groupName = fixture.Create<GroupName>(); return new Scenario() .Given(tenantId, new RegisteredAsTenant { TenantId = tenantId, Subdomain = fixture.Create<Subdomain>() }) .When(new StartPrivateGroup { TenantId = tenantId, GroupId = groupId, Name = groupName }) .Then(groupId, new PrivateGroupStarted { TenantId = tenantId, GroupId = groupId, Name = groupName }) .Assert(); } //anonymous, random values }
  • 39. 1: public class PrivateGroupScenarios { 2: [Fact] public Task when_starting_a_private_group() { 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: var fixture = new ScenarioFixture(); var tenantId = fixture.Create<TenantId>(); var otherGroupId = fixture.Create<GroupId>(); var groupId = fixture.Create<GroupId>(); var groupName = fixture.Create<GroupName>(); return new Scenario() .Given(tenantId, new RegisteredAsTenant { TenantId = tenantId, Subdomain = fixture.Create<Subdomain>() }) .Given(otherGroupId, new PrivateGroupStarted { TenantId = tenantId, GroupId = otherGroupId, Name = groupName }) .When(new StartPrivateGroup { TenantId = tenantId, GroupId = groupId, Name = groupName }) groupName)).Throws(new GroupNameIsAlreadyTakenException(tenantId, .Assert(); } // expected failure }
  • 40. Designinthe small When it comes to testing, try to find the right level of abstraction. For scenario driven tests that means being able to change your implementation without having to touch your tests. Try to assert that only what you expected occurred AND NOTHING ELSE. Keep the boilerplate/distraction out of your tests.
  • 42. Q&A