20 Stimmen

Builder-Design-Muster mit Vererbung: Gibt es einen besseren Weg?

Ich erstelle eine Reihe von Buildern, um die Syntax zu bereinigen, die Domänenklassen für meine Mocks als Teil der Verbesserung unserer gesamten Unit-Tests erstellt. Meine Builder bestücken im Wesentlichen eine Domänenklasse (z. B. eine Schedule ) mit einigen Werten, die durch Aufrufen der entsprechenden WithXXX und sie miteinander zu verketten.

Ich bin auf einige Gemeinsamkeiten zwischen meinen Buildern gestoßen und möchte diese in eine Basisklasse abstrahieren, um die Wiederverwendung von Code zu erhöhen. Leider sieht das, was ich am Ende habe, so aus:

public abstract class BaseBuilder<T,BLDR> where BLDR : BaseBuilder<T,BLDR> 
                                          where T : new()
{
    public abstract T Build();

    protected int Id { get; private set; }
    protected abstract BLDR This { get; }

    public BLDR WithId(int id)
    {
        Id = id;
        return This;
    }
}

Achten Sie besonders auf die protected abstract BLDR This { get; } .

Eine Beispielimplementierung eines Domain Class Builders ist:

public class ScheduleIntervalBuilder :
    BaseBuilder<ScheduleInterval,ScheduleIntervalBuilder>
{
    private int _scheduleId;
    // ...

    // UG! here's the problem:
    protected override ScheduleIntervalBuilder This
    {
        get { return this; }
    }

    public override ScheduleInterval Build()
    {
        return new ScheduleInterval
        {
            Id = base.Id,
            ScheduleId = _scheduleId
                    // ...
        };
    }

    public ScheduleIntervalBuilder WithScheduleId(int scheduleId)
    {
        _scheduleId = scheduleId;
        return this;
    }

    // ...
}

Da BLDR nicht vom Typ BaseBuilder ist, kann ich nicht verwenden return this im WithId(int) Methode der BaseBuilder .

Wird der untergeordnete Typ mit der Eigenschaft abstract BLDR This { get; } meine einzige Option, oder übersehe ich hier einen Syntaxtrick?

Update (da ich nun etwas deutlicher zeigen kann, warum ich das tue):

Das Endergebnis ist, dass die Builder profilierte Domänenklassen erstellen, von denen man erwarten würde, dass sie in einem für [Programmierer] lesbaren Format aus der Datenbank abgerufen werden. Es ist nichts falsch mit...

mock.Expect(m => m.Select(It.IsAny<int>())).Returns(
    new Schedule
    {
        ScheduleId = 1
        // ...
    }
);

denn das ist schon ziemlich gut lesbar. Die alternative Syntax des Builders ist:

mock.Expect(m => m.Select(It.IsAny<int>())).Returns(
    new ScheduleBuilder()
        .WithId(1)
        // ...
        .Build()
);

den Vorteil, den ich mir von der Verwendung von Buildern erhoffe (und der Implementierung all dieser WithXXX Methoden) ist die Abstrahierung der komplexen Eigenschaftserstellung (automatische Erweiterung unserer Datenbanknachschlagewerte mit den richtigen Lookup.KnownValues ohne auf die Datenbank zuzugreifen) und dass der Builder allgemein wiederverwendbare Testprofile für Domänenklassen bereitstellt...

mock.Expect(m => m.Select(It.IsAny<int>())).Returns(
    new ScheduleBuilder()
        .AsOneDay()
        .Build()
);

16voto

Jon Skeet Punkte 1325502

Ich kann nur sagen, dass, wenn es est eine Möglichkeit, dies zu tun, möchte ich auch darüber Bescheid wissen - ich verwende genau dieses Muster in meinem Protokollpuffer-Anschluss . Ich bin sogar froh, dass jemand anderes darauf zurückgegriffen hat - das bedeutet, dass wir zumindest mit einer gewissen Wahrscheinlichkeit Recht haben!

5voto

dvdvorle Punkte 911

Ich weiß, das ist eine alte Frage, aber ich denke, man kann einen einfachen Wurf verwenden, um die abstract BLDR This { get; }

Der resultierende Code würde dann lauten:

public abstract class BaseBuilder<T, BLDR> where BLDR : BaseBuilder<T, BLDR>
                                           where T : new()
{
    public abstract T Build();

    protected int Id { get; private set; }

    public BLDR WithId(int id)
    {
        _id = id;
        return (BLDR)this;
    }
}

public class ScheduleIntervalBuilder :
    BaseBuilder<ScheduleInterval,ScheduleIntervalBuilder>
{
    private int _scheduleId;
    // ...

    public override ScheduleInterval Build()
    {
        return new ScheduleInterval
        {
                Id = base.Id,
                ScheduleId = _scheduleId
                    // ...
        };
    }

    public ScheduleIntervalBuilder WithScheduleId(int scheduleId)
    {
        _scheduleId = scheduleId;
        return this;
    }

    // ...
}

Natürlich können Sie den Builder auch kapseln mit

protected BLDR This
{
    get
    {
        return (BLDR)this;
    }
}

2voto

Brian Punkte 115257

Dies ist eine gute Implementierungsstrategie für C#.

Einige andere Sprachen (mir fällt kein Name einer Forschungssprache ein, in der ich dies gesehen habe) haben Typsysteme, die entweder ein kovariantes "self"/"this" direkt unterstützen oder andere clevere Möglichkeiten haben, dieses Muster auszudrücken, aber mit dem Typsystem von C# ist dies eine gute (einzige?) Lösung.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X