dart-lang / language

Design of the Dart language
Other
2.6k stars 200 forks source link

Enhancing `library` and `part of` Declarations in Dart #3908

Open nathnaeld opened 1 week ago

nathnaeld commented 1 week ago

The current usage of library and part of declarations in Dart has certain limitations, particularly when using the library name as a part of, which is discouraged. The flutter_lints package suggests using URI or relative directory structures instead. However, this approach can be less descriptive and harder to manage, especially in larger projects with complex directory structures like Clean Architecture.

Problem:

  1. Lack of Clarity: Using URI or relative directories for part of is not as descriptive as using the library name. It makes it difficult to quickly understand the relationship between different .dart files.

  2. Difficulty in Management: Tracking and managing file relationships using URI or relative paths in large projects can be cumbersome. This is particularly problematic in architectural patterns emphasizing modularity and clear separation of concerns.

Proposed Changes:

  1. Lints Update for Declaring Libraries: Disallow using relative paths in part of directives and allow capitalized library name (e.g., library Product).

  2. Enhanced Syntax for Library Calls: Allow part of declarations to use updated library name syntax with distinct color highlighting support for the library name. Possible syntaxes include:

    • Product.CatalogPage() (dot notation)

    • Product:CatalogPage() (colon notation)

    • Product::CatalogPage() (double colon notation, similar to C++ namespace)

Benefits:

  1. Improved Readability: The new syntax immediately clarifies which library a part belongs to, improving code readability and maintainability.

  2. Better Project Organization: Helps organize and manage large projects by making the relationships between files more explicit and easier to follow.

  3. Reduced Redundancy: Eliminates the need for redundant naming conventions and shorten class names, such as:

    • ProductCatalogPage to Product::CatalogPage
    • ProductDetailsPage to Product::DetailsPage
    • ShopCatalogPage to Shop::CatalogPage
    • ShopDetailsPage to Shop::DetailsPage

Example:

product.dart

library Product;

part 'presentation/pages/catalog_page.dart';
part 'presentation/pages/details_page.dart';

catalog_page.dart

part of Product;

class CatalogPage extends StatelessWidget {
    // some code
}

// alternate syntax
class Product::CatalogPage extends StatelessWidget {
    // some code
}

main.dart

void main() {
    Product.CatalogPage(); // dot notation
    Product:CatalogPage(); // colon notation
    Product::CatalogPage(); // double colon notation
}

Any insights, concerns, or suggestions for improvement would be greatly appreciated.

lrhn commented 1 week ago

As I read it, you're asking for libraries to have a global namespace, where each library has a unique name. And there is no suggestion for how to map from library name to actual library location. That's the biggest issue.

In main.dart you didn't write an import, so I assume referring to Product automatically imports the Product library. I'll assume that only works within the same package, because a global cross-package namespace for libraries is not going to work. Or scale. Referring to libraries by a single-identifier name is too restrictive, if it means there can't be two libraries with the same name in the same program, even if they are from different packages.

Implicit imports makes dependency control harder. You can't easily see which libraries a library depends on, and you can easily add an undesired dependency simply by writing the wrong name in the wrong file.

Or if it assumes that all libraries are just available to all other libraries, it makes modular compilation impossible. Explicit dependency control makes it possible to control the sizes of mutually dependent library clusters, which is the unit of modular compilation.

immediately clarifies which library a part belongs to

I'd say it doesn't. It specifies a name, but if names are not globally unique (they aren't) then it doesn't actually say much. You can give your library a name today by importing it with a prefix:

import "package:my_package/product.dart" as Product; // ignore: ... something ...

The URI uniquely and globally defines the library.

  • Lack of Clarity: Using URI or relative directories for part of is not as descriptive as using the library name. It makes it difficult to quickly understand the relationship between different .dart files.

I disagree. Using the library URI is very descriptive because it uniquely defines the library and its relation to the project. The URI includes the path-position in the the larger project, which is presumably chosen to be in accordance with whatever architectural pattern you use to manage the project.

  • Difficulty in Management: Tracking and managing file relationships using URI or relative paths in large projects can be cumbersome. This is particularly problematic in architectural patterns emphasizing modularity and clear separation of concerns.

Relative paths can be annoying to update if you move files. Luckily the IDE will do that for you. Relative paths are also signals of the relation between the files and the file it relates to.

And for part files and their part of, only that author of the library needs to look at that, and they should know which file is the library file, and which are part files. If you choose to name them project.dart and project_subfeature_part.dart, project_substructure_part.dart, etc., then it's pretty clear what they are. If you put all parts inside a project_parts/ sub-directory, that's also fine. You get to define the path structure, something you can't do with a flat namespace. If anything, using URIs gives you more tools for managing and controlling the architecture of the project.

A practical reason for part of "path_to/library.dart"; is that any Dart tool pointed to the part file can find the parent file. If it just said part of Project;, you'd have to scan the entire package to find a library Project;. (And hope you only find one.)

For the benefits;

  1. Improved Readability: The new syntax immediately clarifies which library a part belongs to, improving code readability and maintainability.

Here, unsurprisingly, I disagree again. A library is identified by its URI, nothing clarifies which library is being referred better than that URI.

  1. Better Project Organization: Helps organize and manage large projects by making the relationships between files more explicit and easier to follow.

Don't see how. If anything, it removes the information about relationship between files.

  1. Reduced Redundancy: Eliminates the need for redundant naming conventions and shorten class names, such as:

    • ProductCatalogPage to Product::CatalogPage
    • ProductDetailsPage to Product::DetailsPage
    • ShopCatalogPage to Shop::CatalogPage
    • ShopDetailsPage to Shop::DetailsPage

That can be done today with import prefixes. You have to name the prefix where you import it, not where the library is declared, but that's also how it avoids name clashes.

I'm not seeing a concrete problem that this is solving. Can you elaborate on what concrete problems you've had with tracking and managing file relationships in large projects?