Skip to content

Commit 9283386

Browse files
committed
Revise field-accessors
1 parent fc9354f commit 9283386

2 files changed

Lines changed: 25 additions & 36 deletions

File tree

content/associated-types.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -491,9 +491,27 @@ impl<Context> AuthTokenTypeProvider<Context> for UseStringAuthToken {
491491

492492
## Comparison to Newtype Pattern
493493

494-
Abstract types serve as an alternative to the newtype pattern. Compared to the newtype pattern, we can use plain `String` values directly, without wrapping them in a newtype struct. Contrary to common wisdom, in CGP, we place less emphasis on wrapping every domain type in a newtype. This is particularly true when most of the application is written in a context-generic style. The rationale is that abstract types and their accompanying interfaces already fulfill the role of newtypes by encapsulating and "protecting" raw values, reducing the need for additional wrapping.
494+
Let's talk about abstract types and how they compare to the well-known newtype pattern in Rust. Abstract types provide a compelling alternative.
495495

496-
Ultimately, there is no right or wrong whether one should use abstract types, new types, or both together. It is up to your own preference, experience, and requirements, to decide which approach is best suited for you. Just take note that abstract types will be a commonly used pattern in CGP, particularly in this book.
496+
A key difference is that abstract types often allow you to work directly with plain, convenient types like `String`, whereas the newtype pattern typically requires wrapping your value within a dedicated struct.
497+
498+
Within the CGP philosophy, we lean towards a different approach than the common advice to strictly newtype *every* domain concept. This is particularly true when you're writing highly context-generic code.
499+
500+
The reason is simple: abstract types, defined by traits that specify their required behavior and structure, effectively provide the same level of encapsulation and type safety that newtypes offer. They protect the underlying raw values through their defined interfaces, often making the extra layer of newtype wrapping unnecessary boilerplate.
501+
502+
The choice between abstract types, newtypes, or using both together isn't about a rigid "right" or "wrong." It's a flexible decision based on your specific project requirements, team conventions, and personal preference – choosing the best tool for the job at hand.
503+
504+
Just keep in mind that abstract types are a core and widely used pattern in the CGP ecosystem, and you'll see them frequently demonstrated throughout this book.
505+
506+
## Comparison to Newtype Pattern
507+
508+
Abstract types serve as an alternative to the newtype pattern. Compared to the newtype pattern, we can use plain `String` values directly, without wrapping them in a newtype struct.
509+
510+
Contrary to common wisdom, in CGP, we place less emphasis on wrapping every domain type in a newtype. This is particularly true when most of the application is written in a context-generic style. The reason is simple: abstract types, when used within generic code, effectively provide the same level of encapsulation and type safety that newtypes offer. They protect the underlying raw values through their defined interfaces, often making the extra layer of newtype wrapping unnecessary boilerplate.
511+
512+
The choice between abstract types, newtypes, or using both together isn't about a rigid "right" or "wrong." It's a flexible decision based on your specific project requirements, team conventions, and personal preference – choosing the best tool for the job at hand.
513+
514+
Just keep in mind that abstract types are a core and widely used pattern in the CGP ecosystem, and you'll see them frequently demonstrated throughout this book.
497515

498516
## The `UseType` Pattern
499517

content/field-accessors.md

Lines changed: 5 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -312,9 +312,9 @@ where
312312

313313
In this updated code, we use the [`bearer_auth`](https://docs.rs/reqwest/latest/reqwest/blocking/struct.RequestBuilder.html#method.bearer_auth) method from the `reqwest` library to include the authentication token in the HTTP header. In this case, the provider only requires that `Context::AuthToken` implement the `Display` trait, allowing it to work with custom `AuthToken` types, not limited to `String`.
314314

315-
## Accessor Method Minimalism
315+
## Traits with Multiple Getter Methods
316316

317-
When creating providers like `ReadMessageFromApi`, which often need to use both `HasApiBaseUrl` and `HasAuthToken`, it might seem tempting to combine these two traits into a single one, containing both accessor methods:
317+
When creating providers like `ReadMessageFromApi`, which often need to use both `HasApiBaseUrl` and `HasAuthToken`, an alternative design would be to combine these two traits into a single one, containing both accessor methods:
318318

319319
```rust
320320
# extern crate cgp;
@@ -334,41 +334,12 @@ pub trait HasApiClientFields: HasAuthTokenType {
334334
}
335335
```
336336

337-
While this approach works, it introduces unnecessary coupling between the `api_base_url` and `auth_token` fields. If a provider only requires `api_base_url` but not `auth_token`, it would still need to include the unnecessary `auth_token` dependency. Additionally, this design prevents us from implementing separate providers that could provide the `api_base_url` and `auth_token` fields independently, each with its own logic.
338-
339-
This coupling also makes future changes more challenging. For example, if we switch to a different authentication method, like public key cryptography, we would need to remove the auth_token method and replace it with a new one. This change would affect all code dependent on `HasApiClientFields`. Instead, it's much easier to add a new getter trait and gradually transition providers to the new trait while keeping the old one intact.
340-
341-
As applications grow in complexity, it’s common to need many accessor methods. A trait like `HasApiClientFields`, with dozens of methods, could quickly become a bottleneck, making the application harder to evolve. Moreover, it's often unclear upfront which accessor methods are related, and trying to theorize about logical groupings can be a distraction.
342-
343-
From real-world experience using CGP, we’ve found that defining one accessor method per trait is the most effective approach for rapidly iterating on application development. This method simplifies the process of adding or removing accessor methods and reduces cognitive overload, as developers don’t need to spend time deciding or debating which method should belong to which trait. Over time, it's almost inevitable that a multi-method accessor trait will need to be broken up as some methods become irrelevant to parts of the application.
344-
345-
In future chapters, we’ll explore how breaking accessor methods down into individual traits can enable new design patterns that work well with single-method traits.
346-
347-
However, CGP doesn’t prevent developers from creating accessor traits with multiple methods and types. For those new to CGP, it might feel more comfortable to define non-minimal traits, as this has been a mainstream practice in programming for decades. So, feel free to experiment and include as many types and methods in a CGP trait as you prefer.
348337

349-
As an alternative to defining multiple accessor methods, you could define an inner struct containing all the common fields you’ll use across most providers:
350-
351-
```rust
352-
# extern crate cgp;
353-
#
354-
# use cgp::prelude::*;
355-
#
356-
pub struct ApiClientFields {
357-
pub api_base_url: String,
358-
pub auth_token: String,
359-
}
360-
361-
#[cgp_component(ApiClientFieldsGetter)]
362-
pub trait HasApiClientFields {
363-
fn api_client_fields(&self) -> &ApiClientFields;
364-
}
365-
```
366-
367-
In this example, we define an `ApiClientFields` struct that groups both the `api_base_url` and `auth_token` fields. The `HasApiClientFields` trait now only needs one getter method, returning the `ApiClientFields` struct.
338+
While this approach works, it introduces unnecessary coupling between the `api_base_url` and `auth_token` fields. If a provider only requires `api_base_url` but not `auth_token`, it would still need to include the unnecessary `auth_token` dependency. Additionally, this design prevents us from implementing separate providers that could provide the `api_base_url` and `auth_token` fields independently, each with its own logic.
368339

369-
One downside to this approach is that we can no longer use abstract types within the struct. For instance, the `ApiClientFields` struct stores the `auth_token` as a concrete `String` rather than as an abstract `AuthToken` type. As a result, this approach works best when your providers don’t rely on abstract types for their fields.
340+
Furthermore, traits that contain only one method can benefit from the [`UseField`](./use-field-pattern.md) pattern that will be introduced later, which helps to simplify the boilerplate required to implement accessor traits, as well as allowing reusable accessor providers to be defined.
370341

371-
For the purposes of this book, we will continue to use minimal traits, as this encourages best practices and provides readers with a clear reference for idiomatic CGP usage.
342+
Ultimately, CGP does not prevent you from defining multiple accessor methods into one trait, or even mix the trait with other items such as associated types or non-getter methods. It is your own decision of how to design CGP traits. Just keep in mind that it is common in CGP to see accessor traits that each contain only one getter method.
372343

373344
## Implementing Accessor Providers
374345

0 commit comments

Comments
 (0)