Prinzipien der Objektorientierung: Kapselung

Heute starte ich eine kleine Serie über objektorientierte Prinzipien, die man grundsätzlich immer wieder anwenden sollte. Den Anfang macht: Die Kapselung. Dazu bediene ich mich des Beispiels einer User-Suche (ja ich weiß, das ist nicht sonderlich originell, aber es ist leicht zu verstehen). User sind gekennzeichnet durch Informationen wie User-ID, Name, Vorname, Geburtsdatum, oder Telefonnummer. Ein ganz naiver Ansatz zur Suche nach einem solchen User könnte so aussehen:


class User
{
    private string id;
    private string vorname;
    private string name;
    private DateTime geburtsdatum;
    private string telefonnummer;


    public User(string id, string vorname, string name, DateTime geburtsdatum, string telefonnummer)
    {
        this.id = id;
        this.vorname = vorname;
        this.name = name;
        this.geburtsdatum = geburtsdatum;
        this.telefonnummer = telefonnummer;
    }

    public string ID
    {
        get { return id; }
    }

    public string Vorname
    {
        get { return vorname; }
    }

    public string Name
    {
        get { return name; }
    }

    public DateTime Geburtsdatum
    {
        get { return geburtsdatum; }
    }

    public string Telefonnummer
    {
        get { return telefonnummer; }
    }

}  

class UserDatabase
{
    private List<User> users;

    public UserDatabase()
    {
        users = new List<User>();
    }

    public void AddUser(string id, string vorname, string name, DateTime geburtsdatum, string telefonnummer)
    {
        User user = new User(id, vorname, name, geburtsdatum, telefonnummer);
        users.Add(user);
    }
    public User GetUserByID(string userID)
    {
        foreach (User _user in users)
        {
            User u = _user;
            if (u.ID.Equals(userID))
                return u;
        }
        return null;
    }
    public User Search(User user)
    {
        foreach (User _user in users)
        {
            User u = _user;
            string name = u.Name;
            if ((name != null) && (!name.Equals("")) && (!name.Equals(u.Name)))
                continue;
            string vorname = u.Vorname;
            if ((vorname != null) && (!vorname.Equals("")) && (!vorname.Equals(u.Vorname)))
                continue;
            string telefonnummer = u.Telefonnummer;
            if ((telefonnummer != null) && (!telefonnummer.Equals("")) && (!telefonnummer.Equals(u.Telefonnummer)))
                continue;
            DateTime geburtsdatum = u.Geburtsdatum;
            if ((!geburtsdatum.ToShortDateString().Equals("")) && (!geburtsdatum.Equals(u.Geburtsdatum)))
                continue;
            return u;
        }
        return null;
    }
}

Sieht chaotisch aus? Ist es auch. Aber immerhin wird hier schon ein bisschen Kapselung betrieben. Alle Attribute der User-Klasse sind private und nur durch Properties zugreifbar. Bravo! Das ist eine Art der Kapselung. Man kontrolliert den Zugriff. Man versteckt das wahre Attribut vor dem Client. Das ist aber noch nicht alles, was man unter dem Begriff der Kapselung zusammenfassen kann. Aber was ist jetzt eigentlich das Problem an diesem Entwurf? Die vielen Parameter im Konstruktor von User und in der Add-Methode von UserDatenbank sind ein wahrer Graus. Beide Klassen hängen von diesen Variablen ab. Ändert sich eine Variable in der einen Klasse, muss man sofort auch Änderungen in der anderen Klasse vornehmen, um Fehler zu vermeiden. Eine so große Anzahl von Parametern ist sowieso nicht wirklich wünschenswert und obendrein wird alles auch noch schlecht les – und überschaubar. Man sollte aber erst mal bei den Strings zwischen Groß – und Kleinschreibung unterscheiden, da es sonst dort bei Tippfehlern schon zu Fehlern kommen kann. Eigentlich ist es das aber nur Ergebniskosmetik. Was weiterhin ins Auge fällt, ist, dass nach dem ersten Suchergebnis abgebrochen wird. Wenn also 10 User Meier heißen, dann wird bei der Suche nur der erste gefundene ausgegeben. Das hat jetzt nichts mit Kapselung zu tun, ist aber trotzdem Mist. Was kann man also dagegen unternehmen? Man könnte eine Ergebnisliste zurückgeben. Ist bei Google schließlich auch so. Also los!

public List<User> Search(User user)
{
    List<User> gefundeneUser = new List<User>();
    foreach (User _user in users)
    {
        //...
        gefundeneUser.Add(u);
    }
    return gefundeneUser;
}

Nachdem das geschafft ist, nun zurück zum Hauptthema: Der Kapselung. Kapselung findet nämlich nicht nur bei private und public statt, sondern hilft einem auch, eine Anwendung logisch aufzuteilen. Das Problem am obigen Code ist, dass es viel zu viele Suchparameter gibt, die auch noch in verschiedenen Klassen vorkommen. Diese Klassen sind eng gekoppelt, denn das Hinzufügen von einem neuen Suchparameter zieht notwendige Änderungen in weiteren Klassen nach sich. Diese Suchparameter sollten demzufolge in eine eigene Klasse gekapselt werden. Daher baut man sich eine Klasse UserSearchData und passt die anderen Klassen entsprechend an, indem man sie nur noch mit einem Objekt vom Typ UserSearchData umgehen lässt (dieses Prinzip nennt sich Delegation). Das sieht dann so aus:

class User
{
    private string id;
    private UserSearchData data;

    public User(string id, UserSearchData data)
    {
        this.id = id;
        this.data = data;
    }

    public string ID
    {
        get { return id; }
    }

    public UserSearchData Data
    {
        get { return data; }
    }
}

class UserSearchData
{
    private string telefonnummer;
    private string vorname;
    private string name;
    private DateTime geburtsdatum;

    public UserSearchData(string telefonnummer, string vorname, string name, DateTime geburtsDatum)
    {
        this.telefonnummer = telefonnummer;
        this.vorname = vorname;
        this.name = name;
    }

    public bool Matches(UserSearchData userData)
    {
        if (name.ToLower() != userData.Name.ToLower())
            return false;
        if (vorname.ToLower() != userData.Vorname.ToLower())
            return false;
        if (telefonnummer != userData.Telefonnummer)
            return false;
        if (geburtsdatum != userData.Geburtsdatum)
            return false;
        return true;
    }

    public string Telefonnummer
    {
        get { return telefonnummer; }
    }

    public string Vorname
    {
        get { return vorname; }
    }

    public string Name
    {
        get { return name; }
    }

    public DateTime Geburtsdatum
    {
        get { return geburtsdatum; }
    }
}

class UserDatenbank
{
    //...
    public void AddUser(string id, UserSearchData data)
    {
        User user = new User(id, data);
        //...
    }
    public List<User> Search(UserSearchData userData)
    {
        //...
        foreach (User user in users)
        {
            User u = user;
            if (u.Data.Matches(userData))
                gefundeneUser.Add(u);
        }
        //...
    }
}

Durch diese Änderungen hat das Design wirklich eine logische Kapselung erfahren. Fügt man nun neue Attribute zum User hinzu, wirkt sich das nicht mehr auf die anderen Klassen aus, wie das zuvor der Fall war, denn auch der Algorithmus, der prüft, ob Suchanfragen erfolgreich waren, befindet sich nun an der Stelle, an die er gehört: bei den User-Daten. Das Veränderliche wurde gekapselt und das Programm obendrein sogar noch besser erweiterbar und wartbar gemacht. Zum Ende unterziehe ich den Code noch ein paar kosmetischen Korrekturen. Das hat mit dem Thema Kapselung aber nichts mehr zu tun, sondern ist eher als kleineres Refactoring zu sehen. Der gesamte Code:

using System;
using System.Collections.Generic;

namespace Kapselung
{
    class User
    {
        private readonly string id;
        private readonly UserSearchData data;
        
        public User(string id, UserSearchData data)
        {
            this.id = id;
            this.data = data;
        }

        public string ID
        {
            get { return id; }
        }
    
       public UserSearchData Data
        {
            get { return data; }
        }
    }

    class UserDatabase
    {
        private readonly List<User> users;

        public UserDatabase()
        {
            users = new List<User>();
        }

        public void AddUser(string id, UserSearchData data)
        {
            var user = new User(id, data);
            users.Add(user);
        }
        public User GetUserByID(string userID)
        {
            foreach (var user in users)
            {
                var u = user;
                if (u.ID.Equals(userID))
                    return u;
            }
            return null;
        }
        public List<User> Search(UserSearchData userData)
        {
            var matchingUsers = new List<User>();
            foreach (var user in users )
            {
                var u = user;
                if(u.Data.Matches(userData))
                    matchingUsers.Add(u);
            }
            return matchingUsers;
        }
    }

    class UserSearchData
    {
        private readonly string telephoneNumber;
        private readonly string firstName;
        private readonly string lastName;
        private readonly DateTime birthDate;

        public UserSearchData(string telephoneNumber, string firstName, string lastName, DateTime birthDate)
        {
            this.telephoneNumber = telephoneNumber;
            this.firstName = firstName;
            this.lastName = lastName;
            this.birthDate = birthDate;
        }

        public bool Matches(UserSearchData userData)
        {
            if (lastName.ToLower() != userData.LastName.ToLower())
                return false;
            if (firstName.ToLower() != userData.FirstName.ToLower())
                return false;
            if (telephoneNumber != userData.TelephoneNumber)
                return false;
            if (birthDate != userData.BirthDate)
                return false;
            return true;
        }

        public string TelephoneNumber
        {
            get { return telephoneNumber; }
        }

        public string FirstName
        {
            get { return firstName; }
        }

        public string LastName
        {
            get { return lastName; }
        }

        public DateTime BirthDate
        {
            get { return birthDate; }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var db = new UserDatabase();
            db.AddUser("1",new UserSearchData("1234","hans","Schmidt",new DateTime(1989,02,01)));

            var data = new UserSearchData("1234","Hans","Schmidt",new DateTime(1989,02,01));
            var users = db.Search(data);
            if(users.Count!=0)
                Console.WriteLine("User found!");
            else
                Console.WriteLine("User not found");
            Console.Read();
        }
    }
}

Zusammenfassend lässt sich also sagen, dass Kapselung eben nicht nur auf Information Hiding abzielt, sondern auch für logische Trennung von Programmteilen verwendet werden kann. Man strebt danach, Dinge, die häufig zu Änderungen führen, an einen bestimmten Orten zu platzieren. Delegation ist dabei ein nützliches Werkzeug.
PS: Ich weiß, dass diese Suche keine tolle Funktionalität bietet. Es ging ja auch weniger um den Suchalgorithmus, als um das Prinzip der Kapselung 🙂

Advertisements

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s