Selain Immutable, Pure Function adalah salah satu principle dari Functional Programming yang juga bermanfaat dan bisa diimplementasikan pada Object Oriented Programming (OOP). Pure Function artinya function atau method tersebut isinya murni logika saja tanpa efek samping terhadap objek atau value lainnya di luar function tersebut dan output value-nya selalu sama terhadap input value yang diberikan. Walaupun aslinya ini principle dari Functional Programming, tapi Pure Function ini juga cocok diterapkan pada OOP, khususnya Static Method. Static Method pada OOP umumnya sulit di-maintain dan sangat rentan terhadap bugs kalau ga di-handle dengan baik. Selain itu, juga ribet dibuat unit test-nya. Untuk itu, gw punya tips menggunakan Static Method pada OOP menggunakan Pure Function.
Pure Function memiliki 2 kriteria:
- Return value hasilnya harus identik terhadap input parameter yang sama;
- Tidak ada efek samping terhadap objek atau value di luar function;
Ada 4 cara untuk memenuhi 2 kriteria di atas:
- Jangan biarkan ada perubahan terhadap static variables;
- Jangan biarkan ada perubahan terhadap global objects;
- Tidak ada mutasi terhadap parameter objects;
- Tidak ada koneksi input/output terhadap external system, seperti koneksi ke database, service lain, file system, atau koneksi ke luar aplikasi lainnya;
Sekarang kita coba bedah satu-persatu😎.
1. No Static Variables Changed!
Kita cobain yang pertama dulu. Contohnya seperti berikut:
private static double DISCOUNT = 10D;
public static double calculateFinalPrice(double price){
// global discount can be changed at runtime
DISCOUNT++;
return price - (price * DISCOUNT / 100);
}
Code di atas melanggar kriteria Pure Function karena user bisa melakukan perubahan discount. Code tersebut akan mengubah global value discount setiap eksekusi dan akan menghasilkan output berbeda karena variable "DISCOUNT" bisa berubah setiap eksekusi meskipun dengan input parameter yang sama. Walaupun perubahan tersebut ga diinginkan, tapi bisa saja terjadi by accident karena dengan code tersebut artinya kita "mengijinkan" user untuk melakukan perubahan value. Untuk itu kita harus mengubah code di atas menjadi seperti berikut:
private static final double DISCOUNT_PERCENTAGE = 10D/100;
public static double calculateFinalPrice(double price){
// global discount can't be changed at runtime
return price - (price * DISCOUNT_PERCENTAGE);
}
Sekarang, tidak ada "ijin" untuk melakukan perubahan value karena variable-nya final sehingga global value-nya jadi tetap dan tidak berubah. Misalkan user melakukan perubahan value discount, maka akan compile error. Output value-nya akan selalu sama terhadap input parameter yang sama, misalkan inputnya price=1000, maka output-nya akan selalu 900 setiap eksekusi. Selain itu, juga bisa dibuat seperti berikut:
public double calculateFinalPrice(double price, double discount){
// no global values used
return price - (price * discount / 100);
}
Ini juga Pure Function karena tanpa global variable sama sekali. Value pada parameter juga ga ada efek sampingnya ke luar method dan hasilnya akan selalu sama jika diinput dengan parameter yang sama.
2. No Global Objects Changed!
Ini mirip-mirip kayak global variables.
private static final List<String> ALLOWED_STATUSES = new ArrayList<>(Arrays.asList("Submitted", "Pending"));
public static List<String> getAllowedStatuses(){
// object change is allowed and will add new values
ALLOWED_STATUSES.add("Rejected");
return ALLOWED_STATUSES;
}
Ini juga melanggar kriteria Pure Function karena meskipun constant "ALLOWED_STATUSES" ga bisa di-assign ulang, tapi elemen objeknya masih bisa diubah oleh user. Setiap eksekusi, return value-nya bisa berbeda. Oleh karena itu, kita akan ubah code di atas menjadi immutable collection menggunakan Pure Function.
private static final List<String> ALLOWED_STATUSES = Collections.unmodifiableList(Arrays.asList("Submitted", "Pending"));
public static List<String> getAllowedStatuses(){
// object change can't be executed
return ALLOWED_STATUSES;
}
Dengan code di atas, secara teknis "ALLOWED_STATUSES" ga akan bisa diubah elemennya. Walaupun method add(), remove(), set(), atau mutator lainnya masih tetap bisa dipanggil, tapi akan kena Runtime Exception. Dengan begitu return value-nya pun akan selalu sama setiap dieksekusi dan tanpa efek samping terhadap global objek. Oh ya, untuk Java 9 ke atas, bisa menggunakan method List.of(...) untuk menggunakan immutable collection.
3. No Parameters Mutated!
Sekarang kita coba poin selanjutnya.
Class Object
public class Order{
private double price;
public void setPrice(double price){
this.price = price;
}
public double getPrice(){
return price;
}
}
Contoh code
public static Order getCalculatedOrder(Order order, double initialPrice){
double finalPrice = calculateFinalPrice(initialPrice);
// object parameter is mutated via setter
order.setPrice(finalPrice);
return order;
}
Pada code di atas, hasil return-nya akan selalu sama setiap dieksekusi. Tapi code diatas akan memberikan efek samping terhadap parameter Order karena akan mengubah value "price" ketika dieksekusi. Sekarang, kita ubah code di atas menggunakan Pure Function.
Class Order
public class Order{
private final double price;
public Order(double price){
this.price = price;
}
public double getPrice(){
return price;
}
}
Contoh Code
public static Order getCalculatedOrder(double initialPrice){
double finalPrice = calculateFinalPrice(initialPrice);
// object value is set via constructor
return new Order(finalPrice);
}
public static void printOrderPrice(Order order){
// object is read-only, no mutators
System.out.println(order.getPrice());
}
Objek Order diubah menjadi immutable object. Objek ga dibuat di luar method lalu di-set value-nya secara terpisah, melainkan value-nya diinput lewat constructor saat objek dibuat. Ga ada lagi mutator atau setter sehingga parameter tersebut read-only. Oleh karena itu ga ada lagi efek samping terhadap objek dari parameter tersebut dan return value-nya juga akan selalu sama dengan input parameter yang sama. Value objeknya pun akan selalu konsisten sama seperti saat dibuat walaupun objeknya digunakan di berbagai tempat.
4. No Input/Ouput to External System!
Selanjutnya, kita ke poin terakhir, tentang Input/Output ke external system.
private static final OrderGateway orderGateway = new OrderGateway();
public static Order saveOrder(Order order){
// external side effects
return orderGateway.save(order);
}
Kita mendeklarasikan OrderGateway secara static sehingga bisa dipanggil secara static juga oleh method. Hasil yang didapatkan setelah eksekusi code di atas bisa jadi berbeda-beda setiap eksekusi karena ada dependency ke system di luar aplikasi. Tentu saja ada efek sampingnya, sehingga code di atas melanggar kriteria Pure Function. Selain itu sulit melakukan mocking object saat unit testing. Next, code tersebut kita refactor seperti berikut:
public class OrderCreationUseCase{
private final OrderGateway orderGateway;
public OrderCreationUseCase(OrderGateway orderGateway){
this.orderGateway = orderGateway;
}
public Order saveOrder(Order order){
// external side effects via dependency injection
return this.orderGateway.save(order);
}
}
Kali ini agak berbeda, side effect tetap ada karena tujuannya memang untuk berhubungan dengan external system. Tapi best practice-nya adalah menggunakan dependency injection pada objek yang memiliki side effect ke system di luar aplikasi untuk meminimalisasi impurity dan menjadikannya testable. Jadi sebenarnya ini bukan pure function juga sih, tapi mendekati pure function. Hindari penggunaan static method, static variable, ataupun hidden dependency pada kasus seperti ini. Mocking object pun bisa dilakukan dengan mudah saat unit testing.
Verdict
Dengan Pure Function, kita bisa maintain code dengan lebih baik karena code-nya lebih konsisten dan gampang dibuat unit testingnya. Pada Mockito juga ada cara untuk mocking static method, tapi penggunaannya lebih ribet dan lebih lambat dibanding mocking objek biasa. Pure Function hanya melarang perubahan terhadap global variable, sedangkan untuk local variable yang dideklarasikan di dalam method boleh-boleh saja di-assign ulang karena ga ada efek sampingnya di luar function. Sebenarnya ga hanya untuk static method doang sih, untuk non-static method juga cocok diterapkan. Tapi menurut gw untuk static method pada OOP, penggunaan Pure Function adalah sebuah kewajiban karena paling sering disalahgunakan sehingga bisa menciptakan inkonsistensi dan rentan terhadap bugs.