Tworzenie abstrakcji według Uncle Boba.

0

Czytam książkę "Czysty kod" autorstwa Robert C. Martin a w niej znalazłem coś takiego(str. 114):

class Point
{
public double x,y;}

// jest gorsze niż:
interface Point
{
double GetX();
double GetY();
}

Argumentuje to tym, że ukrywa implementacje danych i nie sposób określić na jakich wew. operuje. Nie do końca to rozumiem.

Próbuje stworzyć klasę, interfejs, który będzie najlepiej reprezentować punkt w układzie kartezjańskim oraz da możliwość późniejszego rozszerzenia do punktu 3D lecz w układzie kartezjańskim będzie można operować na liczbach całkowitych i rzeczywistych. W konsoli potrzebuje tych pierwszych a w WPF drugich.
Tak więc wpadłem na pomysł zastosowania typów generycznych z wykorzystaniem interfejsu.

interface IPoint<T>
{
T X {get; }
T Y {get; }
// gdzie tutaj jest mowa o tym o czym mówi Pan Martin? O ukrywaniu implementacji? Jeżeli użyje właściwości a metod.
T GetX();
T GetY();
}

// A klasa
class Point : IPoint<int> // dla konsoli - potrzebuje możliwości operacji arytmetycznych na X i Y więc muszę podać typ :( Jak to obejść? 
public int X {get; private set; }
// ...
public int GetX() // ..

Nie rozumiem jak tutaj stworzyć tą abstrakcje. Czy w ogóle będzie ona potrzebna? Jedyną klasą która implementuje IPoint jest Point może kiedyś będzie Point3D.
Ale chcę zrobić Point dla int i double a tworzenie klasy

class Point : IPoint<int> /* i osobno */ IPoint<double>  

nie zadowala mnie. Jak zrobić by móc użyć operacji arytmetycznych na obiekcie T(w ogóle z tego korzystać?) w klasie Point.

0

Właściwość X { get; ] i metoda GetX() to to samo, w C# po prostu raczej używa się pierwszej wersji.
Martinowi chodziło o to, aby nie tworzyć publicznych pól, tylko udostępniać dane przechowywane przez klasę za pomocą metod/właściwości tylko do odczytu. W ten właśnie sposób ukrywasz implementację. Klient Twojej klasy wie tylko to, co mu potrzebne - jak pobrać wartość, nie musi wiedzieć jak się ją ustawia.
Tworzenie abstrakcji przez użycie interfejsu sprawia, że kod staje się bardziej elastyczny, bo wówczas do metod, które przyjmują obiekty danego interfejsu możesz przekazywać obiekty różnych klas.

Nie sądzę, aby dało się stworzyć sensowny wspólny interfejs dla punktu 2D i 3D.

Jak już Ci pisałem w poprzednim wątku - nie zrobisz w C# ograniczenia dla typów generycznych na operatory matematyczne.
Co dokładnie chcesz dodawać, może wymyślimy inny sposób.

Ogólnie uważam, że ten przykład jest akurat zbyt prosty jak na implementowanie jakichś wymyślnych abstrakcji i tworzenie intefejsów. Punkt to punkt, nie wyrośnie mu nigdy trzecia ręka ani nowy algorytm kompresji. Książka Martina jest bardzo radykalna, rzekłbym nawet ekstremistyczna i nie trzeba za nią podążać w 100%.

A - i punkt to akurat świetny przykład czegoś, co w C# powinno być strukturą, a nie klasą.

0

Mógłbyś rozszerzyć dlaczego miałby być strukturą?

0

Martin nie sugerował, by robić dziedziczenie takie jak ty opisujesz. Zresztą jeśli byś go zrobił, to i tak musiałbyś nadpisać wszystkie metody, bo np dodawanie dwóch punktów 2W jest inne niż dodawanie dwóch punktów 3W.

Ale możesz się pobawić. Zaszyj w interfejsie Point metody do dodawania punktów, skalowania, negowania, etc i zrób to generyczne, a będziesz mógł potem stworzyć generyczne metody operujące na dowolnych punktach. Przykład w Javie:

interface Point<T extends Point<T>> {
    T add(T that);
}

class Point2D implements Point<Point2D> {
    double x, y;

