Question

J'ai récemment abordé une question intéressante, que devrait revenir sur les méthodes courantes? Devraient-ils changer l'état de l'objet actuel ou en créer un tout nouveau avec un nouvel état?

Dans le cas où cette brève description n'est pas très intuitive, voici un exemple (malheureusement) long. C'est une calculatrice. Il effectue des calculs très lourds et c'est pourquoi il renvoie les résultats via un rappel asynchrone:

public interface ICalculator {
    // because calcualations are too lengthy and run in separate thread
    // these methods do not return values directly, but do a callback
    // defined in IFluentParams
    void Add(); 
    void Mult();
    // ... and so on
}

Alors, voici une interface fluide qui définit les paramètres et les rappels:

public interface IFluentParams {
    IFluentParams WithA(int a);
    IFluentParams WithB(int b);
    IFluentParams WithReturnMethod(Action<int> callback);
    ICalculator GetCalculator();
}

J'ai deux options intéressantes pour cette implémentation d'interface. Je les montrerai tous les deux, puis j'écrirai ce que je trouve bien et mauvais chacun d'eux.

Donc, d'abord est habituel, qui revient cette:

public class FluentThisCalc : IFluentParams {
    private int? _a;
    private int? _b;
    private Action<int> _callback;

    public IFluentParams WithA(int a) {
        _a = a;
        return this;
    }

    public IFluentParams WithB(int b) {
        _b = b;
        return this;
    }

    public IFluentParams WithReturnMethod(Action<int> callback) {
        _callback = callback;
        return this;
    }

    public ICalculator GetCalculator() {
        Validate();
        return new Calculator(_a, _b);
    }

    private void Validate() {
        if (!_a.HasValue)
            throw new ArgumentException("a");
        if (!_b.HasValue)
            throw new ArgumentException("bs");
    }
}

La deuxième version est plus compliquée, il renvoie un nouvel objet À chaque changement d'état:

public class FluentNewCalc : IFluentParams {
    // internal structure with all data
    private struct Data {
        public int? A;
        public int? B;
        public Action<int> Callback;

        // good - data logic stays with data
        public void Validate() {
            if (!A.HasValue)
                throw new ArgumentException("a");
            if (!B.HasValue)
                throw new ArgumentException("b");
        }
    }

    private Data _data;

    public FluentNewCalc() {
    }

    // used only internally
    private FluentNewCalc(Data data) {
        _data = data;
    }

    public IFluentParams WithA(int a) {
        _data.A = a;
        return new FluentNewCalc(_data);
    }

    public IFluentParams WithB(int b) {
        _data.B = b;
        return new FluentNewCalc(_data);
    }

    public IFluentParams WithReturnMethod(Action<int> callback) {
        _data.Callback = callback;
        return new FluentNewCalc(_data);
    }

    public ICalculator GetCalculator() {
        Validate();
        return new Calculator(_data.A, _data.B);
    }

    private void Validate() {
        _data.Validate();
    }
}

Comment comparent-ils:

Pro d'abord (cette) version:

  • plus facile et plus court

  • fréquemment utilisé

  • semble être plus économe en mémoire

  • quoi d'autre?

Pro seconde (Nouveau) version:

  • stocke les données dans un conteneur séparé, permet de séparer la logique des données et toute manipulation

  • permettez nous de facilement Corrigez une partie des données, puis remplissez d'autres données et gérez-les séparément. Regarde:

        var data = new FluentNewCalc()
            .WithA(1);
    
        Parallel.ForEach(new[] {1, 2, 3, 4, 5, 6, 7, 8}, b => {
            var dt = data
                .WithB(b)
                .WithReturnMethod(res => {/* some tricky actions */});
    
            // now, I have another data object for each value of b, 
            // and they have different callbacks.
            // if I were to do it with first version, I would have to create each 
            // and every data object from scratch
            var calc = dt.GetCalculator();
            calc.Add();
        });
    

