diff --git a/general/app/development/development-guide.md b/general/app/development/development-guide.md index 595498b68..3e1754c1c 100644 --- a/general/app/development/development-guide.md +++ b/general/app/development/development-guide.md @@ -168,9 +168,17 @@ Same as [core/classes/](#coreclasses), but containing functional utilities inste #### core/singletons/ {/* #coresingletons */} -This folder contains some core [Singletons](#singletons) and the logic to make Service Singletons. +This folder contains the logic to make [Service Singletons](#service-singletons). -Other than Pure Singletons, there are also some third-party services exposed through Service Singletons. However, service Singletons for application services should be declared in the same file where the service is defined. +There are also some third-party services exposed through Service Singletons. However, Service Singletons for application services should be declared in the same file where the service is defined. + +#### core/static/ {/* #corestatic */} + +This folder contains some core static classes. These utility classes are intended to be used throughout the whole application. + +These classes are intentionally implemented as static rather than Angular services. + +The rationale is that these classes provide stateless, low-level utility functions that have no dependencies on Angular or the application's dependency injection (DI) system. Since they are purely functional, there is no expectation that they will need to be substituted, extended, or mocked in tests. #### core/features/ {/* #corefeatures */} @@ -334,27 +342,11 @@ You can find an example of this pattern in `CoreUserParticipantsPage`, where par ## Singletons {/* #singletons */} -The application relies heavily on the [Singleton design pattern](https://en.wikipedia.org/wiki/Singleton_pattern), and there are two types of singletons used throughout the application: Pure singletons and Service singletons. - -### Pure Singletons {/* #pure-singletons */} - -Pure singletons, or just "singletons", are plain Typescript classes whose functionality does not depend on the lifecycle of the application. These normally contain helper or utility methods that enhance existing apis or encapsulate reusable functionality. - -Their implementations usually consist of a collection of static methods (so technically they are not singletons, but in practice this is easier to work with). - -```typescript -export class CoreArray { - - static contains(items: T[], item: T): boolean { - return items.indexOf(item) !== -1; - } - -} -``` +The application relies heavily on the [Singleton design pattern](https://en.wikipedia.org/wiki/Singleton_pattern). Right now the app only uses the singleton pattern for Angular Services, what we call "Service Singleton". ### Service Singletons {/* #service-singletons */} -Service singletons are instances resolved from the [root application injector](https://angular.io/guide/hierarchical-dependency-injection). In contrast with pure singletons, these are defined as Angular services. In particular, these should be [singleton services](https://angular.io/guide/singleton-services). +Service Singletons are instances resolved from the [root application injector](https://angular.io/guide/hierarchical-dependency-injection). In contrast with pure singletons, these are defined as Angular services. In particular, these should be [singleton services](https://angular.io/guide/singleton-services). The motivation behind using this pattern to access service instances is improving the development experience (easier auto-imports) and delaying the instantiation of services until they are really needed. @@ -404,6 +396,70 @@ All the nomenclature can be a bit confusing, so let's do a recap: - Service Singleton: An instance of a Singleton Service. - Singleton Proxy: An object that relays method calls to a Service Singleton instance. +## Services {/* #services */} + +There are no strict rules for structuring services in the Moodle App. Different features may require different approaches, and it's important to prioritize readability and maintainability over following rigid patterns. + +As a general guideline, services should avoid becoming large mixed layers where UI logic, orchestration, and Web Service call details are all coupled together. + +### Common layering pattern {/* #common-service-layering-pattern */} + +A common pattern in the app is to have: + +- A base service with the core business logic for a feature (for example, `MyFeatureService`). +- An optional helper service on top for more advanced flows or UI-adjacent orchestration (for example, `MyFeatureHelperService`). +- An optional offline service to handle storing and retrieving information in the app database for offline usage (for example, `MyFeatureOfflineService`). In general, this is for data generated by the user in the app; cached Web Service responses are not handled by this service. +- An optional prefetch service to handle downloading Web Service information and files in a batch (for example, `MyFeaturePrefetchService`). For example, this is used when downloading activities in the app. +- An optional sync service to handle sending data generated in offline in the app back to the server (for example, `MyFeatureSyncService`). + +This pattern is not mandatory, but it is useful when it improves separation of concerns. + +When using this pattern: + +- The helper can depend on the base service, but the base service should not depend on the helper. +- The base service can depend on the offline service, but the offline service should not depend on the base service. + +### Web Service access layer {/* #web-service-access-layer */} + +For services that need to call Moodle Web Services, new code should separate business logic from WS access logic. + +For example: + +- `MyFeatureService` should expose the feature API used by components and pages. +- `MyFeatureWSService` should encapsulate Web Service call details. + +Components and pages should use `MyFeatureService`, not `MyFeatureWSService` directly. + +In this split: + +- `MyFeatureService` owns business rules and orchestration. +- `MyFeatureWSService` owns WS function names, parameter formatting, and request/response typing. + +### WS service method signature agreement {/* #ws-service-method-signature-agreement */} + +All methods in `*WSService` should use the same call signature shape: + +1. First parameter: one object containing all Web Service params (both required and optional). It must be an object even if it only contains a single parameter. +2. Second parameter: one object containing call configuration options (like cache-related options). + +For example: + +```typescript +async getUser( + params: { userId: number; courseId?: number }, + wsOptions: CoreSitesCommonWSOptions = {}, +): Promise { + // ... +} +``` + +Using one consistent signature in WS services makes call sites easier to read and evolve, especially when optional parameters are introduced over time. It also makes all Web Service related signatures consistent. + +In some cases, this WS service can also contain methods to invalidate cached data for Web Service calls. These invalidate methods should follow a similar pattern as the Web Service call methods: + +1. First parameter: one object containing all the params needed to invalidate the information (both required and optional). It must be an object even if it only contains a single parameter. +2. Second parameter: the site ID to invalidate the information from (optional). + ## Database {/* #database */} Most of the persistent data in the application is stored in SQLite databases. In particular, there is one database for global app configuration, and one for each site. Reading and writing data is encapsulated in the `CoreDatabaseTable` class. Each table can be configured to use one of the following caching strategies: