Function Based Filtering in Go(slice and generic)
Yazılım geliştirme süreçlerinde sıkça karşılaşılan temel işlemlerden biri veri filtrelemedir. Örneğin array gibi veri yapıları üzerinde bazı koşullara uyan bazı filtreler geliştirmek isteyebiliriz. Bu metotlar genellikle girdi olarak bir array alır ve şarta uyan elemanları yeni bir array olarak döndürür. Geleneksel olarak, filtreleme işlemlerini döngülerle veya sabit kurallar içeren if bloklarıyla gerçekleştiririz. Ancak bu yaklaşım esneklikten yoksundur, kodun tekrar kullanılabilirliğini azaltır.
Bu makalede Go dili üzerinde generic bir filter mekanizması oluşturmaya çalışacağız.
Öncelikle basit bir filter yazarak başlayalım.
package main
import "fmt"
func main() {
values := []int{1, 2, 3, 4, 5, -8, -4}
positiveValues := []int{}
for i := range values {
if values[i] > 0 {
positiveValues = append(positiveValues, values[i])
}
}
fmt.Println(positiveValues)
}
values isimli bir slice veri yapısına hem pozitif hem de negatif değerler atayarak döngüsel, basit bir filter yapısı gerçekleştirmiş olduk. Teker teker tüm elemanları gezerek pozitif değerleri yeni bir slice veri yapısına ekleyen ve çıktı olarak pozitif değerleri ekrana yazdıran bu kod oldukça basit ve sade bir filter logic içermektedir.
Ek olarak bir tane de struct olarak kurgulayabileceğimiz bir örnek verelim.
Id, Name, Age ve Role taşıyan User isimli bir struct yaratalım:
package models
type User struct {
Id int
Name string
Age int
Role string
}
Bu bilgilere göre logicler içeren bir servis oluşturalım.
package services
import "function-based-filtering/models"
type UserService interface {
GetAll() []models.User
GetById(id int) models.User
GetByName(name string) models.User
GetByRole(role string) []models.User
GetByAge(age int) []models.User
GetByGreaterThanAge(age int) []models.User
GetByLessThanAge(age int) []models.User
}
type userService struct {
users []models.User
}
func NewUserService(users []models.User) UserService {
return &userService{users: users}
}
func (u *userService) GetAll() []models.User {
return u.users
}
func (u *userService) GetById(id int) models.User {
for _, user := range u.users {
if user.Id == id {
return user
}
}
return models.User{}
}
func (u *userService) GetByName(name string) models.User {
for _, user := range u.users {
if user.Name == name {
return user
}
}
return models.User{}
}
func (u *userService) GetByRole(role string) []models.User {
var users []models.User
for _, user := range u.users {
if user.Role == role {
users = append(users, user)
}
}
return users
}
func (u *userService) GetByAge(age int) []models.User {
var users []models.User
for _, user := range u.users {
if user.Age == age {
users = append(users, user)
}
}
return users
}
func (u *userService) GetByGreaterThanAge(age int) []models.User {
var users []models.User
for _, user := range u.users {
if user.Age > age {
users = append(users, user)
}
}
return users
}
func (u *userService) GetByLessThanAge(age int) []models.User {
var users []models.User
for _, user := range u.users {
if user.Age < age {
users = append(users, user)
}
}
return users
}
UserService’i kullanan ve verileri filtreleyen main.go:
package main
import (
"fmt"
"function-based-filtering/models"
"function-based-filtering/services"
)
func main() {
users := []models.User{
{Id: 1, Name: "Ali", Age: 25, Role: "Admin"},
{Id: 2, Name: "Ayşe", Age: 30, Role: "User"},
{Id: 3, Name: "Mehmet", Age: 35, Role: "User"},
{Id: 4, Name: "Zeynep", Age: 28, Role: "Admin"},
}
userService := services.NewUserService(users)
fmt.Println("Tüm Kullanıcılar:", userService.GetAll())
fmt.Println("ID 2 Olan Kullanıcı:", userService.GetById(2))
fmt.Println("Ayşe Kullanıcısı:", userService.GetByName("Ayşe"))
fmt.Println("Admin Rolündeki Kullanıcılar:", userService.GetByRole("Admin"))
fmt.Println("Yaşı 30 Olan Kullanıcılar:", userService.GetByAge(30))
fmt.Println("30 Yaşından Büyük Kullanıcılar:", userService.GetByGreaterThanAge(30))
fmt.Println("30 Yaşından Küçük Kullanıcılar:", userService.GetByLessThanAge(30))
}
$ go run main.go
Tüm Kullanıcılar: [{1 Ali 25 Admin} {2 Ayşe 30 User} {3 Mehmet 35 User} {4 Zeynep 28 Admin}]
ID 2 Olan Kullanıcı: {2 Ayşe 30 User}
Ayşe Kullanıcısı: {2 Ayşe 30 User}
Admin Rolündeki Kullanıcılar: [{1 Ali 25 Admin} {4 Zeynep 28 Admin}]
Yaşı 30 Olan Kullanıcılar: [{2 Ayşe 30 User}]
30 Yaşından Büyük Kullanıcılar: [{3 Mehmet 35 User}]
30 Yaşından Küçük Kullanıcılar: [{1 Ali 25 Admin} {4 Zeynep 28 Admin}]
slice veri tipinde User nesnesini içerdiği özelliklere göre filtrelemek istediğimizde hemen hemen yukarıdaki örnek gibi her bir özelliği içeren yeni filter yordamlar eklemek gerekir. Aslında bu yöntem UserService arayüzünün oldukça fazla özellik içermesine neden olur. Temelde bütün yordamların yaptığı iş benzerdir. Ek olarak her zaman sadece bir yordamı kullanmak yeterli olmayabilir. Örneğin hem Admin rolüne sahip hem de 30 yaşından küçük kullanıcıları bulabilmek için iki yöntem vardır.
- Yeni bir yordam yazarak bu kuralın implemente edilmesi
- Kullanılan yerde Admin rolüne sahip kullanıcıların çekilip yordamın döndürdüğü user slice ile 30 yaşından küçük kullanıcı yordamını tekrar çağırmak
Go dili, fonksiyonları first-class citizens olarak kabul eder. Yani fonksiyonları değişkenler gibi kullanabilir, parametre olarak gönderebilir ve döndürebiliriz. Bu özellikten faydalanarak Function Based Filtering yaklaşımı ile dinamik, kolay genişletilebilir ve modüler bir filtreleme mekanizması oluşturabiliriz.
Temelde tüm filtreler aynı mantıkla çalışır, kurala uyan bir item varsa döndürülür. Yani filtrenin tek kuralı item’ın match olmasıdır.
İlgili entity üzerinde kullanabilecek bütün filtreleri birer metot olarak tasarlayarak başlayabiliriz.
filterTypes.go isimli bir dosya içerisinde:
package models
func AgeFilter(age int) func(User)bool{
return func(u User) bool {
return u.Age == age
}
}
func NameFilter(name string) func(User)bool{
return func(u User) bool {
return u.Name == name
}
}
func RoleFilter(role string) func(User)bool{
return func(u User) bool {
return u.Role == role
}
}
func GreaterThanAgeFilter(age int) func(User)bool{
return func(u User) bool {
return u.Age > age
}
}
func LessThanAgeFilter(age int) func(User)bool{
return func(u User) bool {
return u.Age < age
}
}
İlgili entity için belitilen Filter kurallarını implemente edebiliriz.
Her filter parametre olarak bir veya daha fazla girdi alabilirken hepsi geriye birer func döndürür. Bu func parametre olarak ilgili entity yani User objesini alır ve bool döndürür. Eğer kurala uygunsa true değil ise false döndürerek ilgili kayıt için match bilgisine sahip olmuş oluruz.
Artık bu function’lar için generic olarak kuralları çalıştırabilecek ve projemizde herhangi bir entity için (user, product, customer vb.) kullanılabilecek yeni bir servis oluşturabiliriz.
package services
type FilterableService[T any] interface {
Filter(items []T, filters ...func(T) bool) []T
}
type filterableService[T any] struct{}
func NewFilterableService[T any]() FilterableService[T] {
return &filterableService[T]{}
}
func (f *filterableService[T]) Filter(items []T, filters ...func(T) bool) []T {
var result []T
for _, item := range items {
include := true
for _, filter := range filters {
if !filter(item) {
include = false
break
}
}
if include {
result = append(result, item)
}
}
return result
}
FilterableService proje içerisindeki slice tipindeki bütün yapılar için function based filtering yapabilecek bir Filter metodu içerir. Bu metod, kendisine parametre olarak gelen itemlar ve yine kendisine parametre olarak gelen metodlar içerisinde dönerek verinin include olup olmadığını kontrol eder. Filter AND mantığı ile çalışır. Yani kendisine parametre olarak gelen bütün yordamların true yani match olarak dönmesini kabul eder. Eğer herhangi bir filter uymuyorsa döngüden çıkar ve bir sonraki item’a geçer.
package main
import (
"fmt"
"function-based-filtering/models"
"function-based-filtering/services"
)
func main() {
users := []models.User{
{Id: 1, Name: "Ali", Age: 25, Role: "Admin"},
{Id: 2, Name: "Ayşe", Age: 30, Role: "User"},
{Id: 3, Name: "Mehmet", Age: 35, Role: "User"},
{Id: 4, Name: "Zeynep", Age: 28, Role: "Admin"},
}
filterableService := services.NewFilterableService[models.User]()
ageFilter := models.AgeFilter(30)
roleFilter := models.RoleFilter("Admin")
nameFilter := models.NameFilter("Ali")
greaterThanAgeFilter := models.GreaterThanAgeFilter(30)
lessThanAgeFilter := models.LessThanAgeFilter(30)
filteredUsers := filterableService.Filter(users, ageFilter, roleFilter, nameFilter, greaterThanAgeFilter, lessThanAgeFilter)
fmt.Println("Filtrelenmiş Kullanıcılar:", filteredUsers)
}
Bu yöntem ile tanımlanan varlıklar üzerinde filtre kabiliyetini kolaylıkla arttırabiliriz. Her yeni filter özelliğini bir metot olarak tanımlayıp kullanmak istediğiniz yerde bu filtreyi oluşturup generic olarak filtreleyebilirsiniz. Böylece projelerinizde filtreleme mantığı ortak olup varlıklarınızın içerdiği alanlara göre filtreleme yapabilirsiniz.
Generic filter yapısına, projenize ve kullanım ihtiyaçlarınıza göre yeni özellikler kazandırabilirsiniz. Benim oldukça sık kullandığım Any, All, First, Last, Count, FindIndex, FindIndexes gibi yeni özellikler ekledim. Bu sayede runtime’da elimde olan bir slice için hızlıca filtreler ekleyebiliyor ve generic’ler sayesinde kullanabiliyorum.
Kodun son hali:
package services
type FilterableService[T any] interface {
FilterAnd(items []T, filters ...func(T) bool) []T
FilterOr(items []T, filters ...func(T) bool) []T
Any(items []T, filters ...func(T) bool) bool
All(items []T, filters ...func(T) bool) bool
First(items []T, filters ...func(T) bool) T
Last(items []T, filters ...func(T) bool) T
Count(items []T, filters ...func(T) bool) int
FindIndex(items []T, filters ...func(T) bool) int
FindIndexes(items []T, filters ...func(T) bool) []int
}
type filterableService[T any] struct{}
func NewFilterableService[T any]() FilterableService[T] {
return &filterableService[T]{}
}
func (f *filterableService[T]) FilterAnd(items []T, filters ...func(T) bool) []T {
var result []T
for _, item := range items {
include := true
for _, filter := range filters {
if !filter(item) {
include = false
break
}
}
if include {
result = append(result, item)
}
}
return result
}
func (f *filterableService[T]) FilterOr(items []T, filters ...func(T) bool) []T {
var result []T
for _, item := range items {
include := false
for _, filter := range filters {
if filter(item) {
include = true
break
}
}
if include {
result = append(result, item)
}
}
return result
}
func (f *filterableService[T]) Any(items []T, filters ...func(T) bool) bool {
for _, item := range items {
for _, filter := range filters {
if filter(item) {
return true
}
}
}
return false
}
func (f *filterableService[T]) All(items []T, filters ...func(T) bool) bool {
for _, item := range items {
for _, filter := range filters {
if !filter(item) {
return false
}
}
}
return true
}
func (f *filterableService[T]) First(items []T, filters ...func(T) bool) T {
for _, item := range items {
include := true
for _, filter := range filters {
if !filter(item) {
include = false
break
}
}
if include {
return item
}
}
return items[0]
}
func (f *filterableService[T]) Last(items []T, filters ...func(T) bool) T {
for i := len(items) - 1; i >= 0; i-- {
include := true
for _, filter := range filters {
if !filter(items[i]) {
include = false
break
}
}
if include {
return items[i]
}
}
return items[len(items)-1]
}
func (f *filterableService[T]) Count(items []T, filters ...func(T) bool) int {
count := 0
for _, item := range items {
include := true
for _, filter := range filters {
if !filter(item) {
include = false
break
}
}
if include {
count++
}
}
return count
}
func (f *filterableService[T]) FindIndex(items []T, filters ...func(T) bool) int {
for i, item := range items {
include := true
for _, filter := range filters {
if !filter(item) {
include = false
break
}
}
if include {
return i
}
}
return -1
}
func (f *filterableService[T]) FindIndexes(items []T, filters ...func(T) bool) []int {
var indexes []int
for i, item := range items {
include := true
for _, filter := range filters {
if !filter(item) {
include = false
break
}
}
if include {
indexes = append(indexes, i)
}
}
return indexes
}
Umarım faydalı olur. 👋