Proponując coś sprzecznego z obiegową opinią - bez względu czy opiera się ona na faktach czy na folklorze ludowym - narażamy się na zmasowany ostrzał z trololo-katiuszy.
Na przykład dzieląc się w niektórych wsiach opinią, że niektórzy Niemcy to tak naprawdę mili i uczynni ludzie , zapewne zostaniemy odprowadzeni na widłach do najbliższego dworca PKP (zwłaszcza 11 listopada).
Podobnej klasy odpowiedzi mogą nas spotkać na niektórych forach gdy wspomnimy, że są obok rozwiązań obiektowych są inne - nieobiektowe - które też mogą być dobre.
Najpierw zachęta. W Javie 7 chcemy posortować listę Stringów w odwrotnej kolejności. Użyjemy obiektu typu Comparator :
List<String> elements=Arrays.asList("1","2","3");
Collections.sort(elements,new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o2.compareTo(o1);
}
});
for (String element : elements) {
System.out.println(element);
}
W Javie 8 uzyska się to samo przy pomocy czegoś mniej obiektowego. Po ogarnięciu nowej składni powinno być znacznie czytelniej :
List<String> elements=Arrays.asList("1","2","3");
Collections.sort(elements, (s1, s2) -> s2.compareTo(s1));
elements.forEach( word-> { System.out.println(word); } );
Albo takie coś, filtrowanie i konwersja Stringów do Intów w Javie 7
List<String> elements=Arrays.asList("1","22","11","4444","3","33");
List<Integer> filteredList=new ArrayList<>();
for (String element : elements) {
if(element.length()==2){
filteredList.add(Integer.parseInt(element));
}
}
for (Integer integer : filteredList) {
System.out.println(integer);
}
I to samo w Javie 8
List<String> elements=Arrays.asList("1","22","11","4444","3","33");
List<Integer> result=elements.stream().filter(e->e.length()==2).map(e->Integer.parseInt(e));
result.forEach( word-> { System.out.println(word); } );
Paradygmaty
Do niedawno żyłem w świadomości, że są tylko dwie szkoły programowania. Pierwsza to ta lśniąca będąca synonimem jakości czyli programowanie obiektowe. Druga uprawiana przez mniej zdolnych ludzi, którzy nie rozumieją jak się programuje obiektowo - programowanie proceduralne. Okazuje się, że jednak nie bo oto jeden z wielu podziałów na paradygmaty wygląda tak :
Otóż poza programowaniem obiektowym i proceduralnym, które są zgrupowane pod kategorią Imperatywne - jest jeszcze jedna kategoria Deklaratywne, w której mamy podkategorię zwaną Programowaniem Funkcyjnym.
Powyższy diagram był dla mnie nie lada zaskoczeniem ( jak to mawiają amerykanie w pornolach "I didn't see it coming" ). Przez dłuższy czas myślałem, że proceduralne,strukturalne czy funkcyjne to jeden ch.. i generalnie ostatni wysyp książek na temat Programowania Funkcyjnego jest spowodowany tym , że rynek lektur obiektowych już się nasycił. I znowu byłem w błędzie. Programowanie Funkcyjne to zupełnie inna para kaloszy i w zasadzie powstało jeszcze przed komputerami zbudowanymi z lamp elektronowych.
Jak to się dzieje, że OOP jest tak popularne? Moim zdaniem aby to zrozumieć należy uzupełnić rysunek o najpopularniejszy obecnie paradygmat w polskich biedronkach informatycznych (a przynajmniej łódzkich)
Otóż wiele osób tonąc w otchłani syfu generowanego przez nierealne deadlajny i pseudo-metodyki projektowe w geście rozpaczy wyciąga rękę po ratunek. I teraz następuje coś takiego jak robienie kroku stojąc pośrodku bieguna północnego
czyli gdziekolwiek nie pójdziemy to idziemy na południe. Jeśli dana osoba trafi na materiały o poprawnym podejściu obiektowym to rzecz jasna poprawi jakość kodu technikami obiektowymi (i jest na dobrej drodze aby stać się fanatykiem obiektowym). Jeśli ktoś dla odmiany zacznie stosować poprawnie podejście funkcyjne to również poprawi jakość projektu (i jest na dobrej drodze aby stać się fanatykiem funkcyjnym). Tak czy inaczej idziemy na południe. A, że przynajmniej wśród programistów Javy programowanie obiektowe jest najpopularniejsze to uzyskujemy efekt dodatniego sprzężenia zwrotnego : "Coraz więcej osób używa windowsa gdyż windows jest najpopularniejszy"
Póki kod jest nieczytelny to nie ma znaczenia, którego podejścia użyjemy ponieważ kod o wielkości 10000 linii jest zwyczajnie zjebany bez względu an to czy jest w klasie, funkcji czy procedurze. Dopiero jak to się ogarnie można zastanowić się, które podejście ma jakie zalety.
Jakkolwiek warto tu wspomnieć o jednej ciekawej anomalii w kontekście teorii "Wszystko jest obiektem" - najpopularniejsza i najbardziej przenośna biblioteka Javy wszech czasów nazywa się StringUtils
Programowanie nieobiektowe
Nie chcę nazywać tego paragrafu Programowaniem funkcyjnym gdyż cały czas nie jestem pewny czy moje rozumienie tego tematu jest poprawne. Tak czy inaczej to co mnie uderzyło w trakcie nauki PF to ilość zaufania jakim trzeba obdarzyć autora danego źródła zanim pokaże on jakieś zastosowanie, które mogę użyć w swojej codziennej pracy. Zazwyczaj trzeba cierpliwie odczekać trochę teorii matematycznych oraz przebrnąć przez masę epickich przykładów z szukaniem liczb nieparzystych w zbiorze.
To co spróbuje zrobić poniżej to wyjście od jakiejś tam konstrukcji obiektowej i zakończenie na czymś co ma w zamierzeniach być rozwiązaniem funkcyjnym.
Wizytator
To tak, na początek mamy jakiś obiekt przechowujący kolekcję innych obiektów. Chcemy bez ujawniania jak ta kolekcja jest zaimplementowana dać możliwość operowania na niej w sposób wszelaki.
Z tego co pamiętam takie coś robi się przy pomocy wzorca wizytator
public class Company {
private List<? extends User> users;
public List<String> visitAll(Visitor visitor){
List<String> resultList=new LinkedList<>();
for (User user : users) {
String result=visitor.apply(user);
resultList.add(result);
}
return resultList;
}
}
public interface Visitor {
String apply(User user);
}
public class CapitalizingFirstNameVisitor implements Visitor{
@Override
public String apply(User user) {
return capitalize(user.getFirstName());
}
private String capitalize(String firstName) {
return Character.toUpperCase(firstName.charAt(0)) +firstName.substring(1);
}
}
Firma ma użytkowników i bez zdradzania tego "w jakiej formie ich ma " pozwala wizytatorowi na manipulacje. W przykładzie powyżej wizytator zwróci listę imion wszystkich użytkowników z Pierwszą literą WIELKĄ (tak jakby ktoś zapomniał przy rejestracji.) Testowanie tego je proste bo to nie ma żadnego stanu ani zależności czyli nic mokować nie trza
@Test
public void shouldCapitalizeFirstName() {
//given
User user=new StandardUser("firstName", "lastName");
//when
String result = new CapitalizingFirstNameVisitor().apply(user);
//then
assertThat(result).isEqualTo("FirstName");
}
Pojawia się powtórzenie w kodzie
public class Category {
private List<Product> products;
public List<String> visitAll(Visitor visitor){
List<String> resultList=new LinkedList<>();
for (Product product: products) {
String result=visitor.apply(product);
resultList.add(result);
}
return resultList;
}
}
Struktura jest podobna ale deklaracja typów w metodzie wizytatora będzie inna. Na początek postarajmy się użyć wspólnego interfejsu
public interface Visitor<A,B> {
B apply(A input);
}
public class CapitalizingFirstNameVisitor implements Visitor<User,String>{
@Override
public String apply(User user) {
return capitalize(user.getFirstName());
}
private String capitalize(String firstName) {
return Character.toUpperCase(firstName.charAt(0)) +firstName.substring(1);
}
}
I mamy już jakąś reużywalność (jeśli takie słowo w ogóle jest w języku polskim)
public class Category {
private List<Product> products;
public List<String> visitAll(Visitor<Product, String> visitor){...}
}
public class Company {
private List<? extends User> users;
public List<String> visitAll(Visitor<User, String> visitor){...}
}
To tyle jeśli chodzi o interfejs ale cały czas jest problem z implementacją. Jeśli np. chcemy zamienić pierwszą literę nazwy produktu na WIELKĄ to kawałek kodu nadal się będzie powtarzać :
public class CapitalizingProductNameVisitor implements Visitor{
@Override
public String apply(Product input) {
return capitalize(input.getName());
}
private String capitalize(String firstName) {
return Character.toUpperCase(firstName.charAt(0)) +firstName.substring(1);
}
}
Jeśli ktoś stracił wątek to powtarza się ta prywatna metoda zwiększająca pierwszą literę wyrazu (implementacje z netu skopiowałem)
Repeatability
I co teraz można zrobić? Popularne kroki to np. napisanie StringUtils - a jak. Inni mogą kombinować z wraperam RichString (bardziej obiektowo) albo bezstanowym FirstLetterKapitalizer (też bardziej obiektowo).
Ale pomyślmy teraz o bardzo ciekawym przypadku. Co jeśli mam teraz taką klaskę, która enkapsuluje kolekcję tytułów książek i chcę mieć wizytatora, który zwiększy pierwszą literę każdego tytułu.
public class CapitalizingFirstLetterVisitor implements Visitor<String,String>{
@Override
public String apply(String input) {
return Character.toUpperCase(input.charAt(0))+input.substring(1);
}
}
Czyli nie żadne Utilsy czy dziwne wrappery ale kolejny Wizytator. Jak teraz zrobić aby użyć tej funkcjonalności w pozostałych wizytatorach. Czas pomyślenia nad jakimś zunifikowanym sposobem kompozycji wizytatorów aby można było zrobić coś takiego :
public interface Visitor<A,B> {
B apply(A input);
<C> Visitor<A,C> andThen(Visitor<B,C> secondVisitor);
}
Zaczyna się robić coraz ciekawiej. Mamy wizytator który przekształca typ A na B , V1:A=>B i drugi , który zamienia B na C , V2:B=>C . Ponieważ pierwszy zwraca to co drugi przyjmuje na wejściu toteż można napisać coś takiego V3 = V1.andThen(V2) czyli V3 : A=>C.
Teraz mamy :
- V1 - zwraca imię użytkownika User=>String
- V2 - Zwiększa pierwszą litere wyrazu String=>String
- V3 - Zwraca imię użytkownika z zamianą pierwszej litery na wielką User=>String
Podobnie można zrobić dla produktu z "reużyciem" V2 (nie mylić z niemiecką rakietą balistyczną).
Przypomina to znane wywołanie z powłoki popularnego systemu ps -ax | grep java , które jest możliwe gdyż pierwsza instrukcja nie jest w żaden sposób powiązana z kolejną (poza typem).
Czyli mamy :
Atomowe operacje (high cohesion?)
public class ProductModule {
public static final Visitor<String, String> capitalizeFirstLetter=new GeneralVisitor<String, String>() {
@Override
public String apply(String input) {
return Character.toUpperCase(input.charAt(0))+input.substring(1);
}
};
public static final Visitor<Product, String> retrieveProductName=new GeneralVisitor<Product, String>() {
@Override
public String apply(Product input) {
return input.getName();
}
};
....
}
Linia produkcyjna (low coupling?)
public static final Visitor<Product, String> capitalizeProductName=retrieveProductName.andThen(capitalizeFirstLetter);
Jak to zaimplementować ?
public abstract class GeneralVisitor<A,B> implements Visitor<A, B> {
@Override
public <C> Visitor<A, C> andThen(final Visitor<B, C> secondVisitor) {
return new GeneralVisitor<A, C>() {
@Override
public C apply(A input) {
B firstResult = GeneralVisitor.this.apply(input);
return secondVisitor.apply(firstResult);
}
};
}
}
Po pierwsze to nie ma coś się bać tej konstrukcji gdyż jest to konstrukcja biblioteczna. Podobnie wiele osób skutecznie używa Listy w javię bez zbędnego interesowania się jak to tam jest zaimplementowane.
A druga kwestia to GeneralVisitor.this.apply(input); jest sposobem na wywołanie this na klasie zewnętrznej.
V1 andThen V2 działa tak, ze najpierw jest wywołane V1 a potem V2. Można też w drugą stronę - V1.compose(V2) - najpierw V2 potem V1.
@Override
public <C> Visitor<C, B> compose(final Visitor<C, A> firstVisitor){
return new GeneralVisitor<C, B>() {
@Override
public B apply(C input) {
A firstResult = firstVisitor.apply(input);
return GeneralVisitor.this.apply(firstResult);
}
};
}
Grunt aby typy się zgadzały.
public class TextModule {
public static final Visitor capitalizeFirstLetter=new GeneralVisitor() {
@Override
public String apply(String input) {
return Character.toUpperCase(input.charAt(0))+input.substring(1);
}
};
public static final Visitor addPrefix=new GeneralVisitor() {
@Override
public String apply(String input) {
return "some prefix "+input;
}
};
out.println(capitalizeFirstLetter.andThen(addPrefix).apply("input text"));
//some prefix Input tekst
out.println(capitalizeFirstLetter.compose(addPrefix).apply("input text"));
//Some prefix input tekst
Wstrzykiwanie parametrów
Co jeśli wezmę Wizytatora "addPrefix" z poprzedniego przykładu i będę chciał dodać możliwość ustawiania prefixu?
@Override
public static final TwoArgumentVisitor<String, String, String> addPrefix=new TwoArgumentVisitor<String,String,String>() {
@Override
public String apply(String prefix, String text) {
return prefix+text;
}
};
public static void main(String[] args) {
out.println(addPrefix.apply("https://","www.wp.pl"));
// https://www.wp.pl
}
Idziemy dalej. Chcę mieć możliwość ustawienia jednego parametru ale tylko raz. Czyli coś jak konfiguracja. Wtedy jeden moduł może konfigurować protokół a inny użyje prekonfigurowanych wizytatorów do budowy URLa
@Override
Visitor<String, String> secureUrl=addPrefix.partiallyApply("https://");
Visitor<String, String> unsecureUrl=addPrefix.partiallyApply("http://");
Visitor<String, String> filetransferUrl=addPrefix.partiallyApply("ftp://");
public static void main(String[] args) {
out.println(secureUrl.apply("www.wp.pl"));
// -> https://www.wp.pl
out.println(unsecureUrl.apply("www.wp.pl"));
// -> http://www.wp.pl
out.println(filetransferUrl.apply("www.wp.pl"));
// -> ftp://www.wp.pl
}
I kodzik dla funkcji bibliotecznej :
public abstract class TwoArgumentVisitor<A,B,C> {
public abstract C apply(A firstArgument,B secondArgument);
public Function<B, C> partiallyApply(final A fixedArgument) {
return new GeneralFunction<B, C>() {
@Override
public C apply(B variableArgument) {
return TwoArgumentVisitor.this.apply(fixedArgument, variableArgument);
}
};
}
}
Budowanie z cegiełek
Mając wspomniane wizytatory, możemy łatwo budować coraz to bardziej skomplikowaną funkcjonalność
Visitor<String, String> navigateUserToPurchase=new GeneralVisitor<String, String>() {
@Override
public String apply(String userName) {
return "domain/purchase?name="+userName;
}
};
public static final Visitor<User, String> secureUserNavigation
=getUserFirstName
.andThen(navigateUserToPurchase)
.andThen(secureUrl);
out.println(secureUserNavigation.apply(new StandardUser("Czesiek","Kowalski")));
// -> https://domain/purchase?name=Czesiek
Prawdziwe nazwy
Jakby ktoś nie zgadł to Visitor => Function gdzie Function:A=>B
f andThen g → g(f(x)) ; f compose g → f(g(x))
public interface Function<A,B> {
B apply(A input);
<C> Function<A,C> andThen(Function<B,C> secondVisitor);
<C> Function<C, B> compose(Function<C, A> secondVisitor);
}
Jeszcze jedno powtórzenie
List<? extends User> users=Arrays.asList(
new StandardUser("roman","Cichocki"),
new StandardUser("stefan","Zabłocki"));
Company company=new Company(users);
List<String> names = company.visitAll(new CapitalizingFirstNameVisitor());
for (String name : names) {
System.out.println(name);
}
List<Product> products=Arrays.asList(
new Product("dietanabol"),
new Product("hyper protein 120%"));
Category category=new Category(products);
List<String> names = category.visitAll(new CapitalizingProductNameVisitor());
for (String name : names) {
System.out.println(name);
}
Co zastępujemy zwykłym :
public class FunctionalList<A> {
private List<A> elements;
public <B> List<B> map(Function<A, B> function){
List<B> resultList=new LinkedList<>();
for (A element : elements) {
resultList.add(function.apply(element));
}
return resultList;
}
public void foreach(Function<A> function){
for (A element : elements) {
function.apply(element);
}
}
}
Co nam daje Java 8
Można wywalić masę boilerplaejtu
FunctionalList products=
new FunctionalList<>(Arrays.asList(
new Product("Rocket Fuel"),
new Product("Amino Juice"))
);
products.foreach(new VoidFunction() {
@Override
public void apply(Product input) {
System.out.println(input.getName());
}
}
);
I dostajemy
FunctionalList products=
new FunctionalList<>(Arrays.asList(
new Product("Rocket Fuel"),
new Product("Amino Juice"))
);
products.foreach(
input → out.println(input.getName())
);
Ciekawsze zastosowanie - Loan Pattern (Scala)
def withPrintWriter(file: File,
op: PrintWriter => Unit) {
val writer = new PrintWriter(file)
try {
op(writer)
} finally {
writer.close()
}
}
withPrintWriter(
new File("date.txt"),
writer => writer.println(new java.util.Date))
withPrintWriter - funkcja biblioteczna i technicznie można pisać bezpieczny kod bez try/catch/finally (jawnie)
Enkapsulacja
Ponieważ mamy tutaj do czynienia z programowaniem deklaratywnym ("tu masz przepis i zrób jak ci wygodnie") to jest dużo miejsca na optymalizacje
public class ParallelFunctionalList<A> {
private List<A> elements;
public <B> List<B> map(Function<A, B> function){
List<B> resultList=new LinkedList<>();
for (A element : elements) {
resultList.add(function.apply(element));
}
return resultList;
}
public void foreach(Function<A> function){
//elements.split
//use Fork-join on elements
}
Zakońćzenie
- Czy używam tego w praktyce? - Tak, scala + FunctionalJava dla Javy 7
- Kto jest największym hejterem OOP? - Chyba ten pan : http://pl.wikipedia.org/wiki/Rich_Hickey
- Kto jest największym hejterem FP? - Wydaje mi się, ze wszelkiej maści analityce od UMLa
- Czy można używać OOP + FP? - Moim zdaniem tak i dlatego polecam Scalę
- Czy muzyka w filmie na początku jest tam umieszczona legalnie? - Według Youtuba nie i dlatego zablokował dźwięk dla odbiorców z Niemiec
- Dlaczego sam sobie odpowiadasz na pytania? - To zapewne początki Schizofrenii
- Myślisz, ze ktoś doczyta to do końca? - Nichuja