Mein Fang in einer Woche

Je mehr ein Entwickler in einem Team an einer Anwendung arbeitet und je besser er seinen Code kennt, desto häufiger beschäftigt er sich mit dem Korrekturlesen der Arbeit seiner Kameraden. Heute werde ich zeigen, was in einer Woche in Code gefangen werden kann, der von sehr guten Entwicklern geschrieben wurde. Unter dem Schnitt befindet sich eine Sammlung lebendiger Artefakte unserer Kreativität (und ein wenig meiner Gedanken).


Komparatoren


Es gibt einen Code:


@Getter class Dto { private Long id; } // another class List<Long> readSortedIds(List<Dto> list) { List<Long> ids = list.stream().map(Dto::getId).collect(Collectors.toList()); ids.sort(new Comparator<Long>() { public int compare(Long o1, Long o2) { if (o1 < o2) return -1; if (o1 > o2) return 1; return 0; } }); return ids; } 

Jemand bemerkt, dass Sie den Stream direkt sortieren können, aber ich möchte Ihre Aufmerksamkeit auf den Komparator lenken. In der Dokumentation zur Comparator::compare Methode heißt es in Englisch in Weiß:


Vergleicht die beiden Argumente für die Reihenfolge. Gibt eine negative Ganzzahl, Null oder eine positive Ganzzahl zurück, da das erste Argument kleiner, gleich oder größer als das zweite ist.

Dieses Verhalten ist in unserem Code implementiert. Was ist los Tatsache ist, dass die Entwickler von Java sehr weitsichtig vorgeschlagen haben, dass viele einen solchen Komparator benötigen würden, und ihn für uns gemacht haben. Wir können es verwenden, indem wir unseren Code vereinfachen:


 List<Long> readSortedIds(List<Dto> list) { List<Long> ids = list.stream().map(Dto::getId).collect(Collectors.toList()); ids.sort(Comparator.naturalOrder()); return ids; } 

Ebenso dieser Code


 List<Dto> sortDtosById(List<Dto> list) { list.sort(new Comparator<Dto>() { public int compare(Dto o1, Dto o2) { if (o1.getId() < o2.getId()) return -1; if (o1.getId() > o2.getId()) return 1; return 0; } }); return list; } 

mit einem Handgriff verwandelt sich in


 List<Dto> sortDtosById(List<Dto> list) { list.sort(Comparator.comparing(Dto::getId)); return list; } 

Übrigens wird es in der neuen Version von „Ideas“ möglich sein, Folgendes zu tun:


Zauberei


Optionaler Missbrauch


Wahrscheinlich hat jeder von uns so etwas gesehen:


 List<UserEntity> getUsersForGroup(Long groupId) { Optional<Long> optional = Optional.ofNullable(groupId); if (optional.isPresent()) { return userRepository.findUsersByGroup(optional.get()); } return Collections.emptyList(); } 

Oft wird Optional verwendet, um das Vorhandensein / Fehlen eines Werts zu überprüfen, obwohl dieser nicht dafür erstellt wurde. Missbrauch binden und einfacher schreiben:


 List<UserEntity> getUsersForGroup(Long groupId) { if (groupId == null) { return Collections.emptyList(); } return userRepository.findUsersByGroup(groupId); } 

Denken Sie daran, dass es bei Optional nicht um das Methodenargument oder -feld geht, sondern um den Rückgabewert. Aus diesem Grund wurde es ohne Serialisierungsunterstützung entwickelt.


void Methoden, die den Status eines Arguments ändern


Stellen Sie sich eine Methode wie diese vor:


 @Component @RequiredArgsConstructor public class ContractUpdater { private final ContractRepository repository; @Transactional public void updateContractById(Long contractId, Dto contractDto) { Contract contract = repository.findOne(contractId); contract.setValue(contractDto.getValue()); repository.save(contract); } } 

