odwiedzacz, double dispatch

1

Często odwiedzacz pattern jest rzucany wszędzie tam, gdzie pojawia się potrzeba przejścia po drzewie, ale jakoś nie potrafię zrozumieć jego zalety względem zwykłego przejścia (czy rekursywnego czy iteratywnego) oraz sensownego triku języka lub w ostateczności switcha

z jednej strony fajnie bo pozwala nam zgrabnie dodawać wielu visitorów, ale co gdy tego nie potrzebujemy?

Oczywiście przez przypadek ;) gdy szukałem info nt. visitora to trafiłem na krytykę
http://nice.sourceforge.net/visitor.html

i generalnie wyszły mi takie kody:

Visitor:

public class Document
{
	public List<DocumentPart> Parts = new List<DocumentPart>();

	public void Accept(IVisitor visitor)
	{
		foreach (var child in Parts)
		{
			child.Accept(visitor);
		}
	}
}

public interface IVisitor
{
	void Visit(AddPart addPart);

	void Visit(MultiplyPart multiplyPart);
}

public abstract class DocumentPart
{
	public List<DocumentPart> Children { get; set; } = new List<DocumentPart>();
	public string Text { get; set; }
	public abstract void Accept(IVisitor visitor);
}

public class AddPart : DocumentPart
{
	public int Value { get; set; } = 15;
	public override void Accept(IVisitor visitor)
	{
		visitor.Visit(this);

		foreach (var child in Children)
		{
			child.Accept(visitor);
		}
	}
}

public class MultiplyPart : DocumentPart
{
	public int Value { get; set; } = 35;
	public override void Accept(IVisitor visitor)
	{
		visitor.Visit(this);

		foreach (var child in Children)
		{
			child.Accept(visitor);
		}
	}
}


public class MyVisitor : IVisitor
{
	public void Visit(AddPart addPart)
	{
		Console.WriteLine($"AddPart {addPart.Text + addPart.Value}");
	}

	public void Visit(MultiplyPart multiplyPart)
	{
		Console.WriteLine($"MultiplyPart {multiplyPart.Text + multiplyPart.Value}");
	}
}

użycie:

public static void Main()
{
    var doc = new Document();

    var part = new AddPart() { Text = "add" };
    part.Children.Add(new MultiplyPart() { Text = "multiply"});

    doc.Parts.Add(part);

    doc.Accept(new MyVisitor());
}

oraz wersja bez Visitora:

public class Document
{
	public List<DocumentPart> Parts = new List<DocumentPart>();

	public void Process(DocProcessor docProc)
	{
		foreach (var child in Parts)
		{
			docProc.Process(child);
		}
	}
}

public abstract class DocumentPart
{
	public List<DocumentPart> Children { get; set; } = new List<DocumentPart>();
	public string Text { get; set; }
}

public class AddPart : DocumentPart
{
	public int Value { get; set; } = 15;
}

public class MultiplyPart : DocumentPart
{
	public int Value { get; set; } = 35;
}


public class DocProcessor
{
	public void Process(DocumentPart node)
	{
		// tutaj dzieje się cały hack/trick/magic
		// odnajdujemy metodę o nazwie 'Visit' która ma taki typ parametru, jak nasz obiekt, a zatem (AddPart | MultiplyPart)
		
		var type = node.GetType();
		var method = typeof(DocProcessor).GetMethods().Where(x => x.Name == nameof(Visit) && x.GetParameters()[0].ParameterType == type).First();
		method.Invoke(this, new[] { node });

		foreach (var child in node.Children)
			Process(child);
	}

	public void Visit(AddPart node)
	{
		Console.WriteLine($"AddPart {node.Text + node.Value}");
	}

	public void Visit(MultiplyPart node)
	{
		Console.WriteLine($"MultiplyPart {node.Text + node.Value}");
	}
}

oraz użycie:

