Чи використовую я ланцюжок методів, я повторно використовую об'єкт чи створюю його?


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, безперебійний малюнок може допомогти розрізнити необов’язкові та необхідні параметри (якщо вони потрібні аргументи конструктора, якщо вони не потрібні). А вільно читати досить приємно.
NimChimpsky

8
@NimChimpsky, що трапилося з старомодними властивостями (для C #), і конструктором, який має необхідні поля - не те, що я підриваю Fluent API, я великий шанувальник, але вони часто надмірно використовуються
Chris S

8
@ChrisS, якщо ти покладаєшся на сеттерів (я з Java), ти повинен зробити свої об'єкти змінними, чого ви, можливо, не хочете робити. А також ви отримуєте приємніший інтелігентний текст, коли вживаєте вільно - вимагає менше мислення, іде майже будує ваш об’єкт для вас.
NimChimpsky

1
@NimChimpsky, так, я бачу, наскільки вільний великий стрибок вперед для Java
Chris S

Відповіді:


41

Я б поставив вільну api у свій власний клас "будівельник", відокремлений від об'єкта, який він створює. Таким чином, якщо клієнт не хоче використовувати вільний 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 статті Вікіпедія Fluent Interface із стійкістю, обробкою датами та знущанням над об’єктами.


Редагувати:

Як зазначається з коментарів; ви можете зробити клас Builder статичним внутрішнім класом (всередині автомобіля), а автомобіль можна зробити незмінним. Цей приклад дозволити автомобілю бути незмінним здається трохи дурним; але в більш складній системі, де ви абсолютно не хочете змінювати вміст об'єкта, який будується, ви можете це зробити.

Нижче наведено один приклад того, як робити як статичний внутрішній клас, так і як керувати створенням незмінного об'єкта, який він накопичує:

// 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);

3
@Baqueta це окреслено ефективну Java
Джоша Блоха

6
@Baqueta вимагає читання для java dev, imho.
NimChimpsky

3
Величезною перевагою IMHO є те, що ви можете використовувати цю модель (якщо її належним чином модифіковано), щоб запобігти появі екземплярів об'єкта, що будується, не завершеного виходу із будівельника. Наприклад, ви можете переконатися, що не буде автомобіля з невизначеним кольором.
шарфридж

1
Хм ... Я завжди називав остаточний метод шаблона build()(або Build()) побудовника , а не ім'я типу, який він будує ( Car()у вашому прикладі). Крім того, якщо Carце дійсно незмінний об'єкт (наприклад, всі його поля є readonly), то навіть будівельник не зможе його мутувати, тому Build()метод стає відповідальним за побудову нового екземпляра. Один із способів зробити це - Carмати лише один конструктор, який бере Аргумент як аргумент; тоді 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 для побудови вашого об’єкта. Іншими словами, цей приклад, який ви навели, має сенс IMHO лише в тому випадку, якщо Автомобіль є об'єктом цінності.


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 так і робить) або поверне цей об'єкт для ланцюгової мети або для ефективності пам'яті).

Я колись розробив об’єкт 3D Vector, і для кожної математичної операції я реалізував обидва методи. Для миттєвого методу масштабування:

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);
}

3
+1. Дуже хороший момент. Я насправді не розумію, чому це спричинило негативний вплив. Зауважу, що вибрані вами імена не дуже зрозумілі. Я б назвав їх scale(мутатор) і scaledBy(генератор).
back2dos

Хороший момент, імена могли бути зрозумілішими. Іменування слідувало за умовами інших математичних класів, які я використовував у бібліотеці. Ефект також був зазначений в коментарях до методу javadoc, щоб уникнути плутанини.
XGouchet

3

Я бачу тут кілька проблем, які, на мою думку, можуть бентежити ... Ваш перший рядок у питанні:

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

Ви викликаєте конструктор (новий) і метод створення ... Метод create () майже завжди був би статичним методом або методом builder, і компілятор повинен зафіксувати це як попередження, так і помилку, щоб повідомити вам про це Таким чином, цей синтаксис або неправильний, або має деякі жахливі назви. Але згодом ви не використовуєте обох, тому давайте розберемося.

// 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

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

Другий підхід, очевидно, є недоліком того, що це більше роботи. Ви також повинні вирішити, чи будуть копії, які ви повертаєте, робити дрібними або глибокими копіями: що найкраще може відрізнятися від класу до класу чи методу до методу, тож ви будете або вводити непослідовність, або компрометувати найкращу поведінку. Варто зазначити, що це єдиний варіант для незмінних об'єктів, наприклад, рядків.

Що б ви не робили, не змішуйте та не співпадайте в одному класі!


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);
        }
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.