Je récupère un ensemble de tuples dans la base de données et je le mets dans une carte. La requête de base de données est coûteuse.
Il n'y a pas naturel ordre évident des éléments dans la carte, mais ordre d'insertion importe quand même. Le tri de la carte serait une opération lourde, donc je veux éviter de le faire, étant donné que le résultat de la requête est déjà trié comme je le veux. Par conséquent, je stocke simplement le résultat de la requête dans un LinkedHashMap
et retourne la carte à partir d'une méthode DAO:
public LinkedHashMap<Key, Value> fetchData()
J'ai une méthode processData
qui devrait faire un peu de traitement sur la carte - modifier certaines valeurs, ajouter de nouvelles clés/valeurs. Il est défini comme
public void processData(LinkedHashMap<Key, Value> data) {...}
Cependant, plusieurs linters (Sonar etc.) se plaignent que Le type de 'données' devrait être une interface telle que 'Map' plutôt que l'implémentation "LinkedHashMap" ( calmar S1319 ).
Donc, fondamentalement, il dit que j'aurais dû
public void processData(Map<Key, Value> data) {...}
Mais je veux que la signature de la méthode indique que l'ordre des cartes est important - cela compte pour l'algorithme dans processData
- afin que ma méthode ne soit pas transmise comme n'importe quelle carte aléatoire.
Je ne veux pas utiliser SortedMap
, car il (du javadoc de Java.util.SortedMap
) "est ordonné selon le ordre naturel de ses clés, ou par un Comparateur généralement fourni au moment de la création de la carte triée."
Mes clés n'ont pas de ordre naturel, et créer un Comparateur pour ne rien faire semble verbeux.
Et je voudrais toujours que ce soit une carte, pour profiter de put
pour éviter les clés en double, etc. Sinon, data
aurait pu être un List<Map.Entry<Key, Value>>
.
Alors, comment puis-je dire que ma méthode veut une carte qui est déjà triée? Malheureusement, il n'y a pas de Java.util.LinkedMap
interface, sinon je l'aurais utilisé.
Merci pour beaucoup de bonnes suggestions et de matière à réflexion.
J'ai fini par étendre la création d'une nouvelle classe de carte, faisant de processData
une méthode d'instance:
class DataMap extends LinkedHashMap<Key, Value> {
processData();
}
J'ai ensuite refactorisé la méthode DAO pour qu'elle ne retourne pas de carte, mais prend à la place une carte target
comme paramètre:
public void fetchData(Map<Key, Value> target) {
...
// for each result row
target.put(key, value);
}
Donc, remplir un DataMap
et traiter les données est maintenant un processus en deux étapes, ce qui est bien, car il y a d'autres variables qui font partie de l'algorithme, qui viennent d'autres endroits.
public DataMap fetchDataMap() {
var dataMap = new DataMap();
dao.fetchData(dataMap);
return dataMap;
}
Cela permet à mon implémentation Map de contrôler la façon dont les entrées y sont insérées, et masque les exigences de classement - c'est maintenant un détail d'implémentation de DataMap
.
Utilisez donc LinkedHashMap
.
Oui, vous devriez utiliser Map
sur une implémentation spécifique chaque fois que possible, et oui, cette est meilleure pratique.
Cela dit, il s'agit d'une situation étrangement spécifique où l'implémentation de Map
est réellement importante. Cela ne sera pas vrai pour 99,9% des cas dans votre code lorsque vous utilisez Map
, et pourtant vous êtes ici, dans cette situation de 0,1%. Sonar ne peut pas le savoir et Sonar vous dit simplement d'éviter d'utiliser l'implémentation spécifique car elle serait correcte dans la plupart des cas.
Je dirais que si vous pouvez justifier l'utilisation d'une implémentation spécifique, n'essayez pas de mettre du rouge à lèvres sur un cochon. Vous avez besoin d'un LinkedHashMap
, pas d'un Map
.
Cela dit, si vous êtes nouveau dans la programmation et tombez sur cette réponse, ne pensez pas que cela vous permet d'aller à l'encontre des meilleures pratiques, car ce n'est pas le cas. Mais lorsque le remplacement d'une implémentation par une autre n'est pas acceptable, la seule chose que vous pouvez faire est d'utiliser cette implémentation spécifique et d'être damné envers Sonar.
Vous vous battez contre trois choses:
La première est la bibliothèque de conteneurs de Java. Rien dans sa taxonomie ne vous donne un moyen de déterminer si la classe itère ou non dans un ordre prévisible. Il n'y a pas d'interface IteratesInInsertedOrderMap
qui pourrait être implémentée par LinkedHashMap
, ce qui rend impossible la vérification de type (et l'utilisation d'implémentations alternatives qui se comportent de la même manière). C'est probablement par conception, car l'esprit de celui-ci est que vous êtes vraiment censé pouvoir traiter des objets qui se comportent comme l'abstrait Map
.
Deuxièmement, nous croyons que ce que dit votre interlocuteur doit être traité comme un évangile et qu'ignorer tout ce qu'il dit est mauvais. Contrairement à ce qui passe pour une bonne pratique de nos jours, les avertissements de linter ne sont pas censés être des barrières pour qualifier votre code de bon. Ils sont invités à raisonner sur le code que vous avez écrit et à utiliser votre expérience et votre jugement pour déterminer si l'avertissement est justifié ou non. Les avertissements injustifiés sont la raison pour laquelle presque tous les outils d'analyse statique fournissent un mécanisme pour leur dire que vous avez examiné le code, que vous pensez que ce que vous faites est correct et qu'ils ne devraient pas s'en plaindre à l'avenir.
Troisièmement, et c'est probablement la viande de celui-ci, LinkedHashMap
peut être le mauvais outil pour le travail. Les cartes sont destinées à un accès aléatoire et non ordonné. Si processData()
parcourt simplement les enregistrements dans l'ordre et n'a pas besoin de trouver d'autres enregistrements par clé, vous forcez une implémentation spécifique de Map
à faire le travail d'un List
. D'un autre côté, si vous avez besoin des deux, LinkedHashMap
est le bon outil car il est connu pour faire ce que vous voulez et vous êtes plus que justifié de l'exiger.
Si tout ce que vous obtenez de LinkedHashMap
est la possibilité d'écraser les doublons, mais que vous l'utilisez vraiment comme List
, alors je suggère qu'il est préférable de communiquer cette utilisation avec la vôtre implémentation personnalisée de List
. Vous pouvez le baser sur une classe de collections Java Java et remplacer simplement toutes les méthodes add
et remove
pour mettre à jour votre magasin de sauvegarde et garder une trace de la clé pour vous assurer Le fait de lui donner un nom distinctif comme ProcessingList
indiquera clairement que les arguments présentés à votre méthode processData
doivent être traités d'une manière particulière.
Je vous entends dire "J'ai une partie de mon système qui produit un LinkedHashMap, et dans une autre partie de mon système, je dois accepter uniquement les objets LinkedHashMap qui ont été produits par la première partie, car ceux produits par un autre processus ont gagné" t fonctionne correctement. "
Cela me fait penser que le problème ici est en fait que vous essayez d'utiliser LinkedHashMap car il correspond principalement aux données que vous recherchez, mais en fait, il ne peut pas être remplacé par une autre instance que celles que vous créez. Ce que vous voulez réellement faire, c'est créer votre propre interface/classe qui est ce que votre première partie crée et votre deuxième partie consomme. Il peut envelopper le "vrai" LinkedHashMap, et fournir un getter de carte ou implémenter l'interface de carte.
Ceci est un peu différent de la réponse de CandiedOrange, en ce que je recommanderais d'encapsuler la vraie carte (et de lui déléguer des appels si nécessaire) plutôt que de l'étendre. C'est parfois une de ces guerres sacrées de style, mais il me semble que ce n'est pas "une carte avec des trucs supplémentaires", c'est "mon sac d'informations utiles sur l'état, que je peux représenter en interne avec une carte".
Si vous aviez deux variables que vous aviez besoin de faire circuler comme ceci, vous auriez probablement fait une classe pour elle sans y penser beaucoup. Mais parfois, il est utile d'avoir une classe même si ce n'est qu'une variable membre, simplement parce que c'est logiquement la même chose, pas une "valeur" mais "le résultat de mon opération avec lequel je dois faire des choses plus tard".
LinkedHashMap est la seule carte Java qui possède la fonction d'ordre d'insertion que vous recherchez. Par conséquent, ignorer le principe d'inversion de dépendance est tentant et peut-être même pratique. Tout d'abord, réfléchissez à ce qu'il faudrait pour Voici ce que SOLID vous demanderait de faire.
Remarque: remplacez le nom Ramdal
par un nom descriptif indiquant que le consommateur de cette interface est le propriétaire de cette interface. Ce qui en fait l'autorité qui décide si l'ordre d'insertion est important. Si vous appelez simplement cela InsertionOrderMap
vous avez vraiment raté le point.
public interface Ramdal {
//ISP asks for just the methods that processData() actually uses.
...
}
public class RamdalLinkedHashMap extends LinkedHashMap implements Ramdal{}
Ramdal<Key, Value> ramdal = new RamdalLinkedHashMap<>();
ramdal.put(key1, value1);
ramdal.put(key2, value2);
processData(ramdal);
Est-ce un gros design à l'avant? Cela dépend peut-être de la probabilité que vous ayez besoin d'une implémentation en plus de LinkedHashMap
. Mais si vous ne suivez pas DIP uniquement parce que ce serait une énorme douleur, je ne pense pas que la plaque de la chaudière soit plus douloureuse que cela. C'est le modèle que j'utilise lorsque je souhaite que le code intouchable implémente une interface qu'il ne fait pas. La partie la plus douloureuse est vraiment de penser à de bons noms.
Alors, laissez-moi essayer de comprendre votre contexte ici:
... l'ordre d'insertion est important ... Trier la carte serait une opération lourde ...
... le résultat de la requête est déjà trié comme je le veux
Maintenant, ce que vous faites actuellement:
Je récupère un ensemble de tuples de la base de données, et le mettre dans une carte ...
Et voici votre code actuel:
public void processData(LinkedHashMap<Key, Value> data) {...}
Ma suggestion est de faire ce qui suit:
Exemple de code
public interface MyTupleRepository {
Collection<MyTuple> GetAll();
}
//Concrete implementation of data access object, that retrieves
//your tuples from DB; this data is already ordered by the query
public class DbMyTupleRepository implements MyTupleRepository { }
//Injects some abstraction of repository into the processing method,
//but make it clear that some exception might be thrown if data is not
//arranged in some specific way you need
public void processData(MyTupleRepository tupleRepo) throws DataNotOrderedException {
LinkedHashMap<Key, Value> data = new LinkedHashMap<Key, Value>();
//Represents the query to DB, that already returns ordered data
Collection<MyTuple> myTuples = tupleRepo.GetAll();
//Optional: this would throw some exception if data is not ordered
Validate(myTuples);
for (MyTupleData t : myTuples) {
data.put(t.key, t.value);
}
//Perform the processing using LinkedHashMap...
...
}
Je suppose que cela supprimerait l'avertissement Sonar et spécifierait également dans la signature la disposition spécifique des données requises par la méthode de traitement.
Si vous souhaitez indiquer que la structure de données que vous avez utilisée existe pour une raison, ajoutez un commentaire au-dessus de la signature de la méthode. Si un autre développeur à l'avenir rencontre cette ligne de code et remarque un avertissement d'outil, il peut également remarquer le commentaire et s'abstenir de "résoudre" le problème. S'il n'y a pas de commentaire, rien ne les empêchera de changer la signature.
Supprimer les avertissements est inférieur à commenter à mon avis, car la suppression elle-même n'indique pas la raison pour laquelle l'avertissement a été supprimé. Une combinaison de suppression d'avertissement et de commentaire conviendra également.