public static void Main()
{
    var doc = new Document();

    var part = new AddPart() { Text = "add" };
    part.Children.Add(new MultiplyPart() { Text = "multiply" });

    doc.Parts.Add(part);

    doc.Process(new DocProcessor());
}

i wydaje mi się, że Visitor w tym przypadku dodaje jedynie dużo boilerplate typu te overridy, interfejs, wymusza aby nasze "obiekty domenowe" miały jakiś Accept oraz chyba zgubiłem gdzie właściwie powinienem dać te pętle od przechodzenia po Childach - Accept czy Visit (co i tak nie jest fajne, bo nadmiarowe), lub inaczej?

2

Visitor jest trasznie śmierdzącym patternem. Widziałem kilka razy w kodzie produkcyjnym, za każdym razie użyto w celu masturbowania się patternami - sensu nie było widać. (A kod powstaje taki, że chce się płakać)

W nowszych językach jest kilka konstrukcji, które zupełnie zmniejszają sens wizytora:

  • Exhaustive pattern matching
  • biblioteki i wsparcie (system typów) dla recursion schemes

To pierwsze powoduje, że wygodnie i bezpiecznie obsługuje mniej więcej przypadek taki jak podałeś.
To drugie umożliwia nam automatyczne traversowanie po własnych strukturach danych.

1

http://nice.sourceforge.net/visitor.html

Another possibility would be to define a static method in the new package. But then it would be necessary to test the argument with instanceof and use downcasts. In short, lose the benefits of object-orientation.

Jak widać, podstawową zaletą wizytatora jest to, że jest obiektowy :] Jeśli komuś bardzo zależy na OOPie to wizytator spełni jego oczekiwania.

http://nice.sourceforge.net/visitor.html

The Visitor pattern is a trick to introduce multiple dispatch in a language that lacks it. However, it raises serious issues. Language support for multi-methods makes it much easier and elegant to handle the common situation where the Visitor pattern applies.

Multimetody są dalej obiektowe i generalnie można nimi zastąpić wizytatora, w dodatku wygodniej, czytelniej i zwięźlej. C# ma multimetody (według https://en.wikipedia.org/wiki/Multiple_dispatch#C# ), ale coś nie do końca działają:

using System;
using System.Collections;
using System.Collections.Generic;

class Printer {
    public void print(object o) {
        Console.WriteLine("got object");
    }
    public void print(int x) {
        Console.WriteLine("got int");
    }
}

class ChildPrinter : Printer {
    public void print(string s) {
        Console.WriteLine("got string");
    }

}

class Program {
    static Printer makePrinter() {
        return new ChildPrinter();
    }
    static void Main(string[] args) {
        var printer = makePrinter();
        printer.print(5 as dynamic); // got int, ok
        printer.print("ala ma kota" as dynamic); // got object, not ok
        printer.print(new List<double>() as dynamic); // got object, ok
    }
}

Multimetody to takie trochę przeciążąnie na sterydach. Nie jestem fanem przeciążania, więc multimetod też pewnie byłbym fanem.

Pattern matching jest nieobiektowy, ale to nie znaczy, że gorszy. Jak dla mnie nawet lepszy :) Scala ma go od wieków, C# ma go od wersji 8: https://github.com/dotnet/roslyn/blob/master/docs/features/patterns.md#arithmetic-simplification Takie zagnieżdżone wzorce jak w tym przykładzie (ale oczywiście nie dotyczące upraszczania wyrażeń arytmetycznych tylko innych rzeczy) często stosuję w kodzie scalowym i się to zdecydowanie przydaje. Wizytator i multimetody raczej nie wyglądają mi na sensowne alternatywy dla zagnieżdżonych wzorców w pattern matchingu (ale może się mylę i jest jakiś trik, dzięki któremu można jedno drugim sensownie zaemulować). Pattern matching dla Javy jest w przygotowaniu https://cr.openjdk.java.net/~briangoetz/amber/pattern-match.html jako część https://openjdk.java.net/projects/amber/

