Jak rozwiązać problem z dziedziczeniem - błędy typowania

1

Opis aplikacji

W ramach ćwiczeń napisałem algorytm, który wyznacza trasy w grafie pomiędzy jednym punktem, a drugim. Napisałem więc klasę bazową:

    public class Point
    {
        public int X { get; protected set; }
        public int Y { get; protected set; }

        public Point(int x, int y)
        {
            this.X = x;
            this.Y = y;
        }

        public Point()
        {
            this.X = 0;
            this.Y = 0;
        }

        public void UpdateLocation(int x, int y)
        {
            this.X = x;
            this.Y = y;
        }
    }

klasę wierzchołka:

    public class Vertex : Point
    {
        public IEnumerable<Vertex> Edges { get; private set; }

        public Vertex(int x, int y) : base(x, y)
        {
        }

        public Vertex() :base(0,0)
        {
        }

        public void AddEdge(IEnumerable<Vertex> edges)
        {
            this.Edges = edges;
        }
    }

oraz klasę grafu:

   public class Graph
    {
        public virtual List<Vertex> Vertexes { get; private set; } = new List<Vertex>();
 
        public Graph(List<Vertex> vertexes)
        {
            this.Vertexes = vertexes;
        }
 
        public Graph()
        {
 
        }
 
        public virtual void AddVertex(Vertex v)
        {
            Vertexes.Add(v);
        }
 
        public virtual void AddVertex(int x, int y)
        {
            Vertex v = new Vertex(x, y);
            AddVertex(v);
        }
 
        public Vertex GetVertex(int x, int y)
        {
            if (Vertexes.Count == 0)
                return null;
 
            Point searchFrom = new Point(x, y);
            Vertex temp = Vertexes.OrderBy(p => Helper.Distance(p, searchFrom)).FirstOrDefault();
            if (Helper.Distance(searchFrom, temp) < (Settings.CircleSize / 2))
                return temp;
 
            return null;
        }
 
        public virtual void RemoveVertex(Vertex v)
        {
            Vertexes.Remove(v);
        }
 
        public IEnumerable<Graph> FindPathsBetween(Vertex start, Vertex end)
        {
            // main code
        }
    }

I wszystko działa poprawne. Twoszę sobię instancje wierzchołków, następnie dodaję im połączenia z innymi wierzchołkami i używając FindPathsBetween dostaję trasę pomiędzy jednym, a drugim wierzchołkiem.

Co chcę osiągnąć

Zdecydowałem, że chciałbym przerobić program tak, by korzystał z GUI. Utworzyłem więc nowy projekt z wykorzystaniem WindowsForms oraz dodałem funkcjonalność dodawania wierzchołków poprzez kliknięcie na PictureBox. Wszystko działało poprawnie, jednak postanowiłem, że fajnie by było, gdyby po kliknięciu w PictureBox zamiast samej elipsy pojawiał się na niej tekst z numerem indeksu, czyli zamiana z czegoś takiego:
title
na:
title
Oczywiście w bardzo prosty sposób udało mi się to osiągnąć lekko modyfikując klasę Graph oraz Vertex. Wystarczyło dodać właściwość Id do wierzchołka, a w klasie Graph przechowywać informację o wolnym numerze wierzchołka.

Pierwsze wątpliwości

Aby uzyskać indeksowanie wierzchołków musiałem zmodyfikować klasę Graph oraz Vertex. Gdybym teraz w innej aplikacji chciał wykorzystać te klasy, to ich wierzchołki musiałby mieć indeksy (co mogłoby być niepotrzebne). Wydaje mi się, że to co zrobiłem jest złe. Wcześniej te klasy były bardziej ogólne. Teraz każda instancja obiektu Vertex posiada swój id dodany na potrzeby tylko tego, bym mógł zobaczyć to na GUi.

Dążę do tego, że wydaje mi się, że w momencie, kiedy chcę dodać do klasy jakąś nową funkcjonalność niekoniecznie związaną z tą klasą powinienem sworzyć nową klasę, która dodaje mi taką możliwość (i np. używa dziedziczenia). No przecież klasy Graph oraz Vertex mógłbym dostać w formie biblioteki i nie miałbym możliwości ich edytowania.

