[JAVA] Être conscient de l'état interne (immuabilité) des objets et des effets secondaires des méthodes

Les objets en programmation orientée objet sont Il existe plusieurs méthodes de gestion de l'état interne en fonction de la méthode de mise en œuvre.

En comprenant correctement cette méthode de gestion d'état interne Les bogues sont difficiles à se produire, et même s'ils se produisent, la cause peut être facilement identifiée. Vous pourrez également créer des programmes faciles à tester.

Dans cet article, j'expliquerai l'immuabilité de l'état interne. Il aborde également brièvement les effets secondaires de la méthode.

Personne cible

――Je voudrais dire que orienté objet en général ... mais les gens qui utilisent le langage Java --Pour la programmation jusqu'à la 2e année

Avant l'explication

Dans cette explication, le code est créé en utilisant "Janken" comme exemple. Les mains Janken (stratégies) et les arbitres suivants sont définis à l'avance. (Le jugement de Janken n'a rien à voir avec ce sujet, je suis donc désolé pour la mise en œuvre approximative)

public enum HandStrategy {
	/**Goo*/
	ROCK,
	/**Par*/
	PAPER,
	/**Choki*/
	SCISSORS
}

public class Referee {

	public static enum Result {
		LEFT_WIN,
		RIGHT_WIN,
		DRAW
	}

	public static Result battle(HandStrategy left, HandStrategy right) {
		if (left == right)
			return Result.DRAW;

		if (left == HandStrategy.ROCK)
			return right == HandStrategy.PAPER ? Result.RIGHT_WIN : Result.LEFT_WIN;
		else if (left == HandStrategy.PAPER)
			return right == HandStrategy.SCISSORS ? Result.RIGHT_WIN : Result.LEFT_WIN;
		else
			return right == HandStrategy.ROCK ? Result.RIGHT_WIN : Result.LEFT_WIN;
	}
}

Quel est l'état interne?

L'état interne d'un objet est la valeur (ou une combinaison de plusieurs) des variables membres (également appelées champs d'instance en Java) que possède chaque objet instancié.

Par exemple, dans les cas suivants

public class Hand {

    private HandStrategy strategy;

    public void setStrategy(HandStrategy strategy) {
        this.strategy = strategy;
    }

    public static void main(String[] args) {
        Hand left = new Hand();
        Hand right = new Hand();

        left.setStrategy(HandStrategy.ROCK);
        right.setStrategy(HandStrategy.PAPER);
    }
}

L'état interne (stratégie) de l'objet gauche est goo ( ROCK), L'état interne («stratégie») de l'objet «droit» est par («PAPIER»). Ce sera.

Le contenu de la variable n'est pas important pour le sujet «invariance» Il classe quand et comment le contenu change ou ne change pas.

Classifications invariantes "Mutable" et "Immuable", Et je vais vous expliquer un peu spécial "apatride".

Mutable-Mutable

Mutable signifie "variable" Un objet dont l'état interne peut changer au milieu.

Définition

public class MutableHand {

	private HandStrategy strategy;

	public void setStrategy(HandStrategy strategy) {
		this.strategy = strategy;
	}

	public HandStrategy getStrategy() {
		return strategy;
	}
}

L'objet a un état (= il y a un champ d'instance), et il existe une méthode pour changer l'état. Cette fois, il existe une méthode pour réécrire intentionnellement l'état, Certaines méthodes peuvent être réécrites sans le vouloir. Plus d'informations à ce sujet dans le chapitre Immuable.

Courir

	private static void execMutable() {
		final MutableHand left = new MutableHand();
		final MutableHand right = new MutableHand();

		left.setStrategy(HandStrategy.ROCK);
		right.setStrategy(HandStrategy.PAPER);
		Referee.battle(left.getStrategy(), right.getStrategy());

		//La deuxième fois que nous changeons de mains et jouons
		left.setStrategy(HandStrategy.SCISSORS);
		Referee.battle(left.getStrategy(), right.getStrategy());
	}

La variable qui stocke l'objet est qualifiée «final», Il est important de noter que cela ne change pas et que l'état interne de l'objet peut être modifié.

Ne créez pas de nouvel objet lors de la lecture du deuxième jeu L'état de l'objet existant est réécrit et exécuté.

Fonctionnalité

Si vous implémentez une classe sans une compréhension invariante, vous vous retrouverez probablement avec cette implémentation Mutable. C'est parce que c'est le plus simple à mettre en œuvre et qu'il y a peu de choses à considérer. D'autre part, l'implémentation Immutable, qui sera expliquée plus loin, a quelques considérations et est assez gênante.

