Czy klasy abstrakcyjne są jeszcze potrzebne?

1

Niedługo nadchodzi java 9 z prywatnymi metodami w interfejsach. W javie 8 już widzę coraz popularniejsze i śmielsze używanie metod domyślnych (Ciekawe czy śmiali producenci bibliotek zawsze pilnują, aby się zdublowane nazwy nie pogryzły...).

Kiedy używacie klas abstrakcyjnych? Osobiście nie pamiętam kiedy ostatnio użyłem klasy abstrakcyjnej.
Jakie macie co do tego przemyślenia?

1

Widziałem pytanie. Pomyślałem, że ciekawe... ale nie mam zdania. (Też nie kojarze kiedy ostanio klasę abstrakcyjną napisałem).

Ale mam gorsze problemy.. ostanio na pokaz frameworka (Lagom) napisałem sieciowego PONGa (tego z lat 70-tych).
https://github.com/javaFunAgain/lagomPong (nadal są tam hello world przykłady z doku - troszkę w pośpiechu pisałem demko na prezentacje )..

Ale ciekawostka... po stronie serwera była mi "potrzebna" tylko jedna **zmienna **(to znaczy wszystkie pozostałe pola i zmienne lokalne sę efektywnie finalne)..
Ta jedna zmienna pełni tylko funkcję pomocniczą (stopuje przeliczanie gier jak nikt nie jest podłączony do serwera).
Po stronie javaskryptu (scalajs) - mam ze 3 zmienne (bo w pośpiechu zrypałem design).

I tu problem - czy to nadal java? Czy ogarnę ten kod za miesiąc?

2

czy to nadal java?

@jarekr000000 brzmi bardziej jak erlang ;)

@InterruptedException istotna różnica jest nadal taka, że interfejs nie zawiera pól więc jeśli implementacja wymaga interakcji z jakimiś zewnętrznymi obiektami to może nie wystarczyć. Oczywiście można zawsze zrobić sobie w interfejsie metodę abstrakcyjną getX ale ryzykujemy że 100 razy zaimplementujemy tą metodę w ten sam sposób.

0

W sumie mnie to troche dziwi że interface ma mieć prywatne metody

1

Jeśli o mnie chodzi to pewnie będzie po staremu - jak jest relacja "X is Y" to Y będzie klasą abstrakcyjną, kiedy będzie relacja "X does what Y" to będzie interfejsem.

0

@InterruptedException:

public interface Reader<S, T, R> {
	R map(T element);
	public Collection<R> read(S source);
}

public abstract class FileReader<R> implements Reader<Path, String, R>{
	public Collection<R> read(Path sourcePath){
		try {
			return Files.lines(sourcePath)
				.map(this::map)
				.collect(Collectors.toList());
		catch(Exception e){
			return Collections.EMPTY_LIST;
		}
	}
}

public class SingleFieldCsvReader<String> extends FileReader<String> {
	private final int column;
	private final String delimeter;
	public SingleFieldCsvReader(String delimeter, int column){
		this.column = column;
		this.delimeter = delimeter;
	}

	@Override
	String map(String element){
		String[] array = element.split(delimeter);
		
		return array[column];
	}
}
2

@InterruptedException: klas abstrakcyjnych trochę w ostatnim projekcie naklepałem, ale dlatego, że był tam MmVmC (Model-multiView-multiController) i wydzielaliśmy fragmenty służące do ogarnięcia różnych metod prezentacji i pracy z tym co przysyła nam klient. Reszta kodu była taka sama więc dziedziczenie było OK.

W Javie 9 będzie na początku tak, że ludzie naklepią się tych metod prywatnych w interfejsach, a potem okaże się, że to nie do końca działa jak powinno. W rzeczywistości może być to niezłe rozwiązanie w kontekście dyskusji o DI i wyrugowania większości DI z projektów. Pozwoli na eleganckie rozdzielenie danych i przepływu bez obawy, że w publicznym API mamy jakieś wewnętrzne bebechy.

@Shalom dobrze prawi, że te interfejsy nadal nie posiadają pól i nadal będziemy musieli to jakoś ogarnąć. Choć skłaniam się ku opcji, gdzie kod będzie wyglądać tak:

interface CośTam{
     Coś getCoś();
     // reszta kodu
}

@lombok.Getter
@RequiredArgsConstructor
class CośTamCośTam implements CośTam{

