Keri sisuni

Osapoolte üldistus

Design Patterns by Erich Gamma, Richard Helm, Ralph Johnson & John Vlissides Teeme vahelduseks läbi ühe mahukama harjutuse, mis annab head aimu klasside modelleerimise ja disainimise telgitagustest. Vaatleme Len Silverstoni raamatust "The Data Model Resource Book, vol. 1" pärit mudelit inimeste ja organisatsioonide esitlemiseks objekt-orienteeritud süsteemides.

Party generalization võiks eesti keelde tõlkida kui "osapoolte üldistus". Selle üldistuse eesmärgiks on mudel, mis võimaldab vajadusel käsitleda inimesi ja organisatsioone samasena. Sellist üldistust on vaja süsteemides, kus on kliendiregister ning klientideks võivad olla nii ettevõtted kui eraisikud. Oma andmete struktuurilt on nad erinevad, kuid omavad siiski piisavat ühisosa, et taoline üldistus meil elu lihtsamaks teeks.

"The Data Model Resource Book" on kolmest raamatust koosnev seeria, kus tutvustatakse andmemudeleid erinevatelt tegevus- ja elualadelt. Kõik mudelid on algul küllaltki lihtsad, kuid kasvavad kiiresti keerukaks ja mahukaks. Need raamatud ei ole mõeldud algajatele. Ma soovitaks nende mudelitega mängida ja katsetada ainult neil, kellel on väga tõsine huvi programmeerimise vastu ja kellel on seejuures piisavalt püsivust.

Kuidas inimesi ja organisatsioone mitte modelleerida

Ma olen praktikas kohanud süsteeme, kus pole osapoolte üldistust rakendatud, kuigi vajadus selle järgi on selgelt näha. Ilma üldistuseta muutub taolises koodibaasis klientidega tegelemine küllaltki keerukaks ja kohmakaks. Selles peatükis mängime, et me ei tea osapoolte üldisest midagi ja kordame nende vigu, kes on samas olukorras.

Et klientideks võivad olla nii ettevõtted kui eraisikud, siis on meil vaja kahte klassi nende esitamiseks.

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Code { get; set; }

    public string FullName
    {
        get 
        {
            return FirstName + ' ' + LastName;
        }
    }
}

public class Company
{
    public string Name { get; set; } 
    public string RegNo { get; set; }
}

Püüame nüüd kliendid siduda mõne teise klassi külge. Olgu selleks lihtne ja primitiivne arve klass.

public class Invoice
{
    public string InvoiceNo { get; set; }
    public decimal Total { get; set; }

    public Person Person { get; set; }
    public Company Company { get; set; }
}

Arve klassi vaadates ei tundu algajale miski kahtlane. Elukutseline tarkvaraarendaja näeb siin aga mitut probleemi:

  • Person ja Company eraldi omadustena tähendab kontrolle, et üks neist oleks arvel küljes, kuid teine oleks määramata.
  • Kui meil on vaja kuvada kliendi nime, siis peame kontrollima kas tegemist on ettevõtte või eraisikuga.
  • Arve klass ei ole arvatavasti viimane klass, mille külge kliendid seotakse ja seega tekib eeltoodud probleeme üle koodibaasi kordades rohkem kui praegu.

Kirjutame lihtsa programmi, mis aitab probleemi illustreerida.

var invoices = new [] 
{
    new Invoice
    {
        InvoiceNo = "ABC-1", 
        Person = new Person { FirstName = "John", LastName = "Smith" } 
    },
    new Invoice
    {
        InvoiceNo = "ABC-2",
        Company = new Company { Name = "Fictional Ltd" }
    }
};

foreach(var invoice in invoices)
{
    Console.Write(invoice.InvoiceNo);
    Console.Write(": ");

    if(invoice.Person != null)
    {
        Console.WriteLine(invoice.Person.FullName);
    }
    else 
    {
        Console.WriteLine(invoice.Company.Name);
    }
}
using System;

namespace OOP
{
    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Code { get; set; }