Stwierdziłem, że pozostawię klasy Graph oraz Vertex w takiej formie jakiej są, czyli najbardziej ogólnej. Skoro chcę ponazywać każdy wierzchołek, to mogę przecież stworzyć nową klasy wzbogacone o nowe właściwości i dziedziczyć po Graph oraz Vertex. W takim razie dodałem klasę FormVertex:

    class FormVertex : Vertex
    {
        public int Id { get; private set; }

        public FormVertex()
        {
        }

        public FormVertex(int id, int x, int y) : base(id, x, y)
        {
            this.Id = id;
        }
    }

A klasa FormGraph jest przecież identyczna jak Graph. Ma mieć te same metody. Jedyna różnica jest taka, że zamiast przechowywać informacje o Vertex będę przechowywał FormVertex. Potrzebuję jeszcze właściwości int NextVertexId, która będzie inkrementowana po dodaniu nowego wierzchołka.

Jaki jest problem

Dostaję błędy związane typowaniem. Klasa Graph w konstruktorze oczekuje listy typu Vertex, a do kontruktora FormGraph przesyłam przecież listę FormVertex.
title

Nie wiem również, czy sprawę dodawania FormVertex rozwiązałem poprawnie. Jak widać do base.AddVertex(fv); prześlę referencję do instancji fv, jednak będzie ona typu Vertex, a nie FormVertex (rzutowanie w górę). Jednak w klasie formularza będę mógł zrobić coś takiego:

foreach (var vertex in graph.Vertexes)
	if (v is FormVertex)
	{
		FormVertex fv = (FormVertex)v;
		// dostep do indeksu mam poprzez fv.Id;
	}

Macie więc jakieś wskazówki jak mogę rozwiązać moje problemy? Głównie chodzi mi o to, jak rozwiąząć sytuację z błędami typowania. Być może rozwiązanie jest bardzo proste, ale siedzę nad tym już tyle, że nie mam na nic pomysłu. Być może w ogólę źle podchodzę do problemu i zamiast dziedziczenia powinienem skorzystać z czegoś innego. Poszę wziąć pod uwagę to, że jestem początkujący w kodowaniu.

1

