Skip to content

A reactive library using publisher-subscriber pattern with elegant object thinking in mind

Notifications You must be signed in to change notification settings

octaviospain/transgressoft-commons

Repository files navigation

Quality Gate Status

Transgressoft Commons

A reactive library for Kotlin & Java projects that implements the Publisher-Subscriber pattern, enabling more maintainable and decoupled systems through reactive programming principles.

πŸ“– Overview

Transgressoft Commons provides a framework for entities that follow a 'reactive' approach based on the Publisher-Subscriber pattern. This allows objects to subscribe to changes in others while maintaining clean boundaries and separation of concerns.

The approach is inspired by object-oriented design principles where entities aren't merely passive data structures, but active objects with their own behaviors and responsibilities. Instead of other objects directly manipulating an entity's state, they subscribe to its changes, creating a more decoupled and maintainable system.

Key Features:

  • Event-Driven Architecture: Built around the Publisher-Subscriber pattern for loosely coupled communication
  • Reactive Entities: Objects that automatically notify subscribers when their state changes
  • Automated JSON Serialization: Repository implementations that persist entity changes to JSON files automatically
  • Thread-Safe Operations: Concurrent operations are handled safely with debounced file I/O
  • Repository Pattern: Flexible data access through repositories with powerful querying capabilities
  • Reactive Primitives: Wrapper types that make primitive values observable
  • Asynchronous Processing: Non-blocking operations using Kotlin coroutines
  • Java Interoperability: Designed to work seamlessly from both Kotlin and Java code

πŸ“‘ Table of Contents

πŸ”„ Core Concepts: Reactive Event System

The heart of Transgressoft Commons is its reactive event system, where objects communicate through events rather than direct manipulation.

Reactive Primitives

The simplest way to understand the reactive approach is through the primitive wrappers. These allow basic values to participate in the reactive system:

// Create a reactive primitive with an ID and initial value
val appName: ReactivePrimitive<String> = ReactiveString("MyApp")

// Subscribe to changes with a simple lambda function
val subscription = appName.subscribe { event ->
    val oldValue = event.oldEntities.values.first().value
    val newValue = event.entities.values.first().value
    println("Config changed: $oldValue -> $newValue")
}

// When value changes, subscribers are automatically notified
appName.value = "NewAppName"  
// Output: Config changed: MyApp -> NewAppName

// Later, if needed, you can cancel the subscription
subscription.cancel()

Reactive Entities

Any object can become reactive by implementing the ReactiveEntity interface, typically by extending ReactiveEntityBase:

// Define a reactive entity
data class Person(override val id: Int, var name: String) : ReactiveEntityBase<Int, Person>() {
    var salary: Double = 0.0
        set(value) {
            // setAndNotify handles the notification logic
            setAndNotify(value, field) { field = it }
        }

    override val uniqueId: String = "person-$id"
    override fun clone(): Person = copy()
}

// Create a person and subscribe to changes using a Consumer
val person: ReactiveEntity<Int, Person> = Person(1, "Alice")
val subscription = person.subscribe { event ->
    val entity = event.entities.values.first()
    val oldEntity = event.oldEntities.values.first()
    println("Salary updated from ${oldEntity.salary} to ${entity.salary}")
}

// Changes trigger notifications
person.salary = 75000.0
// Output: Salary updated from 0.0 to 75000.0

Repository Subscriptions

Repositories manage collections of entities while maintaining the reactive behavior:

// Create a repository for Person entities
val repository: Repository<Int, Person> = VolatileRepository<Int, Person>("PersonRepository")

// Subscribe to CRUD events with lambda functions
val createSubscription = repository.subscribe(StandardCrudEvent.Type.CREATE) { event ->
    println("Entities created: ${event.entities.values}")
}

val updateSubscription = repository.subscribe(StandardCrudEvent.Type.UPDATE) { event ->
    val changeEvent = event as EntityChangeEvent<Int, Person>
    println("Entities updated:")
    changeEvent.entities.forEach { (id, entity) ->
        val oldEntity = changeEvent.oldEntities[id]
        println("  $id: $oldEntity -> $entity")
    }
}

val deleteSubscription = repository.subscribe(StandardCrudEvent.Type.DELETE) { event ->
    println("Entities deleted: ${event.entities.values}")
}

// Repository operations trigger events
repository.add(Person(1, "Alice"))
// Output: Entities created: [Person(id=1, name=Alice)]

// Entity changes are propagated through the repository
repository.runForSingle(1) { person ->
    person.salary = 80000.0
}
// Output: Entities updated:
//   1: Person(id=1, name=Alice, salary=0.0) -> Person(id=1, name=Alice, salary=80000.0)