Quel pourrait être encore mieux dans la deuxième version?

  • Je pourrais implémenter une méthode WithXxx comme ceci:

    public IFluentParams WithXXX(int xxx) {
        var data = _data;
        data.XXX = xxx;
        return new FluentNewCalc(data);
    }
    

    et faire _data Readonly (c'est-à-dire immuable) que certaines personnes intelligentes disent être bonnes.

La question est donc de savoir quelle voie pensez-vous mieux et pourquoi? PS J'ai utilisé C # mais le pourrait bien s'appliquer à Java.

Était-ce utile?

La solution

Lorsque j'essaie de répondre à une telle question dans la conception de ma demande, je pense toujours à ce qu'une personne qui utilise mon code dans son application s'attendrait.

Prenez, pour Instace, le C # DateTime taper. C'est une structure et donc immuable. Lorsque vous demandez

var today = DateTime.Now;
var tomorrow = today.AddDays(1);

Que vous attendriez-vous si vous ne saviez pas cela DateTime est immuable? Je ne m'attendrais pas à ce qu'aujourd'hui soit soudainement demain, ce serait le chaos.

Quant à votre exemple, j'imagine que les nombres sont traités en utilisant une seule instance de la calculatrice, sauf si je décide autrement. Cela a du sens, non? Lorsque j'écris une équation, je n'écris pas chaque expression sur une nouvelle ligne. J'écris le tout avec un résultat, puis je passe à la ligne suivante afin de séparer les préoccupations.

Alors

var calc = new Calculator(1);
calc.Add(1);
calc.PrintCurrentValue(); // imaginary method for printing of a current value of equation

Cela me semble parfaitement sensé.

Autres conseils

J'ai tendance à supposer que les méthodes courantes le rendront. Cependant, vous soulevez un bon point concernant la mutabilité qui m'a attiré lors du test. En quelque sorte en utilisant votre exemple, je pourrais faire quelque chose comme:

var calc = new Calculator(0);
var newCalc = calc.Add(1).Add(2).Mult(3);
var result = calc.Add(1);

Lors de la lecture du code, je pense que beaucoup de gens supposaient que le résultat serait 1 Comme ils verraient Calc + 1. de cause avec un système fluide mutable, la réponse serait différente comme le Add(1).Add(2).Mult(3) serait appliqué.

Les systèmes fluide immuables sont cependant plus difficiles à mettre en œuvre, nécessitant un code plus complexe. Il semble que ce soit très subjectif de savoir si le bénéfice d'immuabilité l'emporte sur le travail requis pour les mettre en œuvre.

N'est-ce pas pour l'inférence de type, on pouvait "prendre le meilleur des deux mondes" en mettant en œuvre non seulement un immuable FluentThing classe définie dans l'API, mais une autre, mutable, FluentThingInternalUseOnly qui a soutenu l'élargissement de la conversion en FluentThing. Les membres courants sur FluentThing construire une nouvelle instance de FluentThingInternalUseOnly et avoir ce dernier type comme type de retour; les membres de FluentThingInternalUseOnly opérerait et reviendrait, this.

Ceci, si l'on disait FluentThing newThing = oldFluentThing.WithThis(4).WithThat(3).WithOther(57);, la WithThis la méthode construire un nouveau FluentThingInternalUseOnly. Cette même instance serait modifiée et renvoyée par WithThat et WithOther; Les données de celles-ci seraient ensuite copiées dans un nouveau FluentThing dont la référence serait stockée dans newThing.

Le problème majeur avec cette approche est que si quelqu'un dit dim newThing = oldFluentThing.WithThis(3);, alors newThing ne tiendrait pas de référence à un immuable FluentThing, mais un mutable FluentThingInternalUseOnly, et cette chose n'aurait aucun moyen de savoir qu'une référence à cela avait été persistée.

Conceptuellement, ce qui est nécessaire est une façon d'avoir FluentThingInternalUseOnly être suffisamment public pour qu'il puisse être utilisé comme type de retour d'une fonction publique, mais pas le plus public pour permettre au code extérieur de la déclaration de variables de ce type. Malheureusement, je ne sais aucun moyen de le faire, mais peut-être une astuce impliquant Obsolete() Les balises peuvent être possibles.

Sinon, si les objets en cours sont compliqué devrait être fait à cet objet [les méthodes courantes de chaînage créeraient efficacement une liste liée] et une référence évaluée paresseusement à un objet sur lequel toutes les modifications appropriées avaient été appliquées. Si l'on appelait newThing = myThing.WithBar(3).WithBoz(9).WithBam(42), un nouvel objet wrapper serait créé à chaque étape du chemin, et la première tentative d'utilisation newThing comme une chose devrait construire un Thing Instance avec trois modifications s'y appliquait, mais l'original myThing serait intact, et il ne serait que nécessaire de faire une nouvelle instance de Thing plutôt que trois.

Je suppose que tout dépendrait de votre utilisation.

La plupart du temps, lorsque j'utilise un constructeur, c'est pour un seul thread pour manipuler les données mutables. Par conséquent, le retour est préféré car il n'y a pas de frais généraux et de mémoire supplémentaires de retour de nouvelles instances partout.

Cependant, beaucoup de mes constructeurs ont un copy() Méthode qui renvoie une nouvelle instance avec les mêmes valeurs actuelles pour les moments où je dois prendre en charge vos cas d'utilisation "Pro seconde"

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top