0

@Wibowit:

Multimetody są dalej obiektowe i generalnie można nimi zastąpić wizytatora, w dodatku wygodniej, czytelniej i zwięźlej.
C# ma multimetody (według https://en.wikipedia.org/wiki/Multiple_dispatch#C# ), ale coś nie do końca działają:

no właśnie próbowałem coś z castami tam poczarować ale mi nie szło i udało mi się uzyskać zadowalający efekt tymi w/w 3 linijakmi

var type = node.GetType();
var method = typeof(DocProcessor).GetMethods().Where(x => x.Name == nameof(Visit) && x.GetParameters()[0].ParameterType == type).First();
method.Invoke(this, new[] { node });
1

Niby niezła próba, ale weź pod uwagę, że typy wcale nie muszą się zgadzać. Jeśli mam typy A (rodzic) > B > C > D (potomkowie), metody dla A, B i D oraz obiekt typu C to powinna się wywołać metoda dla B. Nawet to jest i tak za mało dla pełnej obsługi multimetod, bo te mogą mieć wiele parametrów, a nie tylko jeden (jak w wizytatorze).

0

Po co Visitor tego nie rozumiem.

https://refactoring.guru/design-patterns/visitor/java/example

Na czym pracuje?
Na Interfejs Shape i implementacje: Dot, Line

Kto pracuje?
Interfejs Visitor który ma zdefiniowane metody dla Dot, dla Line

OK. Działa

A co z SOLID?
*Open for extending, closed for modification
*
No to dodajemy do Dot, Line jeszcze Plane

Literę O z SOLID diabli wzięli bo teraz do interfejsu Visitor tzreba dołożyć visit(Plane plane)

Jakby dokładać Triangle, Rectangle, Circle, Arc itd. to interfejs Visitor spuchnie o nowe metody dla visit(Triangle triangle) itd

Drugi koniec kija, każda konkretna implementacja Visotr MUSI "zająć się" metodami zakontraktowanymi w interfejsie Visitor. Nawet gdy nie mają logicznie sensu.

Więc IMHO zamiast Visitor to Iterator powinien wejść w jego miejsce.

PS
Każda implementacja która ma być wizytowana musi implementować interfejs Visitable i metodę

accept(Visitor visotor){
visitor.visit(this);
}

Loose coupling diabli wzięli, bo po co wszystko co ma być wizytowane musi być oznaczone implements Visitable?
Iterator tego nie potrzebuje.

Sorry, mogą być nieścisłości, pisane z palca, bez IDE

Moja teoria? Visitor został z czasów kiedy Banda Czworga pisała Biblijne Design Patterns i nie ma nikogo, kto by powiedział sorry, szaty króla z kolekcji Visitor są już nie bardzo an czasie.

1
BraVolt napisał(a):

Literę O z SOLID diabli wzięli bo teraz do interfejsu Visitor tzreba dołożyć visit(Plane plane)

Jakby dokładać Triangle, Rectangle, Circle, Arc itd. to interfejs Visitor spuchnie o nowe metody dla visit(Triangle triangle) itd

Nie ma rozwiązania bez wad. Wizytator czy pattern matching działają dobrze, jeśli liczba podklas (wizytowanego typu) rzadko się zmienia, a znacznie częściej dodajemy nowe metody (w sensie zewnętrzne, takie za pośrednictwem tego wizytatora czy pattern matchingu). Z drugiej strony OOPowe dziedziczenie działa dobrze jeśli rzadko dodajemy nowe metody, a znacznie częściej dodajemy nowe podklasy.

Weźmy dla przykładu te typy Traingle, Rectangle, Circle, etc oraz metody typu area, perimeter, goodHashCode, printDebug, etc.

Jeżeli chcę dodać nowy kształt (który ma unikalne implementacje metod) to:

  • w OOPowym podejściu (dziedziczenie) wystarczy dodać nową klasę i gotowe
  • przy wizytatorze czy pattern matchingu trzeba poprawić wszystkie wizytatory i drabinki w pattern matchingach