Extensibility

The library is designed to be extensible, allowing you to create custom publishers and subscribers:

  1. Custom Publishers: Implement TransEventPublisher<E> or extend TransEventPublisherBase<E> to create new event sources
  2. Custom Subscribers: Implement TransEventSubscriber<T, E> or extend TransEventSubscriberBase<T, E> to handle events
  3. Custom Events: Create new event types by implementing the TransEvent interface

For Java compatibility or more complex subscription handling, you can also implement a full subscriber:

// Create a subscriber with more control over lifecycle events
val repositorySubscriber: TransEventSubscriber<Person, CrudEvent<Int, Person>> = 
    object : TransEventSubscriberBase<Person, CrudEvent<Int, Person>>("RepositorySubscriber") {
        init {
            // Set up subscription actions
            addOnNextEventAction(StandardCrudEvent.Type.CREATE) { event ->
                println("Entities created: ${event.entities.values}")
            }
            
            addOnErrorEventAction { error ->
                println("Error occurred: $error")
            }
            
            addOnCompleteEventAction {
                println("Publisher has completed sending events")
            }
        }
    }

// Subscribe using the full subscriber
repository.subscribe(repositorySubscriber)

The core API classes that library consumers will typically use:

  • TransEventPublisher - Interface for objects that publish events
  • TransEventSubscriber - Interface for objects that subscribe to events
  • ReactiveEntity - Interface for entities that can be observed
  • Repository - Interface for collections of entities with CRUD operations
  • CrudEvent - Events representing repository operations
  • JsonRepository - Interface for repositories with JSON persistence

πŸ’Ύ Core Concepts: JSON Serialization

Transgressoft Commons provides automatic JSON serialization for repository operations, making persistence seamless. The library uses kotlinx.serialization for JSON processing, so users should familiarize themselves with this library to effectively create serializers for their entity types.

JSON File Repositories

The library includes implementations that automatically persist entities to JSON files:

// Define a serializer for your entity type using kotlinx.serialization
@Serializable
data class Person(val id: Int, val name: String, var salary: Double = 0.0) : ReactiveEntityBase<Int, Person>() {
    override val uniqueId: String = "person-$id"
    override fun clone(): Person = copy()
}

// Create a map serializer for your entities
object MapIntPersonSerializer : KSerializer<Map<Int, Person>> {
    // Serialization implementation details
}

// Define your repository class
class PersonJsonFileRepository(file: File) : JsonFileRepositoryBase<Int, Person>(
    name = "PersonRepository",
    file = file,
    mapSerializer = MapIntPersonSerializer
)

// Create and use the repository
val jsonRepository: JsonRepository<Int, Person> = PersonJsonFileRepository(File("persons.json"))
jsonRepository.add(Person(1, "Alice"))
jsonRepository.add(Person(2, "Bob"))

// When entities change, the JSON file is automatically updated
jsonRepository.runForSingle(1) { person ->
    person.salary = 85000.0
}

// Changes are debounced to prevent excessive file operations

Flexible JSON Repository

For simpler use cases, the library provides a flexible repository for primitive values:

// Create a repository for configuration values
val configRepository  = FlexibleJsonFileRepository(File("config.json"))

// Create reactive primitives in the repository
val serverName: ReactivePrimitive<String> = configRepository.createReactiveString("server.name", "MainServer")
val maxConnections: ReactivePrimitive<Int> = configRepository.createReactiveInt("max.connections", 100)
val debugMode: ReactivePrimitive<Boolean> = configRepository.createReactiveBoolean("debug.mode", false)

// When values change, they are automatically persisted
maxConnections.value = 150
debugMode.value = true
// The JSON file is updated with the new values

Key Benefits of Automatic Serialization

  1. Transparent Persistence - No need to manually save changes
  2. Optimized I/O - Changes are debounced to reduce disk operations
  3. Thread Safety - Concurrent operations are handled safely
  4. Consistency - Repository and file are always in sync

Note: While the examples in this README are in Kotlin, the library is designed with Java compatibility in mind. Java usage examples will be provided in future documentation.

🀝 Contributing

Contributions are welcome! Please see CONTRIBUTING.md for details.

πŸ“„ License and Attributions

Copyright (c) 2025 Octavio Calleya GarcΓ­a.

Transgressoft Commons is free software under GNU GPL version 3 license and is available here.

This project uses:

The approach is inspired by books including Object Thinking by David West and Elegant Objects by Yegor Bugayenko.

About

A reactive library using publisher-subscriber pattern with elegant object thinking in mind

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages