Kihagyás

08 óra

Refaktorálás és a kódminőség javítása

Kódminőség még egyszer

A kódminőség kifejezés egy szoftverkód hatékonyságának, megbízhatóságának és karbantarthatóságának általános értékelését írja le. A kódminőség fő jellemzői az olvashatóság, az érthetőség, a megbízhatóság, a biztonság és a modularitás, hordozhatóság, karbantarthatóság, stb. A kódminőség tehát abból a szempontból értékeli a szoftvert, hogy az mennyire érthető, módosítható, tesztelhető, üzemeltethető.

Korábban már találkoztunk a ISO/IEC 25010 szabvány által meghatározott taxonómiával, amelynek egy részletesebb verziója az alábbi ábrán látható:

ISO25010

A szabvány a szoftverminőség fogalmát szélesebb körben határozza meg, általában beszél a szoftver minőségéről. A kódminőség ennek a követelményrendszernek a részét képezi magas szinten. Azt tudjuk mondani, hogy a szabvány által meghatározott követelmények betartása jellemzően magas kódminőséget is eredményez, ez azonban csak akkor teljesül, amennyiben az arra vonatkozó követelményekre részletes szabályokat állítunk fel és tartunk be. Ilyenek a különböző kódolási szabályok, ajánlások figyelembe vétele, illetőleg alkalmazása. Az ajánlások többsége biztonságkritikus rendszerekre lett megfogalmazva, azonban jelentős számban található közöttük olyan javaslat, amely minden szoftver esetében jól alkalmazható és célszerű azokat követni. Ezen ajánlások közül kiemelkedik a tiszta kód, amely általános érvényű, betartása jellemzően magas kódminőséget biztosít, illetőleg a tervezési minták alkalmazása. Ez utóbbiak a szoftverfejlesztői gyakorlatból származó minták, amelyek alkalmazása igazoltan jó minőségű, stabil, karbantartható kódot eredményez.

A gyakorlatban használt, kódolási irányelvekre vonatkozó de facto szabvány, illetőleg ajánlás:

A kódminőség mérésére a korábbi órákon már említett statikus forráskódelemző programokat (SonarQube, SourceMeter, OpenStaticAnalyzer) használjuk. Az elemzők a szoftvermetrikák alapján, illetőleg kódolási szabályokban megfogalmazott szabálysértések felismerésével képesek a gyanús programrészek megjelölésére és a fejlesztő irányába történő jelzésére.

Absztració

A refaktorálás, szoftverkarbantartás feladatainak elvégzéséhez sokszor szükség lehet arra, hogy az érintett szoftvert magasabb absztrakciós szinten vizsgáljuk meg és adott esetben újratervezzük az érintett folyamatokat, komponenseket.

Visszatervezés

A szoftvervisszatervezés folyamatát E. J. Chikofsky és J. H. Cross az alábbi módon definiálta 1990-ben: "A visszatervezés az elemzés azon folyamata, amikor a kérdéses rendszerben (a) meghatározzuk a rendszer komponenseit és azon kapcsolatait, továbbá (b) elkészítjük a rendszer más alakbeli reprezentációját, vagy az absztrakció magasabb szintjén ábrázoljuk azt."

A visszatervezés során elsőként a komponensek és kapcsolatainak meghatározása zajlik, ahogy azt korábbi tanulmányaink során már említettük. A visszatervezés célja, hogy a szoftvert magasabb szinten tudjuk ábrázolni úgy, amely hatékonyan támogatja annak megértését. Az, hogy ez a magasabb szintű ábrázolás milyen módon valósul meg függ attól, hogy a későbbiek során milyen eszközzel fogunk dolgozni. A forráskód metrikákat számoló eszközök esetében ez a magasabb szint jellemzően egy absztrakt szintaxis fa (AST), az absztrakt szemantikus gráf (ASG), vagy a vezérlési folyam gráf (CFG), azonban emberi feldolgozásra inkább a UML és EK diagramok alkalmasak.

Az absztrakció jelentése a konkrét objektumok ábrázolásának, modellezésének általánosabb szinten történő megvalósítása. Ilyen esetben egyes, a céljaink számára irreleváns részleteket elhanyagoljuk. Az absztrakció egyben egyszerűsít is, és sokszor egy másik ábrázolási formát von maga után. Ilyen absztrakció a forráskód megjelenítése a CodeMetropolis programban, hiszen ebben az esetben is csak a minket érdeklő forráskód elemeket és azok kapcsolatait vesszük figyelembe, valamint azon (ebben az esetben implicit jellemzőket - metrikákat) tulajdonságokat, amelyek a vizsgálatainkhoz, céljainkhoz relevánsak. A forráskód absztrakció szintjeit ebben az esetben az egyes XML modellek képviselik, kiegészítve a grafikus ábrázolással és az inputot reprezentáló graph állománnyal.

Az absztrakció során előálló modellek célja legtöbbször az emberi megértés támogatása. Ennek megvalósítására ad segítséget a vizualizációról szóló összefoglalónk.

Absztrakciós szint

A szoftverfejlesztésben és a számítástudományban az absztrakciós szint annak a módját jelenti, amelynek során egy rendszer vagy folyamat működési részleteit elrejtve ábrázoljuk a rendszer, illetőleg a folyamat releváns összetevőit.

A szoftverfejlesztés folyamatának absztrakciós szintjeit az alábbi ábrán láthatjuk:

Absztrakciós szintek

Fontos kiemelni, hogy az absztrakciós szint nem maga a konkrét modell, hanem az adott szoftverre vonatkozólag a részletek elrejtésének módja, azaz a szempontjaink szerinti szűrés, amelynek segítségével az adott szinten releváns valamely modell előállítható.

A magasabb szinten lévő modellek segítségével a szoftver újratervezhető. Az újratervezés fontos feltétele annak, hogy a refaktorálás folyamatát megfelelő minőségben el tudjuk végezni.

Újratervezés

A szoftver újratervezés folyamatát E. J. Chikofsky és J. H. Cross II. így definiálta: "A reengineering az a folyamat, amikor a rekonstruált rendszert ábrázoljuk egy új alakban, megváltoztatjuk azt, majd implementáljuk az új alakot."

Újratervezés

Forward engineering

E. J. Chikofsky és J. H. Cross az alábbi módon definiálta 1990-ben a forward engineering folyamatát: "A forward engineering az a hagyományos folyamat, amikor a magas absztrakciós szintű, logikai és implementáció-független reprezentációból a valós implementációt, hozzuk létre."

Refaktorálás

A jó kódminőség biztosítása folyamatos ellenőrzést és kódelemzést igényel. Még a legpontosabb és a kódjára is különös figyelmet fordító fejlesztő is megsérti időnként a kód tisztaságára vonatkozó elveket, illetőleg a magas minőségre irányuló elvárásokat. A statikus elemzés ezeket a problémákat képes időben feltárni. Azt a folyamatot, amelyben a programkódot annak érdekében módosítjuk, hogy az hatékonyabb, érthetőbb, jobban karbantarható legyen, refaktorálásnak nevezzük.

Technikai adósság

A szoftverfejlesztésben vagy bármely más informatikai területen (pl. infrastruktúra, hálózatépítés stb.) a technikai adósság (technical dept) a jövőbeni átdolgozás várható költsége, amely akkor merül fel, ha egy egyszerű, de korlátozott megoldást választunk egy jobb, de több időt igénylő megközelítés helyett.

De mitől romlik el a kód?

Valamikor azt tanították, hogy a szoftver nem tud elromlani. Mikor ezt az állítást tették, nem gondoltak arra, hogy a folyamatos változtatások következtében óhatatlanul hibázunk, és még a hibák javítását követően is maradnak a kódban olyan részletek, amelyek későbbi hibák forrásai lehetnek. De miért is hibázunk egy már meglévő kód továbbfejlesztése vagy karbantartása során?

  • Az üzleti körülmények generálta nyomás miatt sietni kell a munkával, esetenként még bejezetlen kódot kell kiadni.
  • Nem ismerjük eléggé a valódi következményeit a technikai adósságnak, és ezért az adott pillanatban az egyszerűbb, ám kevésébé körültekintő megoldást választjuk.
  • Korábbi tervezési problémák miatt az implementált komponensek koherenciája sérül. Ilyen esetben a program egy részének változtatása a program olyan részében történő változásokat eredményez, amire nem számítunk.
  • Tesztelés hiánya is vezethet rejtett, fel nem ismert hibákhoz.
  • A hiányos dokumentáció következtében sérül a program megértésének folyamata, és ezért olyan módosításokat is végrehajthatunk benne, amelyek nem szándékozott következményekhez vezetnek.
  • A csapaton belüli kommunikációs problémák is okozhatnak hibás kódolást.
  • Párhuzamos, elszigetelt fejlesztések esetén a program egészének koherenciája sérül.