   private final Coś coś;

}

i efektywnie implementacja spada na Lomboka... tylko po co tak, skoro można użyć Kotlina?

Kolejna sprawa, to otwarcie drogi do pisania scalo-podobnego i wprowadzenia nowych elementów funkcyjnych do języka. Będzie ciekawie.

3

Metody domyślne służą głównie do zachowania kompatybilności wstecznej (ang. "defender methods").

To jest raczej ostatnia deska ratunku, bo główna zaleta interfejsów to brak jakichkolwiek implementacji zachowań - dzięki czemu nie ma wtórnych zależności, które są zbędne dla użytkownika interfejsu. Klasy abstrakcyjne to głównie szkielety zachowań dla klas po nich dziedziczących.
Koncepcyjnie to są dwie bardzo różne rzeczy, ale technicznie zbliżone.

Tutaj materiał od Oracle'a do przemyślenia ("Abstract Classes Compared to Interfaces"):
http://docs.oracle.com/javase/tutorial/java/IandI/abstract.html

0

W kotlinie możemy stworzyć właściwość, która nie posiada pola (z jasnych względów). W klasie implementującej wystarczy to pole zaimplementować i wszystko pięknie współgra.

Historycznie widzę to tak:

  1. Dziedziczmy jak leci! (C++)
  2. Wielodziedziczenie stwarza problemy. Pozwólmy dziedziczyć tylko raz (klasy abstrakcyjne), ale deklaracje zachowań nic nie psują, więc niech tutaj nie będzie ograniczeń (interfejsy). (Java i przyjaciele)
  3. Przy kilku założeniach które można sprawdzić statycznie, dużo więcej rzeczy możemy zadeklarować (Kotlin, kolejne wersje javy).

Jest jeszcze scala która liniuje (dobre słowo?) klasy abstrakcyjne, ale moim zdaniem to kiepski pomysł i ślepa uliczka. Nie jest to oczywiste co się dzieje na początku i może powodować błędy.
Myślę że droga Javy jest dobra. Interfejs przestał być już interfejsem, ale jakoś mnie to nie boli bo wszystko co się dzieje jest bardzo oczywiste, pilnowane przez kompilator i ma sens.
Obecnie widzę sens używania klas abstrakcyjnych tylko jeżeli używasz pól. Metoda z getem i lombokiem nie jest zła, ale jednak nie tak bardzo czysta jak w kotlinie.

0

W kotlinie możemy stworzyć właściwość, która nie posiada pola (z jasnych względów). W klasie implementującej wystarczy to pole zaimplementować i wszystko pięknie współgra.

Historycznie widzę to tak:

  1. Dziedziczmy jak leci! (C++)
  2. Wielodziedziczenie stwarza problemy. Pozwólmy dziedziczyć tylko raz (klasy abstrakcyjne), ale deklaracje zachowań nic nie psują, więc niech tutaj nie będzie ograniczeń (interfejsy). (Java i przyjaciele)
  3. Przy kilku założeniach które można sprawdzić statycznie, dużo więcej rzeczy możemy zadeklarować (Kotlin, kolejne wersje javy).

Jest jeszcze scala która liniuje (dobre słowo?) klasy abstrakcyjne, ale moim zdaniem to kiepski pomysł i ślepa uliczka. Nie jest to oczywiste co się dzieje na początku i może powodować błędy.
Myślę że droga Javy jest dobra. Interfejs przestał być już interfejsem, ale jakoś mnie to nie boli bo wszystko co się dzieje jest bardzo oczywiste, pilnowane przez kompilator i ma sens.
Obecnie widzę sens używania klas abstrakcyjnych tylko jeżeli używasz pól. Metoda z getem i lombokiem nie jest zła, ale jednak nie tak bardzo czysta jak w kotlinie.

3

@wartek01: @caer @jarekr000000 @twonek

Kod w kotlinie:

