Objekt-orienterat vs funktionellt Objekt-orienterad programmering och design (DIT952) Niklas Broberg, 2017 Quiz • Vad skiljer ”objekt-orienterade språk” från ”funktionella språk”? • Funktionella språk har ”first-class functions”, dvs funktioner som värden. • Funktionella språk har algebraiska datatyper (ADTs) • Objekt-orienterade språk har klasser och objekt med metoder och attribut. • Objekt-orienterade språk har arv. • Ett språk kan vara både objekt-orienterat och funktionellt. • E.g. Scala. Funktioner • En funktion (i programmeringstermer) är en operation som tar ett eller flera argument, och ger ett resultat. • Quiz: Vad skiljer en funktion från en metod? • En statisk metod i Java är en funktion. Vi kallar den bara något annat. • En instansmetod i Java är också en funktion, med särskild syntax, där instansen på vilken metoden anropas är ett implicit argument till funktionen. • Ibland används distinktionen att en funktion ska vara deterministisk, eller till och med pure, men oftast inte. Funktionella språk • Ett programspråk sägs vara funktionellt om det tillåter oss att använda funktioner som om de vore värden (”first-class functions”): • Deklarera variabler som representerar funktioner: f = show • Applicera funktionsvariabler på argument:. x = f 5 • Skicka funktioner som argument till andra funktioner (metoder). map f [1,2,3,4,5] • Skapa anonyma funktioner som värden: map (\x -> x+5) [1,2,3,4,5] Exemplen givna med Haskell-syntax Lambda expression • Termen lambda expression (”lambda-uttryck”) beskriver just en funktion som används anonymt. • Ursprunget är Alonzo Church’s Lambda calculus. (\x -> x+5) = λx.x+5 • ”Ta ett argument, som vi kallar x, och gör så här…” Lambdas och Java • I senaste versionen av Java (Java 8, 2014) har vi fått stöd för lambda expressions i Java. Vad vi kan göra med dessa är dock lite begränsat, jämfört med ett ”riktigt” funktionellt språk. • ”Problemet” ligger i hur vi kan deklarera en funktions-typ. Java är ett statiskt typat språk, så allt måste ges en typ. • I språk som (e.g.) Haskell finns enkel syntax för funktionstyper: f :: Int -> Int • Java har historiskt inget sätt att skriva ut funktionstyper, och de som designade Java 8 ville inte göra för stora avsteg från Java’s kärna. Funktioner som objekt • Det klassiska sättet, i Java innan version 8, att representera (något som liknar) funktioner var att gömma dem inuti ett objekt: public interface MyFunctionType { public int apply(int x); } public class MyAddFive implements MyFunctionType { public int apply(int x) { return x+5; } }; MyFunctionType f = new MyAddFive(); int y = f.apply(5); int[] xs = { 1,2,3,4,5 }; int[] ys = new int[xs.length]; for (int i = 0; i < xs.length; i++) { ys[i] = f.apply(xs[i]); } Anonymous classes • Ett lite smidigare sätt, som fungerade även innan Java 8, var att skapa instanser av så kallade anonyma klasser. public interface MyFunctionType { public int apply(int x); } MyFunctionType f = new MyFunctionType() { public int apply(int x) { return x+5; } }; int y = f.apply(5); … Vi definierar klassen samtidigt som vi skapar instansen. Klassen har inget namn, därav ”anonymous”. Allt vi vet om den är att den implementerar MyFunctionType. Lambdas • Med hjälp av lambdas blir koden ännu lättare: public interface MyFunctionType { public int apply(int x); } MyFunctionType f = x -> x+5; int y = f.apply(5); … Vi definierar den anonyma klassen med hjälp av förenklad lambda-syntax. Functional Interface • Ett functional interface i Java är ett interface som deklarerar exakt en metod: Annotationen @FunctionalInterface public interface MyFunction { public int apply(int x); } @FunctionalInterface är bara en hjälp – ger felmeddelande om interfacet inte uppfyller kriteriet. • Vi kan använda lambda expressions som short-hand för objekt som implementerar ett sådant functional interface, e.g. MyFunction f = x -> x+5; int y = f.apply(5); Lambda syntax • Ett lambda expression i Java har följande syntax: • En lista av parametrar, e.g. (x,y,z) • Om vi bara har en parameter kan vi utelämna parenteserna. • Om vi inte tar något argument alls skriver vi () • (Lustigt nog har vi inget lambda först.) • En pil: -> • En method body: { return x+5; } • Om vår body består av bara ett expression vars resultat ska returneras behöver vi varken {} eller return, vi skriver bara uttrycket: x+5 (x,y) -> x+y; x -> x+5; s -> { System.out.println(s); }; () -> 42; Exempel på giltiga lambda expressions i Java. Live code • Macro.addTransform Command Pattern Encapsulate the information needed to perform an action at a later time. Invoke the action from external clients without depending on the object the action belongs to. • Lägg information som behövs för att utföra ett visst anrop i ett objekt, som kan användas av klienter utan att de behöver veta vad anropet gör internt. • Syftet med Command Pattern kan dels vara att undvika beroenden mellan klienter och utförare; dels att få ”makron” som kan sekvensieras, skickas mellan objekt, aggregeras, ibland även ges extra funktionalitet (e.g. räkna hur många gånger anropet utförts). Command Pattern Command <<Interface>> + execute(…) : … Invoker Beroendena från Client kan skilja lite mellan olika varianter och användningar. - commands : List<Command> + addCommand(Command) : void invoke(…) : … Client … ConcreteCommand - receiver : Receiver - state : … Receiver + action(…) : … + execute(…) : … public void execute() { receiver.action(…); … } Argument- och retur-typer kan skilja mellan olika användningar; jag har valt enklast möjliga i exemplet. Command Pattern för Macro Transform <<Interface>> + transform(IPolygon) : IPolygon OBS: Även Composite Pattern! Macro - transforms : List<Transform> + addTransform(Transform) : void transform(IPolygon) : IPolygon DrawPolygons … ExampleTransform IPolygon + transform(IPolygon) : IPolygon + translate(int,int) : IPolygon public void transform(IPolygon p) { return p.translate(10,10); } Här är ExampleTransform parameteriserad över IPolygon, istället för att ”ha” en specifik IPolygon den anropar action på. Därav streckad pil istället för ifylld. Inte lambdas • Lambda expressions är ett smidigt sätt att förkorta kod. • Vi kan dock göra mer med anonyma klasser än vad lambdas klarar: • En anonym klass kan användas även när interfacet i fråga inte är functional, dvs har fler (eller färre) än exakt en metod. • Skriver vi ut hela klassen kan vi även inkludera attribut, och t ex få state: MyFunctionType f = new MyFunctionType() { public int counter = 0; public int apply(int x) { counter++; return x+5; } }; int y = f.apply(5); int n = f.counter; Självklart borde vi använda en getter istället för att göra counter public... Quiz • Java 8 har lambdas – är Java funktionellt nu? • Svar: Nej. • Lambdas är inte ”first-class functions”, av flera skäl: • Det är inte funktionen som lagras i f i koden nedan; det är objektet som funktionen bor i. MyFunction f = x -> x+5; int y = f.apply(5); Vi kan inte applicera e.g. f(5) • f har typen MyFunction, inte int -> int. Vi kan alltså inte använda f i situationer där vi förväntar oss något som implementerar ett annat functional interface, även om detta andra interface också specificerar en metod som tar en int och returnerar en int. • … och ”first-class functions” är inte allt som definierar det funktionella paradigmet. Objekt vs ADTs • I funktionella språk används algebraiska datatyper, e.g.: data MyIntTree = Leaf Int | Node MyIntTree MyIntTree • En ADT består av olika konstruktorer (inte samma sak som constructors i Java), som kan ta argument. Dessa konstruktorer definierar olika varianter av typen. • En ADT kan plockas isär med hjälp av pattern matching, där olika varianter ger upphov till olika beteende, e.g.: sumTree (Leaf n) = n sumTree (Node l r) = sumTree l + sumTree r Enums • Javas Enum-klasser ger oss en väldigt rudimentär form av ADTer: • Enums kan specificera en lista av olika varianter. • Varianterna i en enum kan inte ta argument. • Varianterna i en enum kan inte vara rekursiva (följer av att de inte kan ta argument). public enum SignalStatus { RED, TOGREEN, GREEN, TORED; } • Vi skulle inte kunna implementera e.g. MyIntTree som en enum. Quiz • Ponera att vi vill definiera en datatyp som består av ett antal varianter (e.g. Polygon: Square, Rectangle, …). • Hur skiljer sig koden när vi använder objekt, respektive en algebraisk datatyp, för att åstadkomma detta? • Svar: De två är varandras ”transponering”. • (Obs: Min användning av termen transponering här är inte generellt vedertagen (än).) Transponering • När vi arbetar med objekt grupperar vi varianter (med data) tillsammans med de grundläggande metoder som arbetar över datan. Olika varianter, även om de är relaterade, definieras separat. • E.g. Vi definierar en Triangle och dess data tillsammans med dess metoder, e.g. paint och translate, tillsammans, men separat från en Rectangle och dess data och metoder. • När vi arbetar med ADTs grupperar vi alla varianter (med data) som är relaterade i en gemensam datatyp. Olika funktioner över denna datatyp läggs separat. • E.g. Vi definierar en datatyp med (bl a) varianterna (konstruktorerna) Triangle och Rectangle. Metoderna för dessa, e.g. move eller getCenterPoint, definieras var och en för sig för alla varianter samtidigt, men separat från varandra. Matris över varianter och funktioner Polygon Square Rectangle Triangle Hexagon translate Square.translate Rectangle.translate Triangle.translate Hexagon.translate rotate Square.rotate Rectangle.rotate Triangle.rotate Hexagon.rotate scale Square.scale Rectangle.scale Triangle.scale Hexagon.scale paint Square.paint Rectangle.paint Triangle.paint Hexagon.paint Objekt: Variant med funktioner Polygon Square Rectangle Triangle Hexagon translate Square.translate Rectangle.translate Triangle.translate Hexagon.translate rotate Square.rotate Rectangle.rotate Triangle.rotate Hexagon.rotate scale Square.scale Rectangle.scale Triangle.scale Hexagon.scale paint Square.paint Rectangle.paint Triangle.paint Hexagon.paint public interface Polygon { public Polygon rotate(double degrees); public Polygon translate(int x, int y); public Polygon scale(double x, double y); public void paint(Graphics g); } public class Square implements Polygon { public Polygon rotate(double degrees){ … } public Polygon translate(int x, int y){ … } public Polygon scale(double x, double y){ … } public void paint(Graphics g){ … }; } public class Square implements Polygon { public Polygon rotate(double degrees){ … } public Polygon translate(int x, int y){ … } public Polygon scale(double x, double y){ … } public void paint(Graphics g){ … }; } Osv för andra varianter… ADT: Funktion med varianter Polygon Square Rectangle Triangle Hexagon translate Square.translate Rectangle.translate Triangle.translate Hexagon.translate rotate Square.rotate Rectangle.rotate Triangle.rotate Hexagon.rotate scale Square.scale Rectangle.scale Triangle.scale Hexagon.scale paint Square.paint Rectangle.paint Triangle.paint Hexagon.paint data Polygon = | | | rotate rotate rotate rotate rotate Square … Rectangle … Triangle … Hexagon … :: Double -> Polygon -> Polygon phi (Square …) = … phi (Rectangle …) = … phi (Triangle …) = … phi (Hexagon …) = … translate:: translate x translate x translate x translate x Int -> Int -> Polygon -> Polygon y (Square …) = … y (Rectangle …) = … y (Triangle …) = … y (Hexagon …) = … Osv för andra funktioner… Quiz • Tänk extensibility: På vilka sätt kan vi utöka funktionaliteten, utan att ändra existerande kod… • … när vi arbetar med ADTer? • … när vi arbetar med (hierarkier av) objekt? • På vilket sätt kan vi inte utöka funktionaliteten utan att ändra kod? • Svar: Med ADTer kan vi lägga till funktioner, men inte varianter. Med objekt kan vi lägga till varianter, men inte funktioner/metoder. The Expression Problem • Phil Wadler (FP/Haskell guru) myntade uttrycket som en utmaning: The expression problem is a new name for an old problem. The goal is to define a datatype by cases, where one can add new cases to the datatype and new functions over the datatype, without recompiling existing code, and while retaining static type safety (e.g., no casts). • Än så länge har vi ingen bra lösning på detta problem, i något språk. • Konsekvens: Vi behöver välja vilken sorts extensibility vi vill kunna tillhandahålla. Live code • Visitor Visitor Pattern Separate an algorithm from the object structure it operates over, allowing new algorithms to be added without the objects knowing about them, by introducing methods that allow object type pattern matching. • Gör det möjligt att (i efterhand) definiera algoritmer som kan operera över ett antal olika klasser, utan att dessa klasser behöver känna till dem. • Ger extensibility i form av att vi kan lägga till fler metoder/beteenden. • Den riktiga styrkan i Visitor är att den låter oss skriva metoder som fungerar över annars helt olika element, av helt olika klasser: • E.g. en Car, dess Engine samt den World den befinner sig i. Visitor Pattern IVisitor <<Interface>> IVisitable <<Interface>> + visit(ConcreteElement1) : void + visit(ConcreteElement2) : void … + accept(IVisitor) : void Notera att olika konkreta element som tillåter visitors kan vara av helt orelaterade typer. ConcreteVisitor ConcreteElement1 ConcreteElement2 +visit(ConcreteElement1) : void +visit(ConcreteElement2) : void + accept(IVisitor) : void + accept(IVisitor) : void public void accept(IVisitor v) { v.visit(this); } Visitor Pattern IPolygonVisitor <<Interface>> IVisitablePolygon <<Interface>> + visit(Rectangle) : void + visit(Triangle) : void … + accept(IVisitor) : void Notera att olika konkreta element som tillåter visitors kan vara av helt orelaterade typer. PolygonLogger Rectangle Triangle +visit(Rectangle) : void +visit(Triangle) : void … + accept(IPolygonVisitor) : void + accept(IPolygonVisitor) : void public void accept(IVisitor v) { v.visit(this); } Sidbyte • Med Visitor Pattern byter vi i någon mening sida: • Vi gör det möjligt att lägga till nya algoritmer (metoder) i efterhand. • Vi har dock låst objekt-strukturen, och eventuella nya klasser vi lägger till kan inte använda sig av de algoritmer vi • Visitor Pattern kräver också att Visitor-objektet känner till (och därför beror på) alla olika klasser den kan besöka. • Slutsats: Visitor Pattern låter oss behandla (grupper av) klasser som om de vore ADTs (typ, nästan). Servant Pattern Define extra behavior (methods) for a group of classes separate from the classes themselves. • Definiera ”hjälp-funktioner” för klasser separat från klasserna själva. • Dvs kombinera dessa klassers publikt användbara metoder för att tillhandahålla mer avancerade beteenden och beräkningar. • En sådan hjälp-funktion kommer typiskt vara static, och ta som första argument det objekt den ska utföra beräkningen eller beteendet på. • Dvs gör det ”implicita argumentet” explicit – precis som i e.g. Haskell! • Funkar ofta bra tillsammans med Visitor Pattern, men kan också användas enskilt. Servant Pattern • Servant Pattern innebär att vi lägger till funktioner som vi skulle gjort i funktionella språk – genom att definiera dem separat, med hjälp av redan existerande funktioner (ofta genom Visitor pattern). • Av vissa kallat ett anti-pattern (dvs dålig design). • Sådana ”funktioner” kan lika gärna läggas som default-metoder i det interface de arbetar över, eller som ”template methods” i en abstrakt superklass. • Dock inte i efterhand utan att ändra i kod – kräver tanke före. • Kan dock ha sina användningar: • Som en del i en Facade, som ska tillhandahålla förenklad funktionalitet. • När vi vill definiera nya funktioner över klasser som ligger i ett bibliotek vi inte kan ändra på. Visitors och Interface Segregation • Interface Segregation Principle säger att vi vill ha små, väldefinierade gränssnitt. Detta är mycket relevant att tänka på när vi implementerar Visitor Pattern. Om vi definierar ett enda gränssnitt Visitor som specificerar ett antal olika element att besöka, då måste en Visitor definiera kod för att hantera alla dessa olika. Men vi kanske bara vill besöka en eller två av dem? • ISP to the rescue: Definiera små gränssnitt, och sätt ihop dem till större vid behov. Sammanfattning • Java lambdas = poor functions (men användbara). • Java enums = broke ADTs (men användbara). • OO har metoder ”på insidan”, FP har metoder ”externt”. • Visitor Pattern låter oss ”byta sida” och definiera metoder externt, men till kostnaden att vi inte längre enkelt kan lägga till fler varianter • Vi byter en sorts extensibility mot en annan. What’s next Block 8-1: Tentamensinfo!!!