6

I'm trying to understand fluent builder pattern by creating the person builder object below. I've written the code as I would like to use it but am having issues with implementing it. My issues are as follows:

  1. When calling HavingJob(), this should create a new job that can then be configured using only applicable methods for a job and ultimately added to the Jobs collection of the person. It feels like should return it so that other fluent job methods can be called on it. Not sure how to implement that awhile allowing chaining at that level and above.
  2. When implementing the IJobBuilder methods, I don't have access to the specific job they created in the HavingJob() method because I need to return the IJobBuilder to restrict the fluent methods to only be the ones related to the job. What is the trick to HavingJob() so that those specific job methods can operate on a specific job while still allowing for chaining?
  3. Once I go down a fluent path that ends with IJobBuilder, I can no longer call Build() or HavingJob() to add additional jobs. Would the answer to that one be to have a separate implementation of IJobBuilder that inherits from PersonBuilder?
    public class Person
    {
        public string Name { get; set; }
        public List<Job> Jobs { get; set; }
        public List<Phone> Phones { get; set; }
    }

    public class Phone
    {
        public string Number { get; set; }
        public string Usage { get; set; }
    }

    public class Job
    {
        public string CompanyName { get; set; }
        public int Salary { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var p = PersonBuilder
                .Create()
                    .WithName("My Name")
                    .HavingPhone("222-222-2222")
                        .WithUsage("CELL")
                    .HavingJob()
                        .WithCompanyName("First Company")
                        .WithSalary(100)
                    .HavingJob()
                        .WithCompanyName("Second Company")
                        .WithSalary(200)
                .Build();

            Console.WriteLine(JsonConvert.SerializeObject(p));
        }
    }

    public class PersonBuilder : IJobBuilder
    {
        protected Person Person;
        public PersonBuilder() { Person = new Person(); }
        public static PersonBuilder Create() => new PersonBuilder();
        public PersonBuilder WithName(string name)
        {
            Person.Name = name;
            return this;
        }

        public PersonBuilder HavingPhone(string phoneNumber)
        {
            // Need instance of phone
            return this;
        }

        public PersonBuilder WithUsage(string phoneUsage)
        {
            // Need instance of phone
            return this;
        }

        public IJobBuilder HavingJob()
        {
            // Need to create a job here and return it so that IJobBuilder methods work on specific instance right?
            return this;
        }

        public Person Build() => Person;

        public IJobBuilder WithCompanyName(string companyName)
        {
            // How do I set the company name if I don't have the job instance here
            job.CompanyName = companyName;
            return this;
        }

        public IJobBuilder WithSalary(int amount)
        {
            // How do I set the salary if I don't have a specific job instance here
            job.Salary = amount;
            return this;
        }
    }

    public interface IJobBuilder
    {
        IJobBuilder WithCompanyName(string companyName);
        IJobBuilder WithSalary(int salary);
    }
Geekn
  • 2,650
  • 5
  • 40
  • 80

1 Answers1

19

Single Responsibility Principle (SRP) and Separation of Concerns (SoC)

A Job Builder should be responsible for building a Job

public interface IJobBuilder {
    IJobBuilder WithCompanyName(string companyName);
    IJobBuilder WithSalary(int salary);
}

public class JobBuilder : IJobBuilder {
    private readonly Job job;

    public JobBuilder() {
        job = new Job();
    }

    public IJobBuilder WithCompanyName(string companyName) {
        job.CompanyName = companyName;
        return this;
    }

    public IJobBuilder WithSalary(int amount) {
        job.Salary = amount;
        return this;
    }

    internal Job Build() => job;
}

A Person Builder should be responsible for building the Person.

public class PersonBuilder {
    protected Person Person;
    
    private PersonBuilder() { Person = new Person(); }

    public static PersonBuilder Create() => new PersonBuilder();

    public PersonBuilder WithName(string name) {
        Person.Name = name;
        return this;
    }

    public PersonBuilder HavingJob(Action<IJobBuilder> configure) {
        var builder = new JobBuilder();
        configure(builder);
        Person.Jobs.Add(builder.Build());
        return this;
    }

    public Person Build() => Person;

}

In the above builder it delegates the building of the job to its responsible builder.

This results in the following refactor

class Program {
    static void Main(string[] args) {
        var p = PersonBuilder
            .Create()
                .WithName("My Name")
                .HavingJob(builder => builder
                    .WithCompanyName("First Company")
                    .WithSalary(100)
                )
                .HavingJob(builder => builder
                    .WithCompanyName("Second Company")
                    .WithSalary(200)
                )
            .Build();

        Console.WriteLine(JsonConvert.SerializeObject(p));
    }
}
Community
  • 1
  • 1
Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • Thanks for the quick response and that indeed would work. Doesn't this approach always require creating a completely separate builder for any type that is not a value type on person. Take a simple list of phone numbers or even a collection of nicknames (I've edited my original post). I just want to add a phone number to a list with the option of setting the phone number usage. Would you create a nickname builder and phonenumber builder just so you can limit the fluent API method options? I'm struggling on a collection where each item can have it's own set of specific fluid method calls. – Geekn Nov 24 '19 at 22:51
  • 1
    @Geekn In a case like that with the phone you could always pass all the details in the function `PersonBuilder HavingPhone(string number, string usage) { .. }` – Nkosi Nov 24 '19 at 22:53
  • Seems like I 've seen a few approaches to this that use generics and inheritance or fluid interfaces with faceted builder. – Geekn Nov 24 '19 at 22:54
  • For sure I could, but I'm stretching that example a bit to show the real issue I'm struggling with. HasPhone.WithCountryCode().CanUseForMarketing() etc... – Geekn Nov 24 '19 at 22:55
  • How do you design fluid on a property that is a collection of items where each item has their own methods is essentially the crux of my question. I've also seen builders that inherit from each other as well. Or some have an interface for each path. – Geekn Nov 24 '19 at 22:55
  • @Nkosi what is the idea behind the Job Builder having a public constructor while the person builder has a private one and is initiated with the static Create? I see most suggestions on builder using the private constructor – kuklei Dec 05 '20 at 22:23