 open class Rectangle(val xSize: Int, val ySize: Int)
 class Square(size: Int) : Rectangle(size, size)

Kwadrat jest prostokątem czyli wszystkie obliczenia i wzory pasujące do prostokąta, można zastosować do kwadratu. LSP zachowane i logika uszanowana.Jedyny haczyk, to niezmienność tych obiektów, ale czy to jest problem?

  • Wersja specjalnie dla @caer
 interface Rectangle{
    val xSize: Int
    val ySize: Int
}
class IrregularRectangle(override  val xSize: Int, override val ySize: Int) : Rectangle
class Square(private val size: Int) : Rectangle{
    override val xSize: Int
        get() = size
    override val ySize: Int
        get() = size
}
4

Niezmienność obiektów to nie problem -> to zaleta.
Jeżeli twój "kontrakt" na Rectangle to robienie obliczeń z a i b -(xSize i ySize) - to taka implementacja jest bardzo dobra.
Dodatkowy plus to że zgadza się z intuicją : czyli kwadrat to specyficzny prostokąt.

0

Kwadrat jest prostokątem czyli wszystkie obliczenia i wzory pasujące do prostokąta, można zastosować do kwadratu. LSP zachowane i logika uszanowana.Jedyny haczyk, to niezmienność tych obiektów, ale czy to jest problem?

Zabijasz cały problem bez de facto rozwiązania go :)

W problemie kwadratu/prostokąta (albo koła i elipsy) właśnie chodzi o to, jak mają zachowywać się settery - natomiast niezmienne obiekty z definicji ich nie mają. Jednym z proponowanych rozwiązań jest coś takiego:

 public class Rectangle {
	private int width; 
	private int height;
	
	Rectangle(int width, int height){
		this.width = width;
		this.height = height;
	}

	public Rectangle setWidth(int width){
		if(this.height = width){
			return new Square(width);
		}

		return new Rectangle(width, this.height);
	}
	// itp. 
}

i wtedy można się zastanawiać, czy settery nie są mylące. Jak pewnie zauważyłeś - temat-rzeka.

4

jedyny set(t)er, który się dobrze zachowuje to taki:
dobry set(t)er

2

bardzo rzadko tworze klasy abstrakcyjne, wyjatkami sa klasy szkieletowe ktore maja jakis mutowalny stan (np kolekcje) czy klasy basowe dla serwerow ktore daja wygodne api dla implementatorow tzn sa praktycznie w pelni funkcjonalnym mikroserwisem z opcjonalna personalizacja (t.j. override) zbioru metod.
update do java 8 czy w przyszlosci 9 nic nie zmieni w tej kwestii.

0

@wartek01
Piękny przykład :). Zauważ że to co napisałeś to nie jest setter w powszechnym tego słowa znaczeniu. Ta metoda zawsze tworzy nowy obiekt i bardzo ładnie by współgrała z moim kodem. Bo przecież po zmianie width, miałbyś już 2 obiekty a nie 1, jak w przypadku standardowego settera. Kwadrat bazowy pozostanie kwadratem takim jakim jaki go programista stworzył, a to co powstanie po zrobieniu setWidth to zupełnie inny obiekt. Jakiego typu? Ważne tylko że Rectangle. LSP uszanowane. Zauważ że tak działa każdy typ immutable, chociażby String::substring.

Przyczepiłbym się tylko do tego że Rectangle nie powinno używać Square. Setter w Rectangle zawsze tworzy Rectangle, nawet jeżeli oba wymiary są takie same, a Square może mieć implementacje Twojego settera.

0

Przyczepiłbym się tylko do tego że Rectangle nie powinno używać Square. Setter w Rectangle zawsze tworzy Rectangle, nawet jeżeli oba wymiary są takie same, a Square może mieć implementacje Twojego settera.

Właśnie w takim przypadku można użyć klasy abstrakcyjnej, Zaimplementować praktycznie wszystko a utworzenie nowego obiektu delegować do klasy potomnej jako metode abstrakcyjną. Miałbyś taką sama funkcjonalność jak tak o której piszesz, bez konieczności dopisywania do każdej metody czegoś w stylu return IfSameSizeGetNewSquareElseGetNewRect()

Albo napisać fabrykę i z ifować w niej wszystkie zależności miedzy wymiarem a klasą do utworzenia.

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