En général, les implémentations de Mutable sont difficiles à écrire des tests, et lorsque des bogues sont introduits, ils ont tendance à être moins reproductibles et prennent plus de temps à identifier la cause. (Reportez-vous à la colonne des inconvénients) Vous ne devez donc pas choisir de manière agressive une implémentation Mutable. Cependant, il est sélectionné pour des raisons négatives, principalement lorsqu'il est difficile de rendre Immuable. (Pourquoi cela peut être difficile plus tard)

mérite

Démérite

Immuable - immuable

Immuable signifie «immuable» Un objet dont l'état interne ne change pas une fois instancié.

Définition

public class ImmutableHand {

	private final HandStrategy strategy;

	public ImmutableHand(HandStrategy strategy) {
		this.strategy = strategy;
	}

	public HandStrategy getStrategy() {
		return strategy;
	}

}

L'objet a un état, mais il n'y a pas de méthode pour changer l'état. L'état est déterminé par le constructeur lors de l'instanciation. Après cela, l'état ne doit pas être changé.

Courir

	private static void execImmutable() {
		final ImmutableHand left = new ImmutableHand(HandStrategy.ROCK);
		final ImmutableHand right = new ImmutableHand(HandStrategy.PAPER);

		Referee.battle(left.getStrategy(), right.getStrategy());

		//Créer une nouvelle instance la deuxième fois
		final ImmutableHand left2 = new ImmutableHand(HandStrategy.SCISSORS);
		Referee.battle(left2.getStrategy(), right.getStrategy());
	}

Après instanciation, l'objet ne peut pas changer son état interne, donc Si vous souhaitez changer de main une seconde fois, vous aurez besoin d'une nouvelle instanciation. Cependant, si vous n'avez pas à changer de main, il est garanti que vous pouvez réutiliser en toute sécurité l'objet précédemment utilisé. S'il est immuable, le contenu ne changera pas.

Fonctionnalité

Les implémentations immuables sont généralement considérées comme supérieures aux implémentations mutables. L'implémentation Immutable peut résoudre certains des problèmes de testabilité et spécifiques aux bogues de l'implémentation Mutable. (Voir la colonne du mérite)

Vous pouvez transmettre des paramètres pour déterminer l'état interne lors de l'instanciation, mais vous ne pouvez pas modifier l'état interne à d'autres moments. Cela garantit que quoi que vous fassiez avec un objet déjà instancié, son état interne ne changera pas, c'est-à-dire que l'objet ne sera pas affecté avant ou après sa réutilisation.

L'implémentation Immutable sonne bien, mais Vous devez respecter des restrictions très strictes afin de profiter de ses avantages.

La première est que l'état interne doit être déterminé lors de l'instanciation comme décrit ci-dessus. L'autre est que les champs d'instance des objets immuables doivent également être immuables.

L'implémentation suivante est-elle immuable?

public class NonImmutable {

	private final String[] word = {"pomme", "gorille", "rap"};

	public String[] getWord() {
		return word;
	}
}

À première vue, il n'y a pas de méthode pour changer l'état interne. Cependant, lorsqu'il est utilisé comme suit,

	private static void execNonImmutable() {
		NonImmutable non = new NonImmutable();
		non.getWord()[1] = "Godzilla";
		System.out.println(non.getWord()[1]);
	}

La chaîne est-elle affichée sur la console «Gorilla» ou «Godzilla»?

La bonne réponse est «Godzilla».

Cette implémentation n'est pas immuable car si vous laissez un champ Mutable hors de l'objet tel quel, il peut être modifié à l'extérieur.

Il convient de noter que l'état interne du champ d'instance ne peut pas être réécrit lors d'une implémentation immuable. (Il faut que tout soit invariant en remontant à l'état interne de l'état interne ...)

La bibliothèque Java possède certaines fonctionnalités qui la rendent immuable. Par exemple Collections.unmodifiableList(list) Vous pouvez utiliser la méthode ci-dessus pour créer une liste immuable à partir d'une liste existante. C'est une bonne idée d'utiliser ces fonctionnalités pratiques pour éviter les modifications accidentelles.

mérite

Démérite

Apatride-apatride

Apatride signifie "n'a pas d'état" Cela signifie que l'objet n'a pas d'état, qu'il soit instancié ou non.

L'apatride est également immuable comme condition préalable. Immuable est classé comme immuable, mais comme il possède des fonctionnalités supplémentaires, il est intentionnellement défini comme une trame séparée.