Sicher haben Sie schon oft etwas Ähnliches gesehen und geschrieben. Hier gefällt mir nicht, dass die Methode den Status der Entität ändert, ihn aber nicht zurückgibt. Wie verhalten sich ähnliche Framework-Methoden? Beispielsweise geben org.springframework.data.jpa.repository.JpaRepository::save und javax.persistence.EntityManager::merge einen Wert zurück. Angenommen, nach der Aktualisierung eines Vertrags müssen wir ihn außerhalb der update abrufen. Es stellt sich so etwas heraus:


 @Transactional public void anotherMethod(Long contractId, Dto contractDto) { updateService.updateContractById(contractId, contractDto); Contract contract = repositoroty.findOne(contractId); doSmth(contract); } 

Ja, wir könnten die Entität direkt an die UpdateService::updateContract Methode übergeben, indem wir ihre Signatur ändern. Es ist jedoch besser, dies folgendermaßen zu tun:


 @Component @RequiredArgsConstructor public class ContractUpdater { private final ContractRepository repository; @Transactional public Contract updateContractById(Long contractId, Dto contractDto) { Contract contract = repository.findOne(contractId); contract.setValue(contractDto.getValue()); return repository.save(contract); } } // @Transactional public void anotherMethod(Long contractId, Dto contractDto) { Contract contract = updateService.updateContractById(contractId, contractDto); doSmth(contract); } 