A fentieken túl megemlíthetjük még az olyan gyakorlatokat, amelyek figyelmen kívül hagyják a megfelelőségi teszteket, vagy a nem megfelelően képzett fejlesztők alkalmazását is.

Néhány tipikus eset, amely a kód karbantartásának, továbbfejlesztésének folyamatát megnehezítheti és ezért refaktorálásra szorul:

  • Hosszú metódusok használata: Ez abban az esetben fordul elő, ha egy metódus implementációja túl sok kódsort tartalmaz. Ilyen esetben az érintett metódust szét kell szedni almetódusokra (extract method).
  • Túl nagy osztályok használata: Az osztályoknak jól meghatározott tulajdonságai és viselkedése van. Egy osztályt akkor tudunk hatékonyan kezelni, ha a méretét is kézben tartjuk. Azokat az osztályokat, amelyek tulajdonságait túlságosan sok attribútum segítségével lehet leírni, az egyes összetartozó attribútumok és viselkedések összefogásával szét lehet szedni több osztályba, vagy alosztályok használatával egyszerűsíteni az érintett osztály szerkezetét (extract class).
  • Hosszú paraméterlista: Gyakori eset, amikor egy osztály valamely metódusa túl sok paramétert tartalmaz. Ha kódunkban ilyen metódusokkal találkozunk, akkor több lehetőségünk is van a probléma kezelésére. Egyrészt a paramétereket egy paraméter objektumba vagy struktúrába tudjuk szervezni, illetőleg egy-egy paraméterátadást metódushívásra is le tudunk cserélni.

    Long parameterlist

    Az alábbi példában Map objektum segítségével kikerülhető lett volna a A, B és C változók direkt használata.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
        ...            
        public void MyMethon( Long valuA,
                              Long valueB,
                              Long valueC,
                              String  nameA,
                              String  nameB,
                              String  nameC,
                              String callerName,
                              Integer numberOfParams){
    
          // Do something...                  
    
        )
    
  • Temporális mezők használata: Többnyire akkor fordul elő, ha egy osztály valamely metódusában alkalmazott algoritmus komplex, és ideiglenes tárolni szeretne valamit az osztályon belül. Azonban ez a megoldás nem jó, ideiglenes mezők használatának az OOP-ben nincs helye.

    Temporális mező

    Az alábbi példában a durations, az average és a standardDeviation mezők temporálisak. Ezek csak a kalkuláció ideje alatt használtak, nem képezik az osztály permanens állapotát.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    public class Estimator {
        private final TimeSpan defaultEstimate;
        private IReadOnlyCollection<TimeSpan> durations;
        private TimeSpan average;
        private TimeSpan standardDeviation;
    
        public Estimator(TimeSpan defaultEstimate) {
            this.defaultEstimate = defaultEstimate;
        }
    
        public TimeSpan CalculateEstimate(IReadOnlyCollection<TimeSpan> durations) {
            if (durations == null)
                throw new ArgumentNullException("durations");
    
            if (durations.Count == 0)
                return this.defaultEstimate;
    
            this.durations = durations;
            this.CalculateAverage();
            this.CalculateStandardDeviation();
    
            var margin = TimeSpan.FromTicks(this.standardDeviation.Ticks * 3);
            return this.average + margin;
        }
    
        private void CalculateAverage() {
            this.average = TimeSpan.FromTicks((long) this.durations.Average(ts => ts.Ticks));
        }
    
        private void CalculateStandardDeviation() {
            var variance = this.durations.Average(ts => Math.Pow((ts - this.average).Ticks, 2));
            this.standardDeviation = TimeSpan.FromTicks((long) Math.Sqrt(variance));
        }
    }
    
  • Lusta osztály (lazy class): Ez az eset akkor fordul elő, ha egy osztály szerepe marginális. Ilyenkor hajlamosak vagyunk a kódkarbantartás során elfeledkezni róla. A megoldás ebben az esetben az osztályhierarchia újragondolása.

    Lazy class

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    class Product {
        private String name;
        private double price;
    
        public Product(String name, double price) {
            this.name = name;
            this.price = price;
        }
    
        // Getters and setters for name and price...
    
        // No additional behavior specific to this class
    }
    
    class ShoppingCartItem {
        private Product product;
        private int quantity;
    
        public ShoppingCartItem(Product product, int quantity) {
            this.product = product;
            this.quantity = quantity;
        }
    
        // Other methods related to shopping cart items...
    }
    
    class Order {
        private List<ShoppingCartItem> items;
    
        public Order() {
            this.items = new ArrayList<>();
        }
    
        public void addItem(ShoppingCartItem item) {
            items.add(item);
        }
    
        // Other methods related to orders...
    }
    
    public class Main {
        public static void main(String[] args) {
        Product lazyProduct = new Product("Generic Item", 10.0);
            ShoppingCartItem cartItem = new ShoppingCartItem(lazyProduct, 2);
    
            Order order = new Order();
            order.addItem(cartItem);
    
            // Other order-related logic...
        }
    }
    
  • Feature envy: Nehezen fordítható le magyarra ez a kifejezés. A probléma itt az, hogy egy osztály más osztály adattagjait vagy metódusait használja. Ez a működés az egységbezárás elvét sérti. Megoldása a metódusok átmozgatása a megfelelő osztályba (move method).

    Feature envy

    Az alábbipéldában a Client osztály ismételten metódust hív Intermediate és Child objektumokon a Parent osztályon keresztül.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    class Parent {
       private Intermediate intermediateContainer;
    
        public Intermediate getIntermediate() {
            return intermediateContainer;
        }
    }
    
    class Intermediate {
        private Child child;
    
        public Child getChild() {
            return child;
        }
    
        public void intermediateOp() {
            // Some operation...
        }
    }
    
    class Child {
        public void something() {
            // Do something...
        }
    
        public void somethingElse() {
            // Do something else...
        }
    }
    
    class Client {
        private Parent parent;
    
        public void something() {
            parent.getIntermediate().getChild().something();
        }
    
        public void somethingElse() {
            parent.getIntermediate().getChild().somethingElse();
        }   
    
        public void intermediate() {
            parent.getIntermediate().intermediateOp();
        }
    }
    
  • Klónok: A kódmásolatokra azért kell különösen odafigyelni, mert ha a másolt kódban módosítunk, akkor azt vélhetően minden másolatában (instance-ban) módosítanunk kell.

  • Halott kód: A nem elérhető kód többféleképeen is létrejöhet. Egyrészt olyan logikai feltételek alkalmazásával, amely soha nem értékelődhet igazra, valamint olyan esetben, ha szándékosan elérhetetlenné teszik. Az ilyen kódok biztonsági kockázatot is jelentenek.

    Dead code

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    public class Example {
        public static void main(String[] args) {
            boolean x = true;
            if (x) {
                // This block will never be executed
                System.out.println("Hello, world!");
            } else {
                // This is dead code!
                System.out.println("Goodbye, world!");
            }
        }
    }
    
  • Alacsony kohézió: Egy osztálynak egy koncepciót kell megvalósítania. Hibás az a tervezés, amikor egy osztályban több koncepció is megvalósításra kerül. Ilyen esetekben az osztályt szét kell bontani több osztályra.

    Low cohesion

    Ebben a példábana MyReader az erőforrás olvasásán túl más, az olvasáshoz nem kapcsolódó feladatokat is végez.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    public class MyReader {
        // Method for reading a resource
        public void readResource(String resourceName) {
            // Implementation...
        }
    
        // Unrelated method: Validates a location
        public boolean validateLocation(String location) {
            // Implementation...
            return true;
        }
    
        // Unrelated method: Checks FTP connection
        public boolean checkFTP(String server, String username, String password) {
            // Implementation...
            return true;
        }
    
        // Unrelated method: Pings a server
        public boolean ping(String host) {
            // Implementation...
            return true;
        }
    
        // ... Other unrelated methods ...
    
        public static void main(String[] args) {
            MyReader reader = new MyReader();
            reader.readResource("myFile.txt");
            // Other operations...
        }
    }
    

A fenti példák csak a leggyakoribb eseteket foglalják magukba. További példákat találunk ezen a linken.

A refaktorálás folyamatos munka, a fejlesztési folyamat egészében végezzük. Ahhoz, hogy hatékonyan tudjunk dolgozni, érdemes egy ellenőrzőlistát készíteni, amelybe felsoroljuk azokat az eseteket, amelyekre a refaktorálás során figyelnünk kell. Refaktorálás során ne implementáljunk új funkcionalitást és minden esetben végezzünk teljes tesztelést annak befejezésével.