Jeżeli chcę dodać nową metodę (która zachowuje się inaczej dla każdego podtypu) to:

  • w OOPowym podejściu muszę zmodyfikować całą hierarchię klas, czyli dać metodę abstrakcyjną w nadtypie i konkretne implementacje w podtypach
  • przy wizytatorze czy pattern matchingu wystarczy dodać nowego wizytatora bądź nową drabinkę pattern matchingu, bez ingerowania w istniejącą hierarchię klas

Z drugiej strony są jeszcze https://en.wikipedia.org/wiki/Type_class i tutaj sprawa jest ciekawsza, bo implementację type classy T dla typu X można umieścić obok tej type class T lub obok samego typu X. Nie rozwiązuje to wszystkich problemów, ale nierzadko się przydaje.

Ze wszystkich tutaj mechanizmów (czyli gdy chcemy mieć coś więcej niż zwykłe typowo javowe (względnie c++owe czy c#owe) dziedziczenie) w praktyce najlepiej wypada moim zdaniem pattern matching. Zwięzły, czytelny, rzadko prowadzi do nieczytelnych błędów kompilacji czy zaskakującego działania w czasie wykonania, a dodatkowo (czego nie ułatwiają pozostałe mechanizmy) można nim sprawdzać zagnieżdżone wzorce, co często jest przydatne.

0

@Wibowit:

Wizytator czy pattern matching działają dobrze, jeśli liczba podklas (wizytowanego typu) rzadko się zmienia, a znacznie częściej dodajemy nowe metody (w sensie zewnętrzne, takie za pośrednictwem tego wizytatora czy pattern matchingu).

Jakby stosować jak pierwotnie GoF, AFAIR do GUI, to pasuje tak jak Czwórka opisała. Klasy się nie zmieniają ale to, co z klasami (elementami interfejsu) robimy to tylko sprawa nowej implementacji następnego wizytatora. Z tej strony patrząc jest jak najbardziej OK.

PS
Gdybym miał "wizytować" to odczytałbym (zawsze?) jako "iterować". Iterator? Tak. Visitor? Nie przyszedłby mi na myśl.

1
BraVolt napisał(a):

PS
Gdybym miał "wizytować" to odczytałbym (zawsze?) jako "iterować". Iterator? Tak. Visitor? Nie przyszedłby mi na myśl.

Co ma wizytator do iteratora? Możesz wizytować jeden obiekt kilkoma wizytatorami. Możesz wizytować kilka obiektów jednym wizytatorem. Analogicznie możesz odpalić kilka metod na jednym obiekcie lub odpalić jedną metodę na kilku obiektach. Czy metody są iteratorami?

0

Można wchodzić w dyskusje jak: https://stackoverflow.com/questions/255214/when-should-i-use-the-visitor-design-pattern
albo stwierdzić, że da się obejść bez Visitor Pattern. Bo inny DP będzie odpowiedni.

PS
https://refactoring.guru/design-patterns/iterator
https://refactoring.guru/design-patterns/visitor
You can treat Visitor as a powerful version of the Command pattern. Its objects can execute operations over various objects of different classes. You can use Visitor to execute an operation over an entire Composite tree. You can use Visitor along with Iterator to traverse a complex data structure and execute some operation over its elements, even if they all have different classes.

I zrobi się dłuższa teoretyczna dyskusja

EDIT
The visitor pattern is a solution to a more general design problem:
https://softwareengineering.stackexchange.com/questions/333692/understanding-the-need-of-visitor-pattern

interface Animal {
  String name();
  String sound();
}

z zastosowaniem Visitor dostajemy "solution to a more general design problem" a Animal wyglądać ma tak

interface Animal {
  String name();
  String sound();
  <R> R acceptVisitor(AnimalVisitor<R> visitor);
}

Więc gdy Zwierzak ma nie tylko, jak to zwierzak, mieć imię Azor, Pysia, szczekać, miauczeć, ale jeszcze być świadomym swojej "wizytowalności" to IMHO przesadzono tu z "solution to a more general design problem"

1
BraVolt napisał(a):

You can use Visitor to execute an operation over an entire Composite tree. You can use Visitor along with Iterator to traverse a complex data structure and execute some operation over its elements, even if they all have different classes.

I zrobi się dłuższa teoretyczna dyskusja

Nie muszę mieć wizytatora, żeby przejść po złożonej strukturze danych. Nie muszę mieć wizytatora, żeby odpalić kilka metod na kolekcji kilku obiektów. Mogę to wszystko zrobić za pomocą zwykłych metod i typowego OOPowego dziedziczenia. Analogicznie zresztą do odwiedzania za pomocą wspomnianego wizytatora.

Przykładowo jeśli mam klasy:

interface Expression {
  Number eval();
}

class Sum implements Expression { ... }
class Product implements Expression { ... }
class Negation implements Expression { ... }
...

To mogę odpalić eval() na wielu obiektach jednocześnie:

var list = new ArrayList<Expression>(new Sum(...), new Product(...), new Negation(...))
var sum = new BigDecimal(0);
for (Expression expr : list) {
  sum = sum.add(expr.eval());
}

Wizytator jest po to, by wyciągnąć implementacje dodatkowych metod poza obecną hierarchię klas. Na przykład chcę dodać metodę simplify. Mogę dodać do istniejącej hierarchii Expression i podtypów, ale gdyby Expression obsługiwało wizytatora to mógłbym zaimplementować simplify całkowicie poza Expression. Iterowanie to zagadnienie ortogonalne.

0

Visitor parterem ma sens kiedy w kodzie musi następować downcast. Pozwala na eliminowanie masy instanceof. Jest to zwłaszcza przydatne kiedy mogą powstawać nowe implementację klasy bazowej. Ten pattern pozwala na walidację poprawności kodu w czasie kompilacji a nie w runtime

0

@xxx_xx_x:

Jest to zwłaszcza przydatne kiedy mogą powstawać nowe implementację klasy bazowej

Czego? Visitora?

Visitor parterem ma sens kiedy w kodzie musi następować downcast. Pozwala na eliminowanie masy instanceof.

No ale wychodzi na to, że zamiast if (node is MyClass myClass) { myClass.DoSomething(); } mamy masę innej obiektowej boilerplejtówki, więc cóż to za kompromis?

3
WeiXiao napisał(a):

No ale wychodzi na to, że zamiast if (node is MyClass myClass) { myClass.DoSomething(); } mamy masę innej obiektowej boilerplejtówki, więc cóż to za kompromis?

Mając N drabinek ifów łatwo czegoś zapomnieć, a przy wizytatorze kompilator cię pilnuje. To jest główna (jedyna sensowna?) zaleta wizytatora w sytuacji, gdy język nie ma pattern matchingu z exhaustiveness check.

1

@WeiXiao: no jest jedna różnica o której napisałem. Jak dodasz Nowa implementację to musisz pamiętać gdzie trzeba dodać kolejne instanceof. Visitor sam to znajdzie na etapie kompilacji. To jest jako główną zaleta.

0

@xxx_xx_x:

niby tak, no chyba że stosujesz TDD, to nie musisz :P

2
WeiXiao napisał(a):

@xxx_xx_x:

niby tak, no chyba że stosujesz TDD, to nie musisz :P

Spotkałem się z opinią, że jak stosujesz TDD to nie potrzebujesz nawet statycznego typowania. Jest w tym trochę prawdy, bo przecież można wytestować zgodność typów (taką kaczą zgodność przynajmniej). Tylko po co to robić, skoro można zlecić tę robotę kompilatorowi? Wszystko zależy od rachunku kosztów: ile kosztuje narzut w kodzie produkcyjnym czy testowym, ile kosztują błędy na produkcji (czasami 0, bo nie odpalamy kodu na środowisku produkcyjnym, tylko na jakimś izolowanym kontenerze czy VMce), ile kosztuje wzrost trudności w utrzymaniu kodu (jeżeli taki wystąpi, a to zależy od wybranego mechanizmu i języka), itp itd

4

Kontynuując wątek TDD (a w zasadzie w ogóle testów) vs Kompilator. Testy są trochę zwykle łatwiejsze do napisania (ws zaprojektowanie typów), ale mają jedną, wielką pięte achillesową:
refaktoring.
Ileś razy byłem zaskoczony jak po kilku !!! dniach walki z kompilatorem - przeorany kod Scali w końcu wstawał i działał bez zająknięcia.
Analogiczne operacje w JS - nieźle pokrytym testami kończyły się tym, że człowiek oduczał się refaktoringu - tony wywalonych testów, nie wiesz nawet gdzie zacząć... a jak już przejdą to okazuje się, że system i tak wywali się po dniu na produkcji - bo testów i tak było za mało.

8

Dla mnie kompilator jest zawsze mocniejszy od testów. Kompilatora nie olejesz, testy owszem możesz.

1
sealed class Product {
    class Account():  Product()
    class Loan() : Product()
}

fun main() {
    val products = listOf(Product.Account(), Product.Loan(), Product.Account())
    var cA = 0; 
    var cL = 0
    for(product in products) {
        when(product) {
            is Product.Account -> cA++
            is Product.Loan -> cL++
        }
    }
    println("Account : $cA, Loan : $cL")
}

jeśli w powyższym kodzie dodamy nowy product Investment, to kod zadziała tak jak poprzednio. Wszystko sie skompiluje i dopiero w runtime zobaczymy braki w implementacji, moze pojda crashe, a moze tylko dane beda błędnie prezentowane. Generalnie polegamy tylko na tym że ręcznie odnajdziemy wszystkie bloki z downcastem.

W przypadku visitora ten kod moze wygladać tak:

interface ProductVisitor {
    fun visit(account: Product.Account)
    fun visit(loan: Product.Loan)
}

sealed class Product {
    abstract fun accept(visitor: ProductVisitor)
    
    class Account() : Product() {
        override fun accept(visitor: ProductVisitor) {
            visitor.visit(this)
        }
    }
    
    class Loan() : Product() {
        override fun accept(visitor: ProductVisitor) {
            visitor.visit(this)
        }
    }
}

class ProductCounter : ProductVisitor {
    var cA = 0 
    var cL = 0
    
    override fun visit(account: Product.Account) { cA++ }
    override fun visit(loan: Product.Loan) { cL++ }
}

fun main() {
    val products = listOf(Product.Account(), Product.Loan(), Product.Account())
   	val counter = ProductCounter()
    products.forEach { it.accept(counter) }
   
    println("Account : ${counter.cA}, Loan : ${counter.cL}")
}

Na pewno pojawilo sie troche boilerplate codu ale teraz jeżeli dodamy "Investment" to :

  1. interface product wymusi dodanie metody "accept(visitor)" w obiekcie Investment. Czyli mamy:
 class Investment() : Product() {
        override fun accept(visitor: ProductVisitor) {
            visitor.visit(this)
        }
    }
  1. Po dodaniu metody accept jak powyżej, dalej nie bedzie sie dało tego skompilować ponieważ żadna metoda accept nie przyjmuje "Investment". Wiec teraz trzeba dodac te metode do ProductVisitor:
interface ProductVisitor {
    fun visit(account: Product.Account)
    fun visit(loan: Product.Loan)
    fun visit(investment: Product.Investment)
}
  1. Teraz każda implementacja ProductVisitor będzie wymagać poprawy więc trzeba poprawić kod ProductCounter:
class ProductCounter : ProductVisitor {
    var cA = 0 
    var cL = 0
    var cI = 0
    
    override fun visit(account: Product.Account) { cA++ }
    override fun visit(loan: Product.Loan) { cL++ }
    override fun visit(investment: Product.Investment) { cI++ }
}
  1. Po zmodyfikowaniu visitorow mozemy sprawdzić ich użycie i sprawdzić czy wymagana jest jakas modyfikacja w kodzie:
    println("Account : ${counter.cA}, Loan : ${counter.cL}, Investment : ${counter.cI}")
  1. Modyfikacja testow, które operowały na ProductVisitor

Na pewno ten pattern zabezpiecza nas przed błędami w implementacji nowych obiektow, kosztem generowania pewnej ilosci kodu. Na pewno warto go rozważyć gdy zwykle większość metod w visitorze będzie implementowana(czyli zwykle mielibyśmy dużą drabinkę ifów instanceof) . W tym przypadku ProductCounter będzie chciał obsługiwać każdą metodę visit + reagować na dodawanie nowych produktów. Z drugiej strony gdybysmy używali tylko ProductCounter i nie przewidywali innej potrzeby na rozpoznawanie "child class" to pattern moze byc overkillem.

0

Macie racje - trzeba się zabezpieczać

private static void EnsureImplemented()
{
    var node_types = Assembly
        .GetCallingAssembly()
        .DefinedTypes
        .Where(x => x.IsAssignableTo(typeof(DocumentPart)) && x.AsType() != typeof(DocumentPart))
        .ToList();

    var methods = typeof(DocProcessor).GetMethods().Where(x => x.Name == "Visit").ToList();

    if (node_types.Count != methods.Count)
        throw new NotImplementedException("Vist is not implemented for some type loool");
}

wrzucamy do w entry point

public static void Main()
{
    EnsureImplemented();
    var doc = new Document();

    var part = new AddPart() { Text = "add" };
    part.Children.Add(new MultiplyPart() { Text = "multiply" });

    doc.Parts.Add(part);

    doc.Process(new DocProcessor());
}

;)

0

@xxx_xx_x:
Jak zobaczyłem to:

sealed class Product {
class Account(): Product()
class Loan() : Product()
}
fun main() {
val products = listOf(Product.Account(), Product.Loan(), Product.Account())
var cA = 0;
var cL = 0
for(product in products) {
when(product) {
is Product.Account -> cA++
is Product.Loan -> cL++
}
}
println("Account : $cA, Loan : $cL")
}

> jeśli w powyższym kodzie dodamy nowy product Investment, to kod zadziała tak jak poprzednio.


To pomyślałem, że jakieś straszne herezje piszesz.
Ale potem spojrzałem drugi raz, to `kotlin`, ale jakby `nie kotlin`. pętla (`for` - wtf?), jakieś incrementacje, mutacje, var... co to jest? `kotlin` spod ciemnej gwiazdy?

Wystarczy przepisać na normalny:

sealed class Product {
class Account(): Product()
class Loan() : Product()
}

fun main() {
val products = listOf(Product.Account(), Product.Loan(), Product.Account())
data class Res(val cA:Int = 0 , val cL:Int = 0)
products.fold ( Res()) {acc, product ->
when(product) {
is Product.Account -> acc.copy(cA = acc.cA+1)
is Product.Loan ->acc.copy(cL = acc.cL+1)
}
}.also {result ->
println("Account : ${result.cA}, Loan : ${result.cL}")
}
}

I teraz dodaj sobie jakiś Product.

Imo dużo mniej męczenia niż babranie się z Visitorem na kilkanaście linijek. Kompilator jednak działa, tylko nie warto mu przeszkadzać.


*Wkazówka dla Siebie - muszę obczaić czy moje standardowe narzędzia łapią taki kod jak pierwszy i wykrywają, że coś nie halo (np. pętla for)).
Jeśli nie to trzeba będzie obczaić jakiegoś wart removera*

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