wtorek, 11 sierpnia 2015

Soczewki,Obiektywy,Lenses

Ten artykuł zaczyna się serio oczywistych stwierdzeń o geterach i seterach i o tym, ze pojawiła się masa artykułów odnośnie tego, że są złe i też pojawiła się seria artykułów o tym, ze są dobre i ze jedni i drudzy się hejtowali.

A skoro mamy to za sobą to z getterami było tak:

Z enkapsulacją jakoś tak:

Czy brak/obecność getterów i setterów to pełne spektrum możliwości? Otóż co ciekawe nie. Poniżej zupełnie inna technika, która wychodzi poza pudełko obiektowych dysput. Idea jest taka by każdy sobie obejrzał i sam zdecydował czy to ma dla niego sens...

Soczewki

Zaczynamy od czegoś co obija się o profanacje czyli damy pola bez modyfikatora private :

public class User {
    String name;
    String email;
}

Ale co ciekawe nie ma tez modyfikatora public. Ciekawe jak to jest z edukacją wśród młodzieży i czy to część wiedzy powszechnej, że brak modyfikatora w javie to równeiż modyfiaktor. pakietowy.

I na przykład z byle jakiego miejsca się tutaj nie dostanę.

public class OuterClass {
    public static void main(String args[]) {
        User user=new User();
        //user.name="Stefan"; //Oj nie nie nie , Co to to nie!
    }
}

Osiągnęliśmy więc pewien poziom enkapsulacji - może nie obiektowej ale "paczkowej". Ale jak ustawić to pole bez gettera jeśli go nie ma? Otóż on jest ale gdzieś indziej.

Out of the box

No własnie gdzie może być ten getter. Na pewnie w tej samej paczce ale w zupełnie innej klasie :

Taka sytuacja

public class UserModule {
    public static Function<User,String> getName=u->u.name;
    public static BiConsumer<User,String> setName= (u,n)->u.name=n;

    //ciekawiej
    public static Function<User,Consumer<String>> setNameCurrying=u->n->u.name=n;

}

Powyższe funkcje są na razie jedynym sposobem dostępu do naszego obiektu :
import static blog.UserModule.*;
public class OuterClass {
    public static void main(String args[]) {
        User user=new User();

        setName.accept(user,"Stefan");
        System.out.println(getName.apply(user));

        setNameCurrying.apply(user).accept("Zofia");
        System.out.println(getName.apply(user));
    }
}

No i fajnie ale poza aspektem naukowo-artystycznym - oddzielenie funkcji od obiektu ma jeden ciekawy aspekt praktyczny - jedno i drugie można niezależnie komponować.

Trochę inna kompozycja

Weźmy coś takiego - to jest taki niby agregat ale aby nikt mnie nie łapał za słówka nie będę tego nazywał agregatem:

public class User {
    String name;
    Email email;
}

class Email{
    String address;
}

O ile jeszcze pamiętam Jave to klasa Email tez jest w zakresie pakietowym i nie widać jej na zewnątrz.

I dalej

//package setters
static Function<User,Consumer<Email>> setEmail=user->newEmail->user.email=newEmail;
static Function<Email,Consumer<String>> setAddress=email->newAddress->email.address=newAddress;
    //package getters
static Function<User,Email> getEmail=u->u.email;
static Function<Email,String> getAddress=e->e.address;

I teraz, i teraz to się stanie, teraz to będzie...teraz to będzie... ściana
public class Kombinatoryka {
    public static Function<User,Consumer<String>> composeSet(Function<User,Consumer<Email>> f1,Function<Email,Consumer<String>> f2){
        //return  ... I co teraz? ściana? To jest przykład na to, że wszelkie wywołanai bez typu zwracanego, czyli wszelkie konsumery
        //komponują się chu***owo
        //mozna niby tak ale to jest bez sensu bo możemy zapomnieć o wszelkiej generalizacji biblioteki
        return u->{
            Email email=UserModule.getEmail.apply(u);
            return f2.apply(email);
        };
    }
}

Podejście numer dwa bez konsumerów

Tym razem zwracamy wynik naszego działania :
static BiFunction<User,Email,User> setEmail=(user,newEmail)->{user.email=newEmail; return user;};
static BiFunction<Email,String,Email> setAddress=(email,newAddress)->{email.address=newAddress;return email;};

