如何高效地创建和使用构建器模式

发布于 2024-12-14 05:27:49 字数 1886 浏览 2 评论 0原文

在我们的上一个项目中,我们最终为单元测试提供了一个共享的测试装置,这带来了很多问题。因此,在我们当前的项目中,我研究了构建器模式。我们在开发机器的内存中运行单元测试,并针对构建服务器上的数据库运行单元测试。

目前,我有一个 T4 模板,它为学生生成以下构建器:

public class StudentBuilder : Builder<Student, StudentBuilder>
{
    public StudentBuilder()
    {
        IsMale = true;
    }

    public StudentBuilder WithFirstName(string firstName)
    {
        this.FirstName = firstName;
        return this;
    }

    public StudentBuilder WithLastName(string lastName)
    {
        this.LastName = lastName;
        return this;
    }

    public StudentBuilder WithIsMale(bool isMale)
    {
        this.IsMale = isMale;
        return this;
    }

    internal override Student Construct()
    {
        Student result = new Student()
        {
            FirstName = FirstName ?? "FirstName:" + id.ToString(),
            LastName = LastName ?? "LastName:" + id.ToString(),
            IsMale = IsMale,
            Id = id,
        };

     /   return result;
    }
}

通过基类,我可以按以下方式使用它:

Student wouter = StudentBuilder.Build()
    .WithFirstName("Wouter")
    .WithLastName("de Kort");
List<Student> students = StudentBuilder.Build().Multiple(10, (builder, index) => builder.WithFirstName("FirstName" + index));

我们在构建服务器上运行集成测试,以确保一切都针对数据库运行。这意味着我们必须确保满足所有引用约束。 但随后问题就开始了。

例如,一个学生需要有一个导师,一个导师属于一所学校,一所学校属于一个城市,一个城市属于一个......。

这将导致代码如下:

