Upgrade to Pro — share decks privately, control downloads, hide ads and more …

DDDesign Challenges

Sponsored · SiteGround - Reliable hosting with speed, security, and support you can count on. →

DDDesign Challenges

Presentation for fixxup.nl

Avatar for Yves Reynhout

Yves Reynhout

May 26, 2016

Other Decks in Programming

Transcript

  1. About Me 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
  2. 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
  3. Because ... {reasons} Wrong team or size (consensus) Second system

    syndrome in brown‹eld Option paralysis in green‹eld ...
  4. 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 };
  5. 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 );
  6. 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 ) );
  7. "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 //... } }
  8. 1: public class PrivateGroup { 2: public static PrivateGroup Start(

    3: GroupId id, 4: GroupName name, GroupObjective objective) 5: { 6: //... 7: } 8: } 9:
  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(); } //... }
  10. 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));
  11. 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); } }
  12. 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()); } }
  13. 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()); } }
  14. 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; } }
  15. 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; } }
  16. 1: public class MaximumFillPercentage : IComparable<FillPercentage> { 2: //... 3:

    4: 5: 6: 7: 8: public int CompareTo(FillPercentage other) { return this.value.Compare(other); } }
  17. 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) } }
  18. 1: public class FillPercentage { 2: //... 3: 4: 5:

    6: 7: public bool Exceeds(MaximumFillPercentage maximum) { return maximum < this; } }
  19. 1: var tryoutSession = session.Add(timeslot); 2: 3: 4: 5: 6:

    7: if (tryoutSession .CurrentFillPercentage .Exceeds(session.MaximumFillPercentage)) { throw new MaximumFillPercentageExceededException(/* ... */); }
  20. Design in the 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).
  21. 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); }); } }
  22. 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); } } }
  23. 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 }
  24. 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 }
  25. 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 }
  26. 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 }
  27. 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 }
  28. Design in the 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.