Dies hilft einerseits, den Code zu vereinfachen, andererseits hilft es beim Testen. Im Allgemeinen ist das Testen von void Methoden eine äußerst trostlose Aufgabe, die ich anhand des gleichen Beispiels zeigen werde:


 @RunWith(MockitoJUnitRunner.class) public class ContractUpdaterTest { @Mock private ContractRepository repository; @InjectMocks private ContractUpdater updater; @Test public void updateContractById() { Dto dto = new Dto(); dto.setValue("- "); Long contractId = 1L; when(repository.findOne(contractId)).thenReturn(new Contract()); updater.updateContractById(contractId, contractDto); //void // ,       dto? -  : ArgumentCaptor<Contract> captor = ArgumentCaptor.forClass(Contract.class); verify(repository).save(captor.capture()); Contract updated = captor.getValue(); assertEquals(dto.getValue(), updated.getValue()); } } 

Aber alles kann einfacher gemacht werden, wenn die Methode einen Wert zurückgibt:


Stellen Sie sicher
 @RunWith(MockitoJUnitRunner.class) public class ContractUpdaterTest { @Mock private ContractRepository repository; @InjectMocks private ContractUpdater updater; @Test public void updateContractById() { Dto dto = new Dto(); dto.setValue("- "); Long contractId = 1L; when(repository.findOne(contractId)).thenReturn(new Contract()); Contract updated = updater.updateContractById(contractId, contractDto); assertEquals(dto.getValue(), updated.getValue()); } } 

Auf einen Schlag wird nicht nur der Aufruf von ContractRepository::save überprüft, sondern auch die Richtigkeit des gespeicherten Werts.


Fahrradbau


Öffnen Sie zum Spaß Ihr Projekt und suchen Sie danach:


 lastIndexOf('.') 

Mit hoher Wahrscheinlichkeit sieht der gesamte Ausdruck ungefähr so ​​aus:


 String fileExtension = fileName.subString(fileName.lastIndexOf('.')); 

Davor kann kein statischer Analysator warnen, es geht um das neu erfundene Fahrrad. Meine Herren, wenn Sie ein bestimmtes Problem im Zusammenhang mit dem Dateinamen / der Dateierweiterung oder dem Pfad dazu lösen, genau wie beim Lesen / Schreiben / Kopieren, dann wurde in 9 von 10 Fällen die Aufgabe bereits vor Ihnen gelöst. Binden Sie sich daher an die Fahrradkonstruktion an und nehmen Sie fertige (und bewährte) Lösungen:


 import org.apache.commons.io.FilenameUtils; //... String fileExtension = FilenameUtils.getExtension(fileName); 

In diesem Fall sparen Sie Zeit, die für die Überprüfung der Eignung des Fahrrads aufgewendet wird, und erhalten erweiterte Funktionen (siehe Dokumentation FilenameUtils::getExtension ).


Oder hier ist ein Code, der den Inhalt einer Datei in eine andere kopiert:


 try { FileChannel sc = new FileInputStream(src).getChannel(); FileChannel dc = new FileOutputStream(new File(targetName)).getChannel(); sc.transferTo(0, sc.size(), dc); dc.close(); sc.close(); } catch (IOException ex) { log.error("", ex); } 

Welche Umstände können uns verhindern? Tausende von ihnen:


  • Das Ziel kann ein Ordner sein, überhaupt keine Datei
  • Die Quelle kann ein Ordner sein
  • Quelle und Ziel können dieselbe Datei sein
  • Ziel kann nicht erstellt werden
  • usw. und so weiter.

Das Traurige ist, dass wir mit der Selbstaufnahme alles lernen, was wir bereits beim Kopieren gelernt haben.
Wenn wir es mit Bedacht tun


 import org.apache.commons.io.FileUtils; try { FileUtils.copyFile(src, new File(targetName)); } catch (IOException ex) { log.error("", ex); } 

Dann wird ein Teil der Überprüfungen vor dem Beginn des Kopierens durchgeführt, und eine mögliche Ausnahme ist informativer (siehe die Quellen von FileUtils::copyFile ).


Vernachlässigung von @ Nullable / @ NotNull


Angenommen, wir haben eine Entität:


 @Entity @Getter public class UserEntity { @Id private Long id; @Column private String email; @Column private String petName; } 

In unserem Fall wird die email Spalte in der Tabelle im Gegensatz zu petName als not null beschrieben. Das heißt, wir können die Felder mit den entsprechenden Anmerkungen markieren:


 import javax.annotation.Nullable; import javax.annotation.NotNull; //... @Column @NotNull private String email; @Column @Nullable private String petName; 

Auf den ersten Blick sieht dies für den Entwickler wie ein Hinweis aus, und das ist es auch. Gleichzeitig sind diese Anmerkungen ein viel leistungsfähigeres Werkzeug als ein normales Etikett.


Entwicklungsumgebungen verstehen sie beispielsweise, und wenn wir nach dem Hinzufügen von Anmerkungen versuchen, dies folgendermaßen zu tun:


 boolean checkIfPetBelongsToUser(UserEnity user, String lostPetName) { return user.getPetName().equals(lostPetName); } 

dann warnt uns die "Idee" mit dieser Nachricht vor der Gefahr:


Der Methodenaufruf 'equals' kann 'NullPointerException' erzeugen.

Im Code


 boolean hasEmail(UserEnity user) { boolean hasEmail = user.getEmail() == null; return hasEmail; } 

Es wird eine weitere Warnung geben:


Bedingung 'user.getEmail () == null' ist immer 'false'

Dies hilft dem eingebauten Zensor, mögliche Fehler zu finden und die Codeausführung besser zu verstehen. Aus dem gleichen Grund sind Anmerkungen nützlich, um Methoden zu platzieren, die einen Wert und ihre Argumente zurückgeben.


Wenn meine Argumente nicht schlüssig erscheinen, schauen Sie sich den Quellcode eines ernsthaften Projekts an, denselben "Frühling" - sie sind mit Anmerkungen wie ein Weihnachtsbaum aufgehängt. Und das ist keine Laune, sondern eine ernsthafte Notwendigkeit.


Der einzige Nachteil scheint mir die Notwendigkeit zu sein, die Anmerkungen in einem modernen Zustand ständig aufrechtzuerhalten. Wenn Sie es sich ansehen, ist es eher ein Segen, denn wenn wir immer wieder zum Code zurückkehren, verstehen wir es immer besser.


Nachlässigkeit


Es gibt keine Fehler in diesem Code, aber es gibt einen Überschuss:


 Collection<Dto> dtos = getDtos(); Stream.of(1,2,3,4,5) .filter(id -> { List<Integer> ids = dtos.stream().map(Dto::getId).collect(toList()); return ids.contains(id); }) .collect(toList()); 

Es ist nicht klar, warum eine neue Liste von Schlüsseln erstellt werden soll, mit denen die Suche durchgeführt wird, wenn sich diese beim Durchlaufen des Streams nicht ändert. Es ist gut, dass es nur 5 Elemente gibt, aber was ist, wenn es 100500 davon gibt? Und wenn die getDtos Methode 100500 Objekte zurückgibt (in der Liste!), Welche Leistung hat dieser Code dann? Keine, also ist es besser so:


 Collection<Dto> dtos = getDtos(); Set<Integer> ids = dtos.stream().map(Dto::getId).collect(toSet()); Stream.of(1,2,3,4,5) .filter(ids::contains) .collect(toList()); 

Auch hier:


 static <T, Q extends Query> void setParams( Map<String, Collection<T>> paramMap, Set<String> notReplacedParams, Q query) { notReplacedParams.stream() .filter(param -> paramMap.keySet().contains(param)) .forEach(param -> query.setParameter(param, paramMap.get(param))); } 

Offensichtlich ist der vom Ausdruck inParameterMap.keySet() Wert unverändert, sodass er in eine Variable verschoben werden kann:


 static <T, Q extends Query> void setParams( Map<String, Collection<T>> paramMap, Set<String> notReplacedParams, Q query) { Set<String> params = paramMap.keySet(); notReplacedParams.stream() .filter(params::contains) .forEach(param -> query.setParameter(param, paramMap.get(param))); } 

Solche Abschnitte können übrigens mit der Prüfung 'Objektzuordnung in einer Schleife' berechnet werden.


Wenn die statische Analyse machtlos ist


Das achte Java ist längst ausgestorben, aber wir alle lieben Streams. Einige von uns lieben sie so sehr, dass wir sie überall verwenden:


 public Optional<EmailAdresses> getUserEmails() { Stream<UserEntity> users = getUsers().stream(); if (users.count() == 0) { return Optional.empty(); } return users.findAny(); } 

Wie Sie wissen, ist der Stream frisch, bevor die Beendigung aufgerufen wird. IllegalStateException Sie also erneut auf die users in unserem Code zugreifen, wird eine IllegalStateException .


Statische Analysegeräte wissen noch nicht, wie sie solche Fehler melden sollen. Daher liegt die Verantwortung für das rechtzeitige Erfassen beim Prüfer.


Es scheint mir, dass das Verwenden von Variablen vom Typ Stream sowie das Übergeben von Variablen als Argumente und das Zurückkehren von Methoden wie das Gehen in einem Minenfeld ist. Vielleicht Glück, vielleicht auch nicht. Daher eine einfache Regel: Jedes Auftreten von Stream<T> im Code sollte überprüft werden (aber auf gute Weise sofort ausgeschnitten werden).


Einfache Typen


Viele sind sich sicher, dass es bei boolean , int usw. nur um Leistung geht. Dies ist teilweise richtig, aber außerdem ist der einfache Typ standardmäßig not null . Wenn sich ein Ganzzahlfeld einer Entität auf eine Spalte bezieht, die in der Tabelle als not null deklariert ist, ist es sinnvoll, int anstelle von Integer . Dies ist eine Art Kombination - und der Speicherverbrauch ist geringer, und der Code wird aufgrund der unnötigen Überprüfung auf null vereinfacht.


Das ist alles Denken Sie daran, dass all das nicht die ultimative Wahrheit ist. Denken Sie mit Ihrem eigenen Kopf und gehen Sie die Anwendung von Ratschlägen sinnvoll an.

Source: https://habr.com/ru/post/de426105/


All Articles