        public string FullName
        {
            get 
            {
                return FirstName + ' ' + LastName;
            }
        }
    }

    public class Company
    {
        public string Name { get; set; } 
        public string RegNo { get; set; }
    }

    public class Invoice
    {
        public string InvoiceNo { get; set; }
        public decimal Total { get; set; }

        public Person Person { get; set; }
        public Company Company { get; set; }
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            var invoices = new [] 
            {
                new Invoice
                {
                    InvoiceNo = "ABC-1", 
                    Person = new Person { FirstName = "John", LastName = "Smith" } 
                },
                new Invoice
                {
                    InvoiceNo = "ABC-2",
                    Company = new Company { Name = "Fictional Ltd" }
                }
            };

            foreach(var invoice in invoices)
            {
                Console.Write(invoice.InvoiceNo);
                Console.Write(": ");

                if(invoice.Person != null)
                {
                    Console.WriteLine(invoice.Person.FullName);
                }
                else 
                {
                    Console.WriteLine(invoice.Company.Name);
                }
            }
        }
    }
}

Tulemuseks on selline väljund:

ABC-1: John Smith
ABC-2: Fictional Ltd

Eeltoodud koodis olev if-kontroll on see, mida sai mainitud eespool. Ilma osapoolte üldistuseta peaksime selliseid kontrolle kirjutama kõikjal koodis, kus me mõne klassi külge omistatud kliendiga midagi teha soovime.

Osapoolte üldistuse rakendamine

Osapoolte üldistus seisneb selles, et me loome inimeste ja organisatsioonide ühtseks käsitlemiseks baasklassi Party. Party tähendab antud kontekstis osapoolt nagu näiteks lepingutel ja muudel tehingutel. Baasklass Party defineerib abstraktse omaduse DisplayName, mida baasklassist pärivad Person ja Company mõlemad oma loogika järgi pakuvad.

public abstract class Party
{
    public abstract string DisplayName { get; }
}

public class Person : Party
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public override string DisplayName
    {
        get 
        {
            return FirstName + ' ' + LastName;
        }
    }
}

public class Company : Party
{
    public string Name { get; set; }

    public override string DisplayName
    {
        get 
        {
            return Name;
        }
    }
}

Kuna nüüd on võimalik käsitleda ettevõtteid ja eraisikuid ühtsena, viime vastava muudatuse sisse ka arvete klassi.

public class Invoice
{
    public string InvoiceNo { get; set; }
    public decimal Total { get; set; }

    public Party Customer { get; set; }
}

Kirjutame ümber arvete kuvamise koodi ja vaatame kui palju lihtsamaks see muutus.

// Arvete kuvamine enne
// foreach(var invoice in invoices)
// {
//     Console.Write(invoice.InvoiceNo);
//     Console.Write(": ");

//     if(invoice.Person != null)
//     {
//         Console.WriteLine(invoice.Person.FullName);
//     }
//     else 
//     {
//         Console.WriteLine(invoice.Company.Name);
//     }
// }

// Arvete kuvamine nüüd
foreach(var invoice in invoices)
{
    Console.Write(invoice.InvoiceNo);
    Console.Write(": ");
    Console.WriteLine(invoice.Customer.DisplayName);
}
using System;

namespace OOP
{
    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Code { get; set; }

        public string FullName
        {
            get 
            {
                return FirstName + ' ' + LastName;
            }
        }
    }

    public class Company
    {
        public string Name { get; set; } 
        public string RegNo { get; set; }
    }

    public class Invoice
    {
        public string InvoiceNo { get; set; }
        public decimal Total { get; set; }

        public public Party Customer { get; set; }
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            var invoices = new [] 
            {
                new Invoice
                {
                    InvoiceNo = "ABC-1", 
                    Person = new Person { FirstName = "John", LastName = "Smith" } 
                },
                new Invoice
                {
                    InvoiceNo = "ABC-2",
                    Company = new Company { Name = "Fictional Ltd" }
                }
            };

            foreach(var invoice in invoices)
            {
                Console.Write(invoice.InvoiceNo);
                Console.Write(": ");
                Console.WriteLine(invoice.Customer.DisplayName);
            }
        }
    }
}

Vaadates arvete klassi ja arvete kuvamise tsüklit näeme, et osapoolte üldistus muutis lihtsamaks nii arvete klassi kui selle kasutamise. Me ei pea enam kontrollima kas klient on ettevõte või eraisik, sest me saame neid kahte vajadusel käsitleda sama asjana.

Pane tähele!

Party-klassis saame defineerida ka kõik muud omadused ja meetodid, mis on ühised nii ettevõtetele kui inimestele. Näiteks e-poe korral võime Party-klassis defineerida aadresside kollektsiooni, kuhu saame lisada arveldusaadressi ja aadressi, kuhu tellitud kaup tuleb toimetada.