dev-resources.site
for different kinds of informations.
๐งน Improve Filtering with the Predicate Interface!
Working with lists in apps often means filtering data before showing it to users.
But, we know that filters can sometimes create complex and tricky code to maintain ๐ตโ๐ซ
Today, weโll dive into a cleaner, more reusable approach using the Predicate
interface ๐
๐ฆ Let's imagine we're working on a fun app to help keep track of all our products in storage. Weโve got a Product
model.
data class Product(val id: Int, val name: String, val price: Int, val inStock: Boolean, val weight: Int, val category: String)
๐ The Problems with Traditional Filtering
Take a look at this filtering example:
fun filter(
products: List<Product>,
name: String?,
price: String?,
inStock: Boolean?,
weight: Int?,
category: String?
): List<Product> {
return products
.asSequence()
.filter { product ->
name?.let { product.name.contains(it, ignoreCase = true) } ?: true
}
.filter { product ->
price?.let { product.price == it.toInt() } ?: true
}
.filter { product ->
inStock?.let { product.inStock == it } ?: true
}
.filter { product ->
weight?.let { product.weight == it } ?: true
}
.filter { product ->
category?.let { product.category == it } ?: true
}
.toList()
}
๐ฉ Problems with this strategy:
- Code is hard to maintain:
- Adding new conditions requires modifying existing code
- Poor readability:
- Multiple filters that are difficult to understand and extend.
- Code is hard to reuse:
- We have to duplicate the code if we want to use it in another context.
โ Improving Validation with the Predicate Interface
The Predicate
interface from java.util.function
is here to save the day! It allows us to encapsulate individual validation rules and chain them together easily.
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
default Predicate<T> negate() {
return (t) -> !test(t);
}
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
static <T> Predicate<T> not(Predicate<? super T> target) {
Objects.requireNonNull(target);
return (Predicate<T>)target.negate();
}
}
โ๏ธ What is the Chain of Responsibility Pattern?
The Chain of Responsibility pattern allows passing requests along a chain of handlers. Each handler decides whether to process the request or pass it to the next handler.
โจ Predicate in Action
Now, we will create predicates for each field of the Product
model, such as category
, weight
, inStock
, and so on. These predicates will check if values meet the given conditions. Each of these predicates
will implement the Predicate<Product>
interface, and we can combine them using the and
, or
, and negate
methods.
class ProductWeightPredicate(private val weight: Int) : Predicate<Product> {
override fun test(product: Product): Boolean {
return product.weight == weight
}
}
class ProductCategoryPredicate(private val category: String) : Predicate<Product> {
override fun test(product: Product): Boolean {
return product.category == category
}
}
Now we can dynamically combine these predicates
. For example:
private val predicates: MutableSet<Predicate<Product>> = mutableSetOf()
fun onWeightChanged(weight: Int) {
predicates.add(ProductWeightPredicate(weight))
}
fun onCategoryChanged(category: String) {
predicates.add(ProductCategoryPredicate(category))
}
fun onFilterClicked(products: List<Product>): List<Product> {
val predicate = predicates.reduce { acc, predicate -> acc.and(predicate) }
return products.filter { predicate.test(it) }
}
๐ Searching by Category or Name
We can also combine predicates into different chains. For example, filtering by category
or product
name when searching:
class ProductNamePredicate(private val name: String) : Predicate<Product> {
override fun test(product: Product): Boolean {
return product.name.contains(name, ignoreCase = true)
}
}
fun filterByCategoryOrName(
products: List<Product>,
query: String,
): List<Product> {
val predicate = ProductNamePredicate(query).or(ProductCategoryPredicate(query))
return products.filter { predicate.test(it) }
}
๐ Benefits of This Approach
- Flexibility: We can add new filtering rules without modifying existing code. Each new condition is a new predicate.
- Reusability: Predicates can be reused in different contexts, greatly simplifying code maintenance.
- Clean Code: Your filtering logic becomes easier to read, extend, and maintain.
Happy coding! โญ
Featured ones: