При использовании метода цепочки я повторно использую объект или создаю его?

37

При использовании метода цепочки, как:

var car = new Car().OfBrand(Brand.Ford).OfModel(12345).PaintedIn(Color.Silver).Create();

может быть два подхода:

  • Повторно используйте тот же объект, например так:

    public Car PaintedIn(Color color)
    {
        this.Color = color;
        return this;
    }
  • Создавайте новый объект типа Carна каждом шаге, например так:

    public Car PaintedIn(Color color)
    {
        var car = new Car(this); // Clone the current object.
        car.Color = color; // Assign the values to the clone, not the original object.
        return car;
    }

Первый ошибся или это скорее личный выбор разработчика?


Я считаю, что его первый подход может быстро вызвать интуитивный / вводящий в заблуждение код. Пример:

// Create a car with neither color, nor model.
var mercedes = new Car().OfBrand(Brand.MercedesBenz).PaintedIn(NeutralColor);

// Create several cars based on the neutral car.
var yellowCar = mercedes.PaintedIn(Color.Yellow).Create();
var specificModel = mercedes.OfModel(99).Create();

// Would `specificModel` car be yellow or of neutral color? How would you guess that if
// `yellowCar` were in a separate method called somewhere else in code?

Есть предположения?

Арсений Мурзенко
источник
1
Что не так с var car = new Car(Brand.Ford, 12345, Color.Silver);?
Джеймс
12
Телескопический конструктор @James, беглый шаблон может помочь различить необязательные и обязательные параметры (если они являются обязательными аргументами конструктора, если не необязательными). И бегло довольно приятно читать.
НимЧимпски
8
@NimChimpsky, что случилось со старыми добрыми (для C #) свойствами и конструктором, в котором есть обязательные поля - не то, что я использую Fluent API, я большой поклонник, но они часто используются слишком часто
Chris S
8
@ChrisS Если вы полагаетесь на сеттеры (я из Java), вы должны сделать ваши объекты изменчивыми, чего вы, возможно, не захотите делать. И вы также получите более хороший интеллигентный текст при использовании свободного владения - требует меньше размышлений, иде почти конструирует ваш объект для вас.
НимЧимпский
1
@NimChimpsky, да, я вижу, насколько свободно это большой шаг вперед для Java
Крис С.

Ответы:

41

Я бы поместил свободный API в его собственный класс "builder" отдельно от объекта, который он создает. Таким образом, если клиент не хочет использовать свободный API, вы все равно можете использовать его вручную, и он не загрязняет объект домена (придерживаясь принципа единой ответственности). В этом случае будет создано следующее:

  • Car который является объектом домена
  • CarBuilder который держит свободный API

Использование будет таким:

var car = CarBuilder.BuildCar()
    .OfBrand(Brand.Ford)
    .OfModel(12345)
    .PaintedIn(Color.Silver)
    .Build();

CarBuilderКласс будет выглядеть следующим образом (я использую C # именования , здесь):

public class CarBuilder {

    private Car _car;

    /// Constructor
    public CarBuilder() {
        _car = new Car();
        SetDefaults();
    }

    private void SetDefaults() {
        this.OfBrand(Brand.Ford);
          // you can continue the chaining for 
          // other default values
    }

    /// Starts an instance of the car builder to 
    /// build a new car with default values.
    public static CarBuilder BuildCar() {
        return new CarBuilder();
    }

    /// Sets the brand
    public CarBuilder OfBrand(Brand brand) {
        _car.SetBrand(brand);
        return this;
    }

    // continue with OfModel(...), PaintedIn(...), and so on...
    // that returns "this" to allow method chaining

    /// Returns the built car
    public Car Build() {
        return _car;
    }

}

Обратите внимание, что этот класс не будет потокобезопасным (каждому потоку потребуется собственный экземпляр CarBuilder). Также обратите внимание, что хотя свободный интерфейс API является действительно классной концепцией, он, вероятно, является излишним для создания простых доменных объектов.

Эта сделка более полезна, если вы создаете API для чего-то гораздо более абстрактного и более сложного в настройке и исполнении, поэтому он отлично работает в модульном тестировании и в структурах DI. Вы можете увидеть некоторые другие примеры в разделе Java статьи Википедии о свободном интерфейсе с объектами персистентности, обработки дат и фиктивных объектов.


РЕДАКТИРОВАТЬ:

Как отмечено в комментариях; вы можете сделать класс Builder статическим внутренним классом (внутри Car), и Car можно сделать неизменным. Этот пример того, как автомобиль был неизменным, кажется немного глупым; но в более сложной системе, где вы абсолютно не хотите изменять содержимое построенного объекта, вы можете захотеть это сделать.

Ниже приведен пример того, как сделать статический внутренний класс и обработать создание неизменного объекта, который он создает:

// the class that represents the immutable object
public class ImmutableWriter {

    // immutable variables
    private int _times; private string _write;

    // the "complex" constructor
    public ImmutableWriter(int times, string write) {
        _times = times;
        _write = write;
    }

    public void Perform() {
        for (int i = 0; i < _times; i++) Console.Write(_write + " ");
    }

    // static inner builder of the immutable object
    protected static class ImmutableWriterBuilder {

        // the variables needed to construct the immutable object
        private int _ii = 0; private string _is = String.Empty;

        public void Times(int i) { _ii = i; }

        public void Write(string s) { _is = s; }

        // The stuff is all built here
        public ImmutableWriter Build() {
            return new ImmutableWriter(_ii, _is);
        }

    }

    // factory method to get the builder
    public static ImmutableWriterBuilder GetBuilder() {
        return new ImmutableWriterBuilder();
    }
}

Использование будет следующим:

var writer = ImmutableWriter
                .GetBuilder()
                .Write("peanut butter jelly time")
                .Times(2)
                .Build();

writer.Perform();
// console writes: peanut butter jelly time peanut butter jelly time 

Редактировать 2: Пит в комментариях сделал сообщение в блоге об использовании компоновщиков с лямбда-функциями в контексте написания модульных тестов со сложными объектами домена. Это интересная альтернатива, чтобы сделать строителя немного более выразительным.

В случае, если CarBuilderвам нужен этот метод вместо:

public static Car Build(Action<CarBuilder> buildAction = null) {
    var carBuilder = new CarBuilder();
    if (buildAction != null) buildAction(carBuilder);
    return carBuilder._car;
}

Который может быть использован как это:

Car c = CarBuilder
    .Build(car => 
        car.OfBrand(Brand.Ford)
           .OfModel(12345)
           .PaintedIn(Color.Silver);
Spoike
источник
3
@Baqueta это обрисовано в общих чертах эффективная ява
Джоша Блоха
6
@Baqueta требуется чтение для java dev, imho.
НимЧимпски
3
ИМХО огромным преимуществом является то, что вы можете использовать этот шаблон (если он будет изменен соответствующим образом), чтобы не допустить выхода экземпляров незавершенного объекта из компоновщика. Например, Вы можете убедиться, что не будет Автомобиля с неопределенным Цветом.
шарфридж
1
Хм ... Я всегда вызывал последний метод шаблона компоновщика build()(или Build()), а не имя типа, который он создает ( Car()в вашем примере). Кроме того, если Carэто действительно неизменяемый объект (например, все его поля readonly), то даже создатель не сможет изменить его, поэтому Build()метод становится ответственным за создание нового экземпляра. Один из способов сделать это - Carиметь только один конструктор, который принимает в качестве аргумента Builder; тогда Build()метод может просто return new Car(this);.
Даниэль Приден
1
Я сделал блог о другом подходе к созданию строителей на основе лямбд. Пост, вероятно, нуждается в небольшом редактировании. Мой контекст был в основном внутри контекста юнит-теста, но его можно применять и в других областях, если это применимо. Его можно найти здесь: petesdotnet.blogspot.com/2012/05/…
Пит
9

Это зависит от.

Является ли ваш автомобиль Entity или объект Value ? Если автомобиль является сущностью, то важна идентификация объекта, поэтому вы должны вернуть ту же ссылку. Если объект является объектом значения, он должен быть неизменным, то есть единственный способ - каждый раз возвращать новый экземпляр.

Примером последнего будет класс DateTime в .NET, который является объектом значения.

var date1 = new DateTime(2012,1,1);
var date2 = date1.AddDays(1);
// date2 now refers to Jan 2., while date1 remains unchanged at Jan 1.

Однако, если модель является сущностью, мне нравится ответ Спойка об использовании класса builder для построения вашего объекта. Другими словами, этот пример, который вы привели, имеет смысл только в том случае, если Car является ценным объектом.

Пит
источник
1
+1 за вопрос «сущность» против «ценности». Вопрос в том, является ли ваш класс изменчивым или неизменным типом (должен ли этот объект быть изменен?), И полностью зависит от вас, хотя это повлияет на ваш дизайн. Обычно я не ожидаю, что цепочка методов будет работать с изменяемым типом, если только метод не вернул новый объект.
Кейси Кубалл
6

Создайте отдельный статический внутренний конструктор.

Используйте обычные аргументы конструктора для обязательных параметров. И свободно API для необязательно.

Не создавайте новый объект при установке цвета, если вы не переименуете метод NewCarInColour или что-то подобное.

Я хотел бы сделать что-то подобное с этим брендом, как требуется, а остальное необязательно (это java, но ваш выглядит как javascript, но почти уверен, что они взаимозаменяемы с небольшим количеством выбора):

Car yellowMercedes = new Car.Builder(Brand.MercedesBenz).PaintedIn(Color.Yellow).create();

Car specificYellowModel =new Car.Builder(Brand.MercedesBenz).WithModel(99).PaintedIn(Color.Yellow).create();
ним чимпский
источник
4

Самое главное, что какое бы решение вы ни выбрали, оно четко указано в названии метода и / или комментарии.

Стандартов нет, иногда метод возвращает новый объект (большинство методов String делают это) или возвращает этот объект для целей объединения в цепочку или для повышения эффективности использования памяти).

Однажды я разработал трехмерный векторный объект, и для каждой математической операции у меня были реализованы оба метода. Для мгновенного метода шкалы:

Vector3D scaleLocal(float factor){
    this.x *= factor; 
    this.y *= factor; 
    this.z *= factor; 
    return this;
}

Vector3D scale(float factor){
    Vector3D that = new Vector3D(this); // clone this vector
    return that.scaleLocal(factor);
}
XGouchet
источник
3
+1. Очень хороший момент. Я действительно не понимаю, почему это получило отрицательный голос. Однако отмечу, что выбранные вами имена не очень понятны. Я бы назвал их scale(мутатор) и scaledBy(генератор).
back2dos
Хороший вопрос, имена могли бы быть более понятными. Именование соответствовало соглашению других математических классов, которые я использовал из библиотеки. Эффект был также указан в комментариях javadoc метода, чтобы избежать путаницы.
XGouchet
3

Я вижу здесь несколько проблем, которые, я думаю, могут сбить с толку ... Ваша первая строка в вопросе:

var car = new Car().OfBrand(Brand.Ford).OfModel(12345).PaintedIn(Color.Silver).Create();

Вы вызываете конструктор (new) и метод create ... Метод create () почти всегда будет статическим методом или методом построителя, и компилятор должен перехватить его в предупреждении или ошибке, чтобы вы знали, либо Кстати, этот синтаксис либо неверен, либо имеет несколько ужасных названий. Но позже вы не используете оба, так что давайте посмотрим на это.

// Create a car with neither color, nor model.
var mercedes = new Car().OfBrand(Brand.MercedesBenz).PaintedIn(NeutralColor);

// Create several cars based on the neutral car.
var yellowCar = mercedes.PaintedIn(Color.Yellow).Create();
var specificModel = mercedes.OfModel(99).Create();

Опять же, с созданием, но не с новым конструктором. Дело в том, я думаю, что вы ищете метод copy () вместо этого. Так что, если это так, и это просто плохое имя, давайте посмотрим на одну вещь ... вы называете mercedes.Paintedin (Color.Yellow) .Copy () - это должно быть легко посмотреть на это и сказать, что он «окрашен» перед копированием - просто нормальный поток логики, для меня. Поэтому поместите копию в первую очередь.

var yellowCar = mercedes.Copy().PaintedIn(Color.Yellow)

мне легко увидеть, что ты рисуешь копию, делая свою желтую машину.

Дрейк Кларрис
источник
+1 за указание на диссонанс между новым и Create ();
Джошуа Дрейк
1

Первый подход имеет тот недостаток, который вы упомянули, но до тех пор, пока вы четко дадите понять в документах, у любого некомпетентного кодера не должно быть проблем. Весь код цепочки методов, с которым я лично работал, работал таким образом.

Второй подход, очевидно, имеет тот недостаток, что требует больше работы. Вы также должны решить, будут ли возвращаемые копии делать полные или глубокие копии: что лучше всего может варьироваться от класса к классу или от метода к методу, поэтому вы будете либо вносить несогласованность, либо идти на компромисс в отношении лучшего поведения. Стоит отметить, что это единственный вариант для неизменяемых объектов, таких как строки.

Что бы вы ни делали, не смешивайте и не сочетайте в одном классе!

vaughandroid
источник
1

Я бы предпочел думать так же, как механизм «Методы расширения».

public Car PaintedIn(this Car car, Color color)
{
    car.Color = color;
    return car;
}
Амир Карими
источник
0

Это вариант вышеуказанных методов. Различия в том, что в классе Car есть статические методы, которые соответствуют именам методов в Builder, поэтому вам не нужно явно создавать Builder:

Car car = Car.builder().ofBrand(Brand.Ford).ofColor("Green")...

Вы можете использовать те же имена методов, которые вы используете при вызовах связанного компоновщика:

Car car = Car.ofBrand(Brand.Ford).ofColor("Green")...

Кроме того, в классе есть метод .copy (), который возвращает конструктор, заполненный всеми значениями из текущего экземпляра, поэтому вы можете создать вариант для темы:

Car red = car.copy().paintedIn("Red").build();

Наконец, метод .build () компоновщика проверяет, все ли требуемые значения были предоставлены, и выдает их, если они отсутствуют. Может быть предпочтительнее требовать некоторые значения в конструкторе компоновщика и позволить остальным быть необязательными; в этом случае вы захотите один из шаблонов в других ответах.

public enum Brand {
    Ford, Chrysler, GM, Honda, Toyota, Mercedes, BMW, Lexis, Tesla;
}

public class Car {
    private final Brand brand;
    private final int model;
    private final String color;

    public Car(Brand brand, int model, String color) {
        this.brand = brand;
        this.model = model;
        this.color = color;
    }

    public Brand getBrand() {
        return brand;
    }

    public int getModel() {
        return model;
    }

    public String getColor() {
        return color;
    }

    @Override public String toString() {
        return brand + " " + model + " " + color;
    }

    public Builder copy() {
        Builder builder = new Builder();
        builder.brand = brand;
        builder.model = model;
        builder.color = color;
        return builder;
    }

    public static Builder ofBrand(Brand brand) {
        Builder builder = new Builder();
        builder.brand = brand;
        return builder;
    }

    public static Builder ofModel(int model) {
        Builder builder = new Builder();
        builder.model = model;
        return builder;
    }

    public static Builder paintedIn(String color) {
        Builder builder = new Builder();
        builder.color = color;
        return builder;
    }

    public static class Builder {
        private Brand brand = null;
        private Integer model = null;
        private String color = null;

        public Builder ofBrand(Brand brand) {
            this.brand = brand;
            return this;
        }

        public Builder ofModel(int model) {
            this.model = model;
            return this;
        }

        public Builder paintedIn(String color) {
            this.color = color;
            return this;
        }

        public Car build() {
            if (brand == null) throw new IllegalArgumentException("no brand");
            if (model == null) throw new IllegalArgumentException("no model");
            if (color == null) throw new IllegalArgumentException("no color");
            return new Car(brand, model, color);
        }
    }
}
Дэвид Конрад
источник