    public Point2D(double x, double y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public Point2D add(Point2D that) {
        return new Point2D(x + that.x, y + that.y);
    }

    @Override
    public String toString() {
        return String.format("Point2D{x=%s, y=%s}", x, y);
    }
}

class Point3D implements Point<Point3D> {
    double x, y, z;

    public Point3D(double x, double y, double z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    @Override
    public Point3D add(Point3D that) {
        return new Point3D(x + that.x, y + that.y, z + that.z);
    }

    @Override
    public String toString() {
        return String.format("Point3D{x=%s, y=%s, z=%s}", x, y, z);
    }
}

public class MainJ {
    @SafeVarargs
    static <T extends Point<T>> T addPoints(T head, T... tail) {
        T result = head;
        for (T point : tail) {
            result = result.add(point);
        }
        return result;
    }

    public static void main(String[] args) {
        Point2D p2a = new Point2D(1, 2);
        Point2D p2b = new Point2D(3, 4);
        Point2D sum2 = addPoints(p2a, p2b, p2a);
        System.out.println(sum2);
        Point3D p3a = new Point3D(1, 2, 3);
        Point3D p3b = new Point3D(3, 4, 5);
        Point3D sum3 = addPoints(p3a, p3b, p3a);
        System.out.println(sum3);
    }
}

Jak widać jest tylko dodawanie punktów, ale to wystarcza do zobrazowania pomysłu. Metoda addPoints jest w stanie zarówno dodać punkty 2W jak i 3W. Analogicznie można by zrobić metodę zwracającą punkt z uśrednionymi współrzędnymi lub nawet zabawić się w generyczną metodę k-średnich.

PS: Nie wiem czy w C# da się zrobić coś takiego :P

0
Młoteczek napisał(a):

Mógłbyś rozszerzyć dlaczego miałby być strukturą?

Bo przechowuje pojedynczą wartość, która nie powinna być zmieniana po utworzeniu i zajmuje w pamięci mniej niż 16 bajtów.
Oryginalny System.Drawing.Point to też struktura, więc jakiś sens to ma. :)

0

To co napisaliście prawdopodobnie zda egzamin jednak potrzebuje użycia operatorów a nie osobnych metod.

Póki co to zrobiłem to tak:

  1. Zrezygnowałem z interfejsu. W przypadku klasy 3D będzie dziedziczyć po 2D i doda nowe zmienne. W końcu po co interfejs dla tylko jednego obiektu, który będzie go implementować?
    Według Internetu interfejs reprezentuje "zachowania" które implementują klasy nie powiązane ze sobą. Więc czemu klasa Point ma implementować IPoint?

  2. W całych projekcie zamiast tworzenia niestandardowych typów generycznych użyłem "dynamic" i tam gdzie potrzebny int przesyłam inta a tam gdzie double to double a gdzie char to char ;)

Czy jest w tym jakieś zagrożenie lub coś czemu nie stosować tak często typów dynamic?

0
Młoteczek napisał(a):

To co napisaliście prawdopodobnie zda egzamin jednak potrzebuje użycia operatorów a nie osobnych metod.

To, że potrzebujesz operatorów to raczej szczegół implementacji wewnętrznej tego, co chcesz z nimi robić.

W końcu po co interfejs dla tylko jednego obiektu, który będzie go implementować?

Interfejs dla punktu, który ma mieć tylko X i Y faktycznie nie ma sensu, ale dla punktu, który ma mieć metody wyrażający konkretne operacje, które można na punkcie zrobić, może mieć sens.

Z drugiej strony, zrób po prostu klasę/strukturę, która będzie miała x i y jako double i tyle. To zbyt prosty przypadek, żeby się męczyć z definiowaniem abstrakcji.

Czy jest w tym jakieś zagrożenie lub coś czemu nie stosować tak często typów dynamic?

  1. Jest to bardzo niewydajne.
  2. Nie daje żadnej weryfikacji typów na etapie kompilacji, skutkiem czego możesz mieć bardzo brzydkie (i nic nie mówiące) błędy w trakcie działania programu.
0

Ok, dzięki wielkie za odpowiedzi mimo mojej oporności w przyjęciu wiedzy od doświadczonych ;)
Póki co zostaje dynamic ponieważ działa.

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