StudentBuilder.Build().WithMentor(MentorBuilder.Build().WithSchool(SchoolBuilder.Build().WithCity(CityBuilder.Build()))

我应该如何优化这个?我考虑过在每个 Builder 的 Construct 方法中进行“默认构建”,但如果我要构建 10 个学生,那么这将导致 10 个城市的 10 所学校的 10 名导师......

或者也许创建像 WithAllCity 这样的方法(..)、WithAll(学校)

有什么想法吗?我实际上是否以正确的方式使用了构建器模式?导演课程有帮助吗?或者我应该从 StudentBuilder 继承类来解决这些不同的情况?

或者另一个想法,在将数据发送到数据库之前,我应该在服务层中添加更多验证吗?然后我会在针对内存数据库的单元测试中发现更多错误。

On our last project we ended up with a shared test fixture for our unit tests which gave a lot of problems. So on our current project I've looked into the builder pattern. We run our unit tests in memory on the development machines and against the database on the build server.

Currently I have a T4 template which produces for example the following builder for a Student:

public class StudentBuilder : Builder<Student, StudentBuilder>
{
    public StudentBuilder()
    {
        IsMale = true;
    }

    public StudentBuilder WithFirstName(string firstName)
    {
        this.FirstName = firstName;
        return this;
    }

    public StudentBuilder WithLastName(string lastName)
    {
        this.LastName = lastName;
        return this;
    }

    public StudentBuilder WithIsMale(bool isMale)
    {
        this.IsMale = isMale;
        return this;
    }

    internal override Student Construct()
    {
        Student result = new Student()
        {
            FirstName = FirstName ?? "FirstName:" + id.ToString(),
            LastName = LastName ?? "LastName:" + id.ToString(),
            IsMale = IsMale,
            Id = id,
        };

     /   return result;
    }
}

Trough the base classes I can use this in the following way:

Student wouter = StudentBuilder.Build()
    .WithFirstName("Wouter")
    .WithLastName("de Kort");
List<Student> students = StudentBuilder.Build().Multiple(10, (builder, index) => builder.WithFirstName("FirstName" + index));

We run integration tests on our build server to make sure everything works against the database. This means we have to make sure that all referential constrains are met.
But then the problems begin.

For example, a student is required to have a mentor, a mentor belongs to a school, a school to a city, a city to a ....

This would result in code like:

StudentBuilder.Build().WithMentor(MentorBuilder.Build().WithSchool(SchoolBuilder.Build().WithCity(CityBuilder.Build()))

How should I optimize this? I've thought about doing the 'default building' in the Construct method of each Builder but if I would build 10 students then it would lead to 10 mentors in 10 schools in 10 cities in 10....

Or maybe creating methods like WithAllCity(..), WithAll(School)

Any ideas? Am I actually using the Builder Pattern the right way? Could a Director class help? Or should I have inherited classes from StudentBuilder which solve these different cases?

Or another idea, should I add more validation in my service layer before sending the data to the database? Then I would catch more errors in my unit tests against the in memory database.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

感情废物 2024-12-21 05:27:49

如果你的单元测试将使用学生的导师、导师的学校和学校的城市,我认为单元测试有代码来构建所有这些是合理的,但我建议你的单元测试可能不会测试只是一件事。让您的单元测试更加具体,这样它们就不会深入研究如此多的属性。

如果问题不是你的单元测试,而是你的学生类要求将一个导师输入到其构造函数中,并且该导师不能为空,请考虑放宽该要求以允许空导师(我想是我的偏好),或者使构建器按照您所说填写“默认”对象。如果您尝试访问默认对象的属性,您甚至可以使默认对象抛出异常,提示您单元测试需要您构建“实际”对象。

If your unit test is going to be using the student's mentor, the mentor's school, and the school's city, I think it is reasonable for the unit test to have code to build all of that, but I suggest your unit test might not be testing just one thing. Make your unit tests more specific so that they are not drilling down through so many properties.

If the problem is not your unit tests, but that your student class demands a mentor to be fed into its constructor, and that mentor cannot be null, consider relaxing that requirement to allow a null mentor (my preference I suppose), or make the builder fill in a "default" object as you say. You could even make your default objects throw exceptions if you try to access their properties, prompting you that your unit test needs you to build an "actual" object.

╰沐子 2024-12-21 05:27:49

如果您要构建学生列表,您可以创建一个列表构建器类 - StudentsBuilder。默认情况下,构建器类将生成一个由您定义的伪随机属性的学生列表。这类似于 AutoPoco 的方法。

我发现制作自己的列表生成器 class 在定义创建行为和支持任何类型的类方面更加灵活。我使用 IList 字段创建一个构建器类(类似于面向数据的数组结构 (SoA) 方法)。

public class StudentsBuilder
{
    private int _size;
    private IList<string> _firstNames; 
    private IList<string> _lastNames;
    private IList<MentorBuilder> _mentors;

    public StudentsBuilder(int size = 10)
    {
        _size = 10;
        _firstNames = new RandomStringGenerator(size).Generate();
        _lastNames = new RandomStringGenerator(size).Generate();
        _mentors = Enumerable.Range(0, size).Select(_ => new MentorBuilder()).ToList();
    }

    public StudentsBuilder WithFirstNames(params string[] firstNames)
    {
        _firstNames = firstNames;
        return this;
    }

    public IList<Student> Build()
    {
        students = new List<Student>();
        for (int i = 0; i < size; i++)
            students.Add(new Student(_firstNames[i], _lastNames[i], _mentors[i].Build());
        return students;
    }
}

每个字段列表都使用采用 params 数组参数的单独方法进行覆盖。您还可以将字段列表公开,以便使用更奇特的 With(Actionaction) 语法来覆盖值。测试代码如下:

var students = new StudentBuilder(size: 4)
    .WithFirstNames("Jim", "John", "Jerry", "Judy")
    .Build();

If you are going to building lists of students you can make a list builder class - StudentsBuilder. By default the builder class will generate a list of Students will psuedo-random properties defined by you. This is similar to the approach of AutoPoco.

I find that making your own list builder class is more flexible in terms of defining the creation behavior and supporting any type of class. I make a builder class with IList<T> fields (similar to a data-oriented structure of arrays (SoA) approach).

public class StudentsBuilder
{
    private int _size;
    private IList<string> _firstNames; 
    private IList<string> _lastNames;
    private IList<MentorBuilder> _mentors;

    public StudentsBuilder(int size = 10)
    {
        _size = 10;
        _firstNames = new RandomStringGenerator(size).Generate();
        _lastNames = new RandomStringGenerator(size).Generate();
        _mentors = Enumerable.Range(0, size).Select(_ => new MentorBuilder()).ToList();
    }

    public StudentsBuilder WithFirstNames(params string[] firstNames)
    {
        _firstNames = firstNames;
        return this;
    }

    public IList<Student> Build()
    {
        students = new List<Student>();
        for (int i = 0; i < size; i++)
            students.Add(new Student(_firstNames[i], _lastNames[i], _mentors[i].Build());
        return students;
    }
}

Each field list is overridden using a separate method taking a params array argument. You could also make field lists public in order to use a fancier With(Action<StudentsBuilder> action) syntax for overriding values. Test code looks like:

var students = new StudentBuilder(size: 4)
    .WithFirstNames("Jim", "John", "Jerry", "Judy")
    .Build();
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文