mirror of
https://github.com/tiennm99/java-design-patterns.git
synced 2026-10-19 16:15:55 +00:00
docs: Collection pipeline explanation (#2875)
* collection pipeline docs + refactoring * restore imperative programming code
This commit is contained in:
1 parent
7c1889b8e5
commit
9538c7820c
5 files changed
+148
-47
No files matched your search
@@ -3,25 +3,145 @@ title: Collection Pipeline
|
|||||||
category: Functional
|
category: Functional
|
||||||
language: en
|
language: en
|
||||||
tag:
|
tag:
|
||||||
- Reactive
|
- Reactive
|
||||||
|
- Data processing
|
||||||
---
|
---
|
||||||
|
|
||||||
## Intent
|
## Intent
|
||||||
Collection Pipeline introduces Function Composition and Collection Pipeline, two functional-style patterns that you can combine to iterate collections in your code.
|
|
||||||
In functional programming, it's common to sequence complex operations through a series of smaller modular functions or operations. The series is called a composition of functions, or a function composition. When a collection of data flows through a function composition, it becomes a collection pipeline. Function Composition and Collection Pipeline are two design patterns frequently used in functional-style programming.
|
The Collection Pipeline design pattern is intended to process collections of data by chaining together operations in a
|
||||||
|
sequence where the output of one operation is the input for the next. It promotes a declarative approach to handling
|
||||||
|
collections, focusing on what should be done rather than how.
|
||||||
|
|
||||||
|
## Explanation
|
||||||
|
|
||||||
|
Real-world example
|
||||||
|
|
||||||
|
> Imagine you're in a large library filled with books, and you're tasked with finding all the science fiction books
|
||||||
|
> published after 2000, then arranging them by author name in alphabetical order, and finally picking out the top 5 based
|
||||||
|
> on their popularity or ratings.
|
||||||
|
|
||||||
|
In plain words
|
||||||
|
|
||||||
|
> The Collection Pipeline pattern involves processing data by passing it through a series of operations, each
|
||||||
|
> transforming the data in sequence, much like an assembly line in a factory.
|
||||||
|
|
||||||
|
Wikipedia says
|
||||||
|
|
||||||
|
> In software engineering, a pipeline consists of a chain of processing elements (processes, threads, coroutines,
|
||||||
|
> functions, etc.), arranged so that the output of each element is the input of the next; the name is by analogy to a
|
||||||
|
> physical pipeline. Usually some amount of buffering is provided between consecutive elements. The information that flows
|
||||||
|
> in these pipelines is often a stream of records, bytes, or bits, and the elements of a pipeline may be called filters;
|
||||||
|
> this is also called the pipe(s) and filters design pattern. Connecting elements into a pipeline is analogous to function
|
||||||
|
> composition.
|
||||||
|
|
||||||
|
**Programmatic Example**
|
||||||
|
|
||||||
|
The Collection Pipeline pattern is implemented in this code example by using Java's Stream API to perform a series of
|
||||||
|
transformations on a collection of Car objects. The transformations are chained together to form a pipeline. Here's a
|
||||||
|
breakdown of how it's done:
|
||||||
|
|
||||||
|
1. Creation of Cars: A list of Car objects is created using the `CarFactory.createCars()` method.
|
||||||
|
|
||||||
|
`var cars = CarFactory.createCars();`
|
||||||
|
|
||||||
|
2. Filtering and Transforming: The `FunctionalProgramming.getModelsAfter2000(cars)` method filters the cars to only
|
||||||
|
include those made after the year 2000, and then transforms the filtered cars into a list of their model names.
|
||||||
|
|
||||||
|
`var modelsFunctional = FunctionalProgramming.getModelsAfter2000(cars);`
|
||||||
|
|
||||||
|
In the `getModelsAfter2000` method, the pipeline is created as follows:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public static List<String> getModelsAfter2000(List<Car> cars){
|
||||||
|
return cars.stream().filter(car->car.getYear()>2000)
|
||||||
|
.sorted(comparing(Car::getYear))
|
||||||
|
.map(Car::getModel)
|
||||||
|
.collect(toList());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Grouping: The `FunctionalProgramming.getGroupingOfCarsByCategory(cars)` method groups the cars by their category.
|
||||||
|
|
||||||
|
`var groupingByCategoryFunctional = FunctionalProgramming.getGroupingOfCarsByCategory(cars);`
|
||||||
|
|
||||||
|
In the getGroupingOfCarsByCategory method, the pipeline is created as follows:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public static Map<Category, List<Car>>getGroupingOfCarsByCategory(List<Car> cars){
|
||||||
|
return cars.stream().collect(groupingBy(Car::getCategory));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Filtering, Sorting and Transforming: The `FunctionalProgramming.getSedanCarsOwnedSortedByDate(List.of(john))` method
|
||||||
|
filters the cars owned by a person to only include sedans, sorts them by date, and then transforms the sorted cars
|
||||||
|
into a list of Car objects.
|
||||||
|
|
||||||
|
`var sedansOwnedFunctional = FunctionalProgramming.getSedanCarsOwnedSortedByDate(List.of(john));`
|
||||||
|
|
||||||
|
In the `getSedanCarsOwnedSortedByDate` method, the pipeline is created as follows:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public static List<Car> getSedanCarsOwnedSortedByDate(List<Person> persons){
|
||||||
|
return persons.stream().flatMap(person->person.getCars().stream())
|
||||||
|
.filter(car->Category.SEDAN.equals(car.getCategory()))
|
||||||
|
.sorted(comparing(Car::getDate))
|
||||||
|
.collect(toList());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In each of these methods, the Collection Pipeline pattern is used to perform a series of operations on the collection of
|
||||||
|
cars in a declarative manner, which improves readability and maintainability.
|
||||||
|
|
||||||
## Class diagram
|
## Class diagram
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Applicability
|
## Applicability
|
||||||
Use the Collection Pipeline pattern when
|
|
||||||
|
|
||||||
* When you want to perform a sequence of operations where one operation's collected output is fed into the next
|
This pattern is applicable in scenarios involving bulk data operations such as filtering, mapping, sorting, or reducing
|
||||||
* When you use a lot of statements in your code
|
collections. It's particularly useful in data analysis, transformation tasks, and where a sequence of operations needs
|
||||||
* When you use a lot of loops in your code
|
to be applied to each element of a collection.
|
||||||
|
|
||||||
|
## Known Uses
|
||||||
|
|
||||||
|
* LINQ in .NET
|
||||||
|
* Stream API in Java 8+
|
||||||
|
* Collections in modern functional languages (e.g., Haskell, Scala)
|
||||||
|
* Database query builders and ORM frameworks
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
Benefits:
|
||||||
|
|
||||||
|
* Readability: The code is more readable and declarative, making it easier to understand the sequence of operations.
|
||||||
|
* Maintainability: Easier to modify or extend the pipeline with additional operations.
|
||||||
|
* Reusability: Common operations can be abstracted into reusable functions.
|
||||||
|
* Lazy Evaluation: Some implementations allow for operations to be lazily evaluated, improving performance.
|
||||||
|
|
||||||
|
Trade-offs:
|
||||||
|
|
||||||
|
* Performance Overhead: Chaining multiple operations can introduce overhead compared to traditional loops, especially
|
||||||
|
for short pipelines or very large collections.
|
||||||
|
* Debugging Difficulty: Debugging a chain of operations might be more challenging due to the lack of intermediate
|
||||||
|
variables.
|
||||||
|
* Limited to Collections: Primarily focused on collections, and its utility might be limited outside of collection
|
||||||
|
processing.
|
||||||
|
|
||||||
|
## Related Patterns
|
||||||
|
|
||||||
|
* [Builder](https://java-design-patterns.com/patterns/builder/): Similar fluent interface style but used for object
|
||||||
|
construction.
|
||||||
|
* [Chain of Responsibility](https://java-design-patterns.com/patterns/chain-of-responsibility/): Conceptually similar in
|
||||||
|
chaining handlers, but applied to object requests rather than data collection processing.
|
||||||
|
* [Strategy](https://java-design-patterns.com/patterns/strategy/): Can be used within a pipeline stage to encapsulate
|
||||||
|
different algorithms that can be selected at runtime.
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
* [Function composition and the Collection Pipeline pattern](https://www.ibm.com/developerworks/library/j-java8idioms2/index.html)
|
* [Function composition and the Collection Pipeline pattern](https://www.ibm.com/developerworks/library/j-java8idioms2/index.html)
|
||||||
* [Martin Fowler](https://martinfowler.com/articles/collection-pipeline/)
|
* [Collection Pipeline described by Martin Fowler](https://martinfowler.com/articles/collection-pipeline/)
|
||||||
* [Java8 Streams](https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html)
|
* [Java8 Streams](https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html)
|
||||||
|
* [Refactoring: Improving the Design of Existing Code](https://amzn.to/3VDMWDO)
|
||||||
|
* [Functional Programming in Scala](https://amzn.to/4cEo6K2)
|
||||||
|
* [Java 8 in Action: Lambdas, Streams, and functional-style programming](https://amzn.to/3THp4wy)
|
||||||
@@ -22,22 +22,11 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
* THE SOFTWARE.
|
* THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
package com.iluwatar.collectionpipeline;
|
|
||||||
|
|
||||||
import lombok.EqualsAndHashCode;
|
package com.iluwatar.collectionpipeline;
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Car class that has the properties of make, model, year and category.
|
* A Car class that has the properties of make, model, year and category.
|
||||||
*/
|
*/
|
||||||
@Getter
|
public record Car(String make, String model, int year, Category category) {
|
||||||
@EqualsAndHashCode
|
}
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class Car {
|
|
||||||
private final String make;
|
|
||||||
private final String model;
|
|
||||||
private final int year;
|
|
||||||
private final Category category;
|
|
||||||
|
|
||||||
}
|
|
||||||
+7
-7
@@ -22,6 +22,7 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
* THE SOFTWARE.
|
* THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.iluwatar.collectionpipeline;
|
package com.iluwatar.collectionpipeline;
|
||||||
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
@@ -54,9 +55,8 @@ public class FunctionalProgramming {
|
|||||||
* @return {@link List} of {@link String} representing models built after year 2000
|
* @return {@link List} of {@link String} representing models built after year 2000
|
||||||
*/
|
*/
|
||||||
public static List<String> getModelsAfter2000(List<Car> cars) {
|
public static List<String> getModelsAfter2000(List<Car> cars) {
|
||||||
return cars.stream().filter(car -> car.getYear() > 2000)
|
return cars.stream().filter(car -> car.year() > 2000).sorted(Comparator.comparing(Car::year))
|
||||||
.sorted(Comparator.comparing(Car::getYear))
|
.map(Car::model).toList();
|
||||||
.map(Car::getModel).toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,7 +66,7 @@ public class FunctionalProgramming {
|
|||||||
* @return {@link Map} with category as key and cars belonging to that category as value
|
* @return {@link Map} with category as key and cars belonging to that category as value
|
||||||
*/
|
*/
|
||||||
public static Map<Category, List<Car>> getGroupingOfCarsByCategory(List<Car> cars) {
|
public static Map<Category, List<Car>> getGroupingOfCarsByCategory(List<Car> cars) {
|
||||||
return cars.stream().collect(Collectors.groupingBy(Car::getCategory));
|
return cars.stream().collect(Collectors.groupingBy(Car::category));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,8 +76,8 @@ public class FunctionalProgramming {
|
|||||||
* @return {@link List} of {@link Car} to belonging to the group
|
* @return {@link List} of {@link Car} to belonging to the group
|
||||||
*/
|
*/
|
||||||
public static List<Car> getSedanCarsOwnedSortedByDate(List<Person> persons) {
|
public static List<Car> getSedanCarsOwnedSortedByDate(List<Person> persons) {
|
||||||
return persons.stream().map(Person::getCars).flatMap(List::stream)
|
return persons.stream().map(Person::cars).flatMap(List::stream)
|
||||||
.filter(car -> Category.SEDAN.equals(car.getCategory()))
|
.filter(car -> Category.SEDAN.equals(car.category()))
|
||||||
.sorted(Comparator.comparing(Car::getYear)).toList();
|
.sorted(Comparator.comparing(Car::year)).toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+9
-9
@@ -61,7 +61,7 @@ public class ImperativeProgramming {
|
|||||||
List<Car> carsSortedByYear = new ArrayList<>();
|
List<Car> carsSortedByYear = new ArrayList<>();
|
||||||
|
|
||||||
for (Car car : cars) {
|
for (Car car : cars) {
|
||||||
if (car.getYear() > 2000) {
|
if (car.year() > 2000) {
|
||||||
carsSortedByYear.add(car);
|
carsSortedByYear.add(car);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,13 +69,13 @@ public class ImperativeProgramming {
|
|||||||
Collections.sort(carsSortedByYear, new Comparator<Car>() {
|
Collections.sort(carsSortedByYear, new Comparator<Car>() {
|
||||||
@Override
|
@Override
|
||||||
public int compare(Car car1, Car car2) {
|
public int compare(Car car1, Car car2) {
|
||||||
return car1.getYear() - car2.getYear();
|
return car1.year() - car2.year();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
List<String> models = new ArrayList<>();
|
List<String> models = new ArrayList<>();
|
||||||
for (Car car : carsSortedByYear) {
|
for (Car car : carsSortedByYear) {
|
||||||
models.add(car.getModel());
|
models.add(car.model());
|
||||||
}
|
}
|
||||||
|
|
||||||
return models;
|
return models;
|
||||||
@@ -90,12 +90,12 @@ public class ImperativeProgramming {
|
|||||||
public static Map<Category, List<Car>> getGroupingOfCarsByCategory(List<Car> cars) {
|
public static Map<Category, List<Car>> getGroupingOfCarsByCategory(List<Car> cars) {
|
||||||
Map<Category, List<Car>> groupingByCategory = new HashMap<>();
|
Map<Category, List<Car>> groupingByCategory = new HashMap<>();
|
||||||
for (Car car : cars) {
|
for (Car car : cars) {
|
||||||
if (groupingByCategory.containsKey(car.getCategory())) {
|
if (groupingByCategory.containsKey(car.category())) {
|
||||||
groupingByCategory.get(car.getCategory()).add(car);
|
groupingByCategory.get(car.category()).add(car);
|
||||||
} else {
|
} else {
|
||||||
List<Car> categoryCars = new ArrayList<>();
|
List<Car> categoryCars = new ArrayList<>();
|
||||||
categoryCars.add(car);
|
categoryCars.add(car);
|
||||||
groupingByCategory.put(car.getCategory(), categoryCars);
|
groupingByCategory.put(car.category(), categoryCars);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return groupingByCategory;
|
return groupingByCategory;
|
||||||
@@ -111,12 +111,12 @@ public class ImperativeProgramming {
|
|||||||
public static List<Car> getSedanCarsOwnedSortedByDate(List<Person> persons) {
|
public static List<Car> getSedanCarsOwnedSortedByDate(List<Person> persons) {
|
||||||
List<Car> cars = new ArrayList<>();
|
List<Car> cars = new ArrayList<>();
|
||||||
for (Person person : persons) {
|
for (Person person : persons) {
|
||||||
cars.addAll(person.getCars());
|
cars.addAll(person.cars());
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Car> sedanCars = new ArrayList<>();
|
List<Car> sedanCars = new ArrayList<>();
|
||||||
for (Car car : cars) {
|
for (Car car : cars) {
|
||||||
if (Category.SEDAN.equals(car.getCategory())) {
|
if (Category.SEDAN.equals(car.category())) {
|
||||||
sedanCars.add(car);
|
sedanCars.add(car);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -124,7 +124,7 @@ public class ImperativeProgramming {
|
|||||||
sedanCars.sort(new Comparator<Car>() {
|
sedanCars.sort(new Comparator<Car>() {
|
||||||
@Override
|
@Override
|
||||||
public int compare(Car o1, Car o2) {
|
public int compare(Car o1, Car o2) {
|
||||||
return o1.getYear() - o2.getYear();
|
return o1.year() - o2.year();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -25,16 +25,8 @@
|
|||||||
package com.iluwatar.collectionpipeline;
|
package com.iluwatar.collectionpipeline;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Person class that has the list of cars that the person owns and use.
|
* A Person class that has the list of cars that the person owns and use.
|
||||||
*/
|
*/
|
||||||
@Getter
|
public record Person(List<Car> cars) {}
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class Person {
|
|
||||||
|
|
||||||
private final List<Car> cars;
|
|
||||||
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user