Dodaj do klasy Graph parametr generyczny i uczyń kolekcję Vertexes generyczną:

 public class Graph<TVertex>
    where TVertex : Vertex
    {
        public virtual List<TVertex> Vertexes { get; private set; } = new List<TVertex>();

i wtedy zdefiniuj class FormGraph : Graph<FormVertex>.

0
somekind napisał(a):

Dodaj do klasy Graph parametr generyczny i uczyń kolekcję Vertexes generyczną:

 public class Graph<TVertex>
    where TVertex : Vertex
    {
        public virtual List<TVertex> Vertexes { get; private set; } = new List<TVertex>();

i wtedy zdefiniuj class FormGraph : Graph<FormVertex>.
A jak mogę wtedy pozbyć się takich błędów?
title

1

W klasie generycznej nie używaj konkretnego typu tylko zawsze tego generycznego.

1

No nie możesz zrobić TVertex v = new TVertex(x, y); - ale możesz mieć np. abstract class Graph<TVertex> i class StandardGraph : Graph<Vertex> oraz class FormGraph : Graph<FormVertex. W StandardGraphiFormGraph` możesz już tworzyć obiekty konkretnych klas vertexów.

0

Tylko, że teraz to już mocno zmieniam wiele rzeczy w projekcie. Choćby to, że FindPathsBetween miało zwracać kolekcję instancji obiektówGraph, a teraz jest to klasa abstrakcyjna.. Nie chciałem ingerować właśnie w te klasy - powiedzmy, że dostałem je w takiej formie, a chciałbym mieć możliwość raz korzystania z grafu tylko w zwierzchołkami, a raz z wierzchołkami, które mają indeksy i np. kolor.

Ogólnie, to udało mi się rozwiązać problem z typowaniem i wygląda to tak (tylko w jednej klasie są zmiany):

// BEZ ZMIAN
public class Point
{
    public int X { get; protected set; }
    public int Y { get; protected set; }

    public Point(int x, int y)
    {
        this.X = x;
        this.Y = y;
    }
}

// BEZ ZMIAN
public class Vertex : Point
{
    public IEnumerable<Vertex> Edges { get; private set; }

    public Vertex(int x, int y) : base(x, y)
    {
    }

    public void AddEdge(IEnumerable<Vertex> edges)
    {
        this.Edges = edges;
    }
}

// BEZ ZMIAN
public class Graph
{
    public virtual List<Vertex> Vertexes { get; private set; } = new List<Vertex>();

    public Graph(List<Vertex> vertexes)
    {
        this.Vertexes = vertexes;
    }
	
	public virtual void AddVertex(Vertex v)
    {
        Vertexes.Add(v);
    }

    public IEnumerable<Graph> FindPathsBetween(Vertex start, Vertex end)
    {
        // main code
    }
}

// BEZ ZMIAN
public class FormVertex : Vertex
{
    public int Id { get; private set; }

    public FormVertex()
    {
    }

    public FormVertex(int id, int x, int y) : base(id, x, y)
    {
        this.Id = id;
    }
}

// TU SĄ ZMIANY
public class FormGraph : Graph
{
	public int NextVertexId {get; private set; } = 0;
	
	public FormGraph(List<Vertex> vertexes) : base(vertexes) // do konstruktora przesyłamy listę przechowującą obiekty typu `Vertex`, jednak obiekty w rzeczywistości są instancjami klasy typu `FormVertex`
	{
		NextVertexId = vertexes.Count;
	}
	
	public override void AddVertex(int x, int y)
	{
        // tworzymy instancję obiektu typu `FormVertex` a do klasa bazowa odbierze obiekt jako referencja o typie `Vertex`. 
		FormVertex fv = new FormVertex(NextVertexId++, x, y);
		base.AddVertex(fv);
		
		NextVertexId++;
	}
}

Teraz mogę zrobić tak:

FormVertex fv0 = new FormVertex(0, 4, 4);
FormVertex fv1 = new FormVertex(1, 7, 1);
FormVertex fv2 = new FormVertex(2, 5, 2);

// dodaję wierzchołki poprzez konstruktor
FormGraph formGraph = new FormGraph(new List<Vertex>() { fv0, fv1, fv2 });

// dodaję wierzchołki poprzez metodę
formGraph.AddVertex(10, 20);
formGraph.AddVertex(30, 30);
formGraph.AddVertex(40, 40);

// jako, że formGraph.Vertexes jest listą typu Vertex nie mam możliwości dostania się do Id. Wiem jednak, że dodałem obiekty typu FormVertex, więc poprzez rzutowanie mogę się do nich dostać

foreach (var vertex in formGraph.Vertexes)
{
    if (vertex is FormVertex)
    {
        FormVertex fv = vertex as FormVertex;
        Console.WriteLine(fv.Id);
    }
}

I dostaję wynik:

0
1
2
3
4
5

Moje rozwiązanie ma jednak pewną lukę. Mogę zrobić tak:

Vertex v = new Vertex(1, 2);
formGraph.AddVertex(v);

Mogę dodać obiekt typu Vertex. Ale to chyba nieuniknione, w końcu FormVertex dziedziczy po Vertex. Czy jest w ogóle możliwość, by mógł zrobić tak, że nie będę mógł dodać do formGraph.Vertexes obiektu typu Vertex? W końcu klasa FormGraph jest przeznaczona do przechowywania wierzchołków, które mają identyfikatory. Nie chciałbym tam móc dodać wierzchołka bez identyfikatora.

Nie mam pojęcia, czy moje rozwiązanie jest dopuszczalne. Prawdopodobnie Twoje jest lepsze, jednak na tym etapie nie jestem w stanie dostrzeć zalet i wad tych rozwiązań. Twoje wydaje mi się przekombinowane. Poza tym, w Twoim rozwiązaniu ingeruję w klasy, a tego chciałem uniknąć. Może nie powinienem tego unikać? Ale jeśli nie powinienem, to co w przypadku, gdybym nie miał możliwości edytowania klas Graph i Vertex?

0

Ja bym kompletnie zrezygnował z dziedziczenia na rzecz kompozycji. Wierzchołek który wyświetlamy ma zupełnie inną odpowiedzialność/powód istnienia niż ten na którym operuje algorytm.

    public class FormVertex
    {
       public int Id { get; private set; }

       public Vertex Vertex {get; private set;}

       
        public FormVertex(Vertex vertex ) 
        {
            this.Vertex = vertex;
        }
    }

1 użytkowników online, w tym zalogowanych: 0, gości: 1