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!!!