Définition

public abstract class StatelessHand {

	private static final Rock ROCK_IMPL = new Rock();
	private static final Paper PAPER_IMPL = new Paper();
	private static final Scissors SCISSORS_IMPL = new Scissors();

	public static StatelessHand of(HandStrategy strategy) {
		switch(strategy) {
			case ROCK:
				return ROCK_IMPL;
			case PAPER:
				return PAPER_IMPL;
			case SCISSORS:
				return SCISSORS_IMPL;
		}
		throw new IllegalArgumentException();
	}

	public abstract HandStrategy getStrategy();
	public abstract String getName();

	private static class Rock extends StatelessHand {
		@Override
		public HandStrategy getStrategy() {
			return HandStrategy.ROCK;
		}
		@Override
		public String getName() {
			return "Goo";
		}
	}

	private static class Paper extends StatelessHand {
		@Override
		public HandStrategy getStrategy() {
			return HandStrategy.PAPER;
		}
		@Override
		public String getName() {
			return "Par";
		}
	}

	private static class Scissors extends StatelessHand {
		@Override
		public HandStrategy getStrategy() {
			return HandStrategy.SCISSORS;
		}
		@Override
		public String getName() {
			return "Choki";
		}
	}
}

L'objet n'a pas d'état. (Aucun champ d'instance) Les méthodes de chaque classe d'implémentation (Rock, Paper, Scissors) renvoient une valeur fixe. (= Le résultat ne change pas)

Un autre objet apatride est déjà apparu. C'est «arbitre».

Courir

	private static void execStateless() {
		final StatelessHand left = StatelessHand.of(HandStrategy.ROCK);
		final StatelessHand right = StatelessHand.of(HandStrategy.PAPER);

		System.out.println(left.getName() + " VS " + right.getName());
		Referee.battle(left.getStrategy(), right.getStrategy());

		//Obtenez une nouvelle instance la deuxième fois
		final StatelessHand left2 = StatelessHand.of(HandStrategy.SCISSORS);
		System.out.println(left2.getName() + " VS " + right.getName());
		Referee.battle(left2.getStrategy(), right.getStrategy());
	}

Les implémentations sans état ne nécessitent presque toujours pas d'instanciation. Referee exécute la méthode battle sans l'instancier. L'état de l'objet n'affecte pas le résultat (valeur de retour).

À première vue, l'implémentation Stateless ne semble pas avoir besoin d'être instanciée, Les sous-classes de StatelessHand sont instanciées. C'est une méthode efficace lorsque vous souhaitez obtenir un polymorphisme même avec une implémentation sans état.

Fonctionnalité

Les méthodes d'objet sans état ont le même résultat si les arguments sont les mêmes. Cependant, il ne semble pas donner le même résultat si vous utilisez des objets sans argument (par exemple statiques) dans la méthode. Ce n'est pas syntaxiquement un argument, mais il agit comme un "argument caché" pour exécuter la méthode. C'est un problème différent du fait que le résultat ne dépend pas de l'état de l'objet sans état.

Lors de l'implémentation de "strict Stateless [^ 1]" (seuls les arguments affectent le résultat), vous devez utiliser des objets immuables autres que les arguments. La sous-classe de StatelessHand est stricte Stateless car elle utilise les objets immuables ʻEnum et String` dans ses méthodes.

[^ 1]: "Strict Stateless" est un mot que j'ai inventé sans permission, donc il ne peut pas être compris par d'autres personnes (transpiration)

mérite

Démérite

lambda

Il fut un temps où les objets sans état étaient rarement utilisés dans la programmation orientée objet il y a dix ans, ou plutôt appelés anti-modèles. (Puisqu'il n'y a pas d'état, la méthode est définie statiquement sans l'instancier, etc.)

Cependant, l'introduction du savoir-faire en programmation fonctionnelle de manière orientée objet a donné à Stateless une position importante. Je ne couvrirai pas tout le savoir-faire qui vous a été apporté ici, mais un exemple est ** Lambda **.

Lambda en Java utilise en fait des objets avec une implémentation sans état. L'exemple suivant est une méthode qui transmet a et b comme arguments et une expression lambda qui effectue un calcul sur ces deux valeurs. calc(a, b, (x,y)->x+y)

Si vous appelez cette méthode sans utiliser lambda, elle ressemblera à ceci:

calc(a, b, new BiFunction<Integer, Integer, Integer>() {
			@Override
			public Integer apply(Integer x, Integer y) {
				return x + y;
			}
		});

Nous instancions et passons une classe qui implémente une interface appelée «BiFunction». Lambda est une méthode de description qui simplifie au maximum l'implémentation et l'instanciation d'une classe, et l'objet est une implémentation sans état.

final int offset = /*Valeur de décalage*/;
calc(a, b, (x,y)->x+y+offset);

calc(a, b, new BiFunction<Integer, Integer, Integer>() {
			@Override
			public Integer apply(Integer x, Integer y) {
				return x + y + offset;
			}
		});

Dans le cas d'une expression lambda, il est clair qu'un objet autre que l'argument est utilisé, donc je pense que la lisibilité est bonne.

Les références Lambda et de méthode apparues dans Java 8 (les détails sont omis) ont des caractéristiques grammaticales différentes de celles de Java traditionnel et sont difficiles à comprendre pour les débutants, mais Avec leur avènement, les apatrides en sont venus à trouver une valeur significative, c'est donc une bonne idée de le considérer sans penser qu'il n'a aucune utilité ou est un anti-modèle.

Effets secondaires

Il y a un «effet secondaire» qui est souvent utilisé (ou ressenti) dans la programmation fonctionnelle.

La méthode d'objet sans état reçoit un argument et renvoie le résultat en tant que valeur de retour, mais lors de la transmission de cette méthode, l'état interne de l'objet passé en tant qu'argument change, ou l'intérieur de «l'argument caché» expliqué ci-dessus Le changement de condition est appelé un «effet secondaire».

Lorsque cet effet secondaire se produit, il peut se comporter comme si l'avantage Stateless «Les méthodes renvoient toujours le même résultat» n'est pas suivi. En fait, il est exécuté avec l'état interne de l'argument modifié, donc ce ne sera pas "si l'argument est le même", mais il y a un risque de l'oublier et de provoquer un bogue difficile à reproduire.

Tous les effets secondaires ne sont pas mauvais. (Au moins dans la programmation orientée objet) Les implémentations immuables ne changent pas d'état, mais peuvent réécrire l'état de l'objet variable donné dans l'argument. (La méthode de paramétrage de l'état interne de l'objet passé en argument est une technique courante)

Lors de la mise en œuvre d'immuable et sans état, il est nécessaire de rendre la méthode exempte d'effets secondaires ou de spécifier les effets secondaires dans les commentaires.

Résumé

Les implémentations mutables sont faciles à créer avec peu de restrictions. Si les avantages l'emportent sur les inconvénients, vous pouvez l'inclure dans les options, mais cela vous rendra heureux plus tard si vous considérez d'abord s'il peut être implémenté dans Immutable.

Les implémentations immuables sont trop restrictives pour être créées sans le savoir, mais vous pouvez obtenir les avantages qu'elles méritent. De plus, le sentiment d'accomplissement lorsqu'il est mis en œuvre avec succès est un plaisir réservé aux programmeurs (rires). Même dans la révision du code, vous devriez pouvoir être considéré par vos aînés comme "ce type, ne le faites pas". Le plus gros problème, la copie défensive, peut être atténué en combinant les implémentations Immutable, ce qui est également une raison pour éliminer Mutable.

L'implémentation sans état semble être plus sujette aux bogues (donc appelée anti-pattern) avec de nombreuses précautions, mais si elle est gérée correctement, elle a le potentiel d'être une implémentation hautement réutilisable et ne se fanant pas. Je suis.

Il est également important de noter que l'implémentation de méthodes ayant des effets secondaires, même des objets immuables, peut affecter d'autres objets mutables.

Si vous avez lu jusqu'ici, je pense que vous pouvez déjà programmer consciemment en apprenant les mots Mutable (variable) et Immutable (immuable). Si vous révisez le code que vous avez écrit jusqu'à présent, n'est-ce pas mieux maintenant?

Enfin, si vous voulez en savoir plus sur l'immuabilité et d'autres bonnes méthodes d'implémentation, qui sont uniquement Java, je vous recommande le livre "Effective Java".

Recommended Posts

Être conscient de l'état interne (immuabilité) des objets et des effets secondaires des méthodes
Résumer le cycle de vie des objets Java à prendre en compte dans le développement Android
Organiser l'état actuel de Java et envisager l'avenir
Surveillez l'état interne des programmes Java avec Kubernetes
Soyez conscient du garbage collection et évitez les fuites de mémoire
J'ai essayé de résumer les méthodes de Java String et StringBuilder