Czy tym raazem się uda? Nie:)
public class Kombinatoryka {
    public static BiFunction<User,String,User> composeSet(BiFunction<User,Email,User> outer,BiFunction<Email,String,Email> inner){
        (user,newAddress)->outer.apply(u,inner.apply(//i znowu ściana, jak wyłuskac z usera email?))
    }

Aby myśleć o kompozycji musimy dorzucić przynajmniej jeden getter.
public class Kombinatoryka {
    public static BiFunction<User,String,User> composeSet(BiFunction<User,Email,User> outer,
                                                            BiFunction<Email,String,Email> inner,
                                                            Function<User,Email> outerGet){
        return (user,newAddress)->outer.apply(user,inner.apply(outerGet.apply(user),newAddress));
    }
}

Tak jest to trochę zamotane. Co możemy zrobić?Najpierw tak

interface GenericLens<O,V>{
        BiFunction<O,V,O> setter();
        Function<O,V> getter();
    }

Później tak
static class UserLens implements GenericLens<User,Email>{
        @Override
        public BiFunction<User, Email, User> setter() {
            return (user,newEmail)->{user.email=newEmail; return user;};
        }

        @Override
        public Function<User, Email> getter() {
            return u->u.email;
        }
    }

    static class EmailLens implements GenericLens<Email,String>{

        @Override
        public BiFunction<Email, String, Email> setter() {
            return (email,newAddress)->{email.address=newAddress;return email;};
        }

        @Override
        public Function<Email, String> getter() {
            return e->e.address;
        }
    }

Następnie tak :
public static <Outer,Inner,Value> GenericLens<Outer,Value> composeLens(
            GenericLens<Outer,Inner> outerLens,
            GenericLens<Inner,Value> innerLens
    ){
            return new GenericLens<Outer, Value>() {
                @Override
                public BiFunction<Outer, Value, Outer> setter() {
                    return (outer,value)->{
                        Inner inner = outerLens.getter().apply(outer);
                        Inner newInner = innerLens.setter().apply(inner, value);
                        return outerLens.setter().apply(outer,newInner);
                    };
                }

                @Override
                public Function<Outer, Value> getter() {
                    return outer->{
                        Function<Outer, Inner> outerGetter = outerLens.getter();
                        Function<Inner, Value> innerGetter = innerLens.getter();
                        Inner inner = outerGetter.apply(outer);
                        return innerGetter.apply(inner);
                    };
                }
            };
    }

I na końcu tak :
static GenericLens<User,String> userEmailLens=Kombinatoryka.composeLens(new UserLens(),new EmailLens());

Co uzyskaliśmy

Teoretycznie mamy enkapsulację obiektów i znowu teoretycznie generyczny mechanizm do kompozycji działań na tych obiektach. Ale wygląda to przerażająco. Niby można sobie założyć, że część z tego kodu byłaby zamknięta przed oczami świata gdzieś w bibliotece...

Czy są inne opcje?

Inne Opcje

Generlanie w Javie nie znalazłem, żadnej biblioteki, która implementuje opisany mechanizm. W scali do dyspozycji jest całkiem ciekawa biblioteczka https://github.com/julien-truffaut/Monocle

libraryDependencies ++= Seq(
 "com.github.julien-truffaut" %% "monocle-generic" % "1.1.1",
 "com.github.julien-truffaut" %%  "monocle-generic" % "1.1.1",
 "com.github.julien-truffaut" %%  "monocle-macro"   % "1.1.1"
)
import monocle.macros.GenLens
case class Email(address:String)
case class User(name:String,email:Email)

val userEmailLens=GenLens[User](_.email)
val emailAddressLens=GenLens[Email](_.address)

val u=User("Stefan",Email("stefan@wp.pl"))

val userAddressLens=userEmailLens composeLens emailAddressLens
val nowyUser=userAddressLens.set("stefan@gmail.com")(u)
nowyUser // User(Stefan,Email(stefan@gmail.com))

I to w zasadzie to tyle kodu potrzeba...

3 komentarze:

  1. Polecam quicklens Adama W. Małe, sprytne, nieskomplikowane

    OdpowiedzUsuń
  2. No proszę, "cudze chwalicie swego nie znacie" , dzięki :D

    OdpowiedzUsuń
  3. DCI? https://www.google.pl/webhp?sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8#q=data%20context%20interactions

    OdpowiedzUsuń