Modificadores de clase para mantenedores de API
Cómo usar los modificadores de clase añadidos en Dart 3.0 para hacer que la API de tu paquete sea más robusta y mantenible.
Dart 3.0 añade unos cuantos modificadores nuevos que puedes colocar en declaraciones de clase y mixin. Si eres el autor de un paquete de biblioteca, estos modificadores te dan más control sobre lo que los usuarios pueden hacer con los tipos que exporta tu paquete. Esto puede facilitar la evolución de tu paquete y hacer que sea más fácil saber si un cambio en tu código podría romper a los usuarios.
Dart 3.0 también incluye un cambio disruptivo en torno al uso de clases como mixins. Es posible que este cambio no rompa tu clase, pero podría romper a los usuarios de tu clase.
Esta guía te acompaña a través de estos cambios para que sepas cómo usar los nuevos modificadores y cómo afectan a los usuarios de tus bibliotecas.
El modificador mixin en clases
#
El modificador más importante a tener en cuenta es mixin.
Las versiones del lenguaje anteriores a Dart 3.0 permiten usar cualquier clase como mixin
en la cláusula with de otra clase, A MENOS QUE la clase:
- Declara cualquier constructor que no sea de tipo factory.
- Extienda cualquier clase que no sea
Object.
Esto hace que sea fácil romper accidentalmente el código de otra persona,
al añadir un constructor o una cláusula extends a una clase
sin darse cuenta de que otros la están utilizando en una cláusula with.
Dart 3.0 ya no permite usar clases como mixins por defecto.
En su lugar, debes optar explícitamente por ese comportamiento declarando una mixin class:
mixin class Both {}
class UseAsMixin with Both {}
class UseAsSuperclass extends Both {}
Si actualizas tu paquete a Dart 3.0 y no cambias nada de tu código, es posible que no veas ningún error. Pero podrías romper inadvertidamente a los usuarios de tu paquete si estaban usando tus clases como mixins.
Migrar clases como mixins
#Si la clase tiene un constructor que no es de tipo factory, una cláusula extends o una cláusula with,
entonces de por sí ya no se puede usar como mixin.
El comportamiento no cambiará con Dart 3.0;
no hay nada de qué preocuparse ni nada que debas hacer.
En la práctica, esto describe aproximadamente el 90% de las clases existentes. Para las clases restantes que se pueden usar como mixins, tienes que decidir qué quieres soportar.
Aquí tienes algunas preguntas para ayudarte a decidir. La primera es pragmática:
- ¿Quieres arriesgarte a romper a algún usuario? Si la respuesta es un rotundo "no",
entonces coloca
mixinantes de todas y cada una de las clases que podrían usarse como mixin. Esto preserva exactamente el comportamiento existente de tu API.
Por otro lado, si quieres aprovechar esta oportunidad para
repensar las posibilidades que ofrece tu API,
entonces es posible que prefieras no convertirla en una mixin class.
Considera estas dos preguntas de diseño:
-
¿Quieres que los usuarios puedan construir instancias de ella directamente? En otras palabras, ¿la clase no es abstracta deliberadamente?
-
¿Quieres que la gente pueda usar la declaración como un mixin? En otras palabras, ¿quieres que puedan usarla en cláusulas
with?
Si la respuesta a ambas es "sí", entonces conviértela en una mixin class. Si la respuesta a la segunda es "no", simplemente déjala como una clase. Si la respuesta a la primera is "no" y la segunda es "sí", cámbiala de clase a una declaración mixin.
Las dos últimas opciones, dejarla como clase o convertirla en un mixin puro, son cambios disruptivos en la API. Si haces esto, querrás aumentar la versión mayor de tu paquete.
Otros modificadores opcionales
#El manejo de clases como mixins es el único cambio crítico en Dart 3.0 que afecta a la API de tu paquete. Una vez que hayas llegado hasta aquí, puedes detenerte si no quieres realizar otros cambios en lo que tu paquete permite hacer a los usuarios.
Ten en cuenta que si continúas y usas cualquiera de los modificadores descritos a continuación, es potencialmente un cambio disruptivo en la API de tu paquete que requerirá un incremento de la versión mayor.
El modificador interface
#
Dart no tiene una sintaxis separada para declarar interfaces puras. En su lugar, declaras una clase abstracta que casualmente contiene solo métodos abstractos. Cuando un usuario ve esa clase en la API de tu paquete, es posible que no sepa si contiene código que puede reutilizar extendiendo la clase, o si en su lugar está pensada para ser utilizada como una interfaz.
Puedes aclarar eso colocando el modificador interface
en la clase.
Eso permite que la clase se use en una cláusula implements,
pero evita que se use en extends.
Incluso cuando la clase sí tiene métodos no abstractos, es posible que quieras evitar que los usuarios la extiendan. La herencia es uno de los tipos de acoplamiento más potentes en el software, porque permite la reutilización de código. Pero ese acoplamiento también es peligroso y frágil. Cuando la herencia cruza los límites del paquete, puede ser difícil evolucionar la superclase sin romper las subclases.
Marcar la clase como interface permite a los usuarios construirla (a menos que también esté marcada como
abstract)
e implementar la interfaz de la clase,
pero les impide reutilizar cualquier parte de su código.
Cuando una clase está marcada como interface, la restricción se puede ignorar dentro
de la biblioteca donde se declara la clase.
Dentro de la biblioteca, eres libre de extenderla, ya que todo es tu código
y presumiblemente sabes lo que haces.
La restricción se aplica a otros paquetes,
e incluso a otras bibliotecas dentro de tu propio paquete.
El modificador base
#
El modificador base
es en cierto modo lo opuesto a interface.
Te permite usar la clase en una cláusula extends,
o usar un mixin o mixin class en una cláusula with.
Sin embargo, no permite que el código fuera de la biblioteca de la clase
use la clase o mixin en una cláusula implements.
Esto asegura que cada objeto que sea una instancia de la interfaz de tu clase o mixin herede tu implementación real. En particular, esto significa que cada instancia incluirá todos los miembros privados que declare tu clase o mixin. Esto puede ayudar a prevenir errores en tiempo de ejecución que de otro modo podrían ocurrir.
Considera esta biblioteca:
class A {
void _privateMethod() {
print('I inherited from A');
}
}
void callPrivateMethod(A a) {
a._privateMethod();
}
Este código parece estar bien por sí mismo, pero no hay nada que impida a un usuario crear otra biblioteca como esta:
import 'a.dart';
class B implements A {
// No implementation of _privateMethod()!
}
main() {
callPrivateMethod(B()); // Runtime exception!
}
Añadir el modificador base a la clase puede ayudar a prevenir estos errores en tiempo de ejecución.
Al igual que con interface, puedes ignorar esta restricción
en la misma biblioteca donde se declara la clase o mixin base.
Luego, se recordará a las subclases en la misma biblioteca
que implementen los métodos privados.
Pero ten en cuenta que la siguiente sección sí se aplica:
Transitividad base
#El objetivo de marcar una clase como base es garantizar que
cada instancia de ese tipo herede concretamente de ella.
Para mantener esto, la restricción base es "contagiosa".
Cada subtipo de un tipo marcado como base —directo o indirecto—
también debe evitar ser implementado.
Eso significa que debe estar marcado como base
(o final o sealed, de los cuales hablaremos a continuación).
Por lo tanto, aplicar base a un tipo requiere cierto cuidado.
Afecta no solo a lo que los usuarios pueden hacer con tu clase o mixin,
sino también a las posibilidades que sus subclases pueden ofrecer.
Una vez que has colocado base en un tipo, se prohíbe implementar
toda la jerarquía debajo de él.
Eso suena intenso, pero es como la mayoría de los otros lenguajes de programación siempre han funcionado. La mayoría no tiene interfaces implícitas en absoluto, por lo que cuando declaras una clase en Java, C# u otros lenguajes, efectivamente tienes la misma restricción.
El modificador final
#
Si deseas todas las restricciones tanto de interface como de base,
puedes marcar una clase o mixin class como final.
Esto evita que cualquier persona fuera de tu biblioteca cree
cualquier tipo de subtipo de ella:
no se puede usar en cláusulas implements, extends, with u
on.
Esto es lo más restrictivo para los usuarios de la clase.
Todo lo que pueden hacer es construirla (a menos que esté marcada como abstract).
A cambio, tú tienes la menor cantidad de restricciones como mantenedor de la clase.
Puedes añadir nuevos métodos, convertir constructores en constructores de tipo factory, etc.,
sin preocuparte por romper a ningún usuario derivado.
El modificador sealed
#
El último modificador, sealed, es especial.
Existe principalmente para habilitar la comprobación de exhaustividad
en la coincidencia de patrones.
Si un switch tiene casos para cada subtipo directo de un tipo marcado como sealed,
entonces el compilador sabe que el switch es exhaustivo.
sealed class Amigo {}
class Lucky extends Amigo {}
class Dusty extends Amigo {}
class Ned extends Amigo {}
String lastName(Amigo amigo) => switch (amigo) {
Lucky _ => 'Day',
Dusty _ => 'Bottoms',
Ned _ => 'Nederlander',
};
Este switch tiene un caso para cada uno de los subtipos de Amigo.
El compilador sabe que cada instancia de Amigo debe ser una instancia de uno
de esos subtipos, por lo que sabe que el switch es exhaustivo de forma segura y no
requiere ningún caso default final.
Para que esto sea sólido, el compilador impone dos restricciones:
-
La clase sealed no puede ser directamente construible por sí misma. De lo contrario, podrías tener una instancia de
Amigoque no sea instancia de ninguno de los subtipos. Por lo tanto, cada clasesealedes también implícitamenteabstract. -
Cada subtipo directo del tipo sealed debe estar en la misma biblioteca donde se declara el tipo sealed. De esta manera, el compilador puede encontrarlos todos. Sabe que no hay otros subtipos ocultos rondando por ahí que no coincidan con ninguno de los casos.
La segunda restricción es similar a final.
Al igual que final, significa que una clase marcada como sealed no se puede
extender, implementar o mezclar directamente fuera de la biblioteca donde está declarada.
Pero, a diferencia de base y final, no hay ninguna restricción transitiva:
sealed class Amigo {}
class Lucky extends Amigo {}
class Dusty extends Amigo {}
class Ned extends Amigo {}
// This is an error:
class Bad extends Amigo {}
// But these are both fine:
class OtherLucky extends Lucky {}
class OtherDusty implements Dusty {}
Por supuesto, si quieres que los subtipos de tu tipo sealed
también estén restringidos, puedes lograrlo marcándolos
usando interface, base, final o sealed.
sealed frente a final
#
Si tienes una clase de la cual no quieres que los usuarios puedan crear subtipos directamente,
¿cuándo deberías usar sealed frente a final?
Un par de reglas simples:
-
Si quieres que los usuarios puedan construir instancias de la clase directamente, entonces no puede usar
sealedya que los tipos sealed son implícitamente abstractos. -
Si la clase no tiene subtipos en tu biblioteca, entonces no tiene sentido usar
sealedya que no obtienes beneficios de la comprobación de exhaustividad.
De lo contrario, si la clase tiene algunos subtipos que tú defines,
entonces es probable que sealed sea lo que deseas.
Si los usuarios ven que la clase tiene algunos subtipos, es útil poder
manejar cada uno de ellos por separado como casos de switch
y que el compilador sepa que todo el tipo está cubierto.
Usar sealed significa que si más adelante añades otro subtipo a la biblioteca,
es un cambio disruptivo en la API.
Cuando aparece un nuevo subtipo,
todos esos switches existentes dejan de ser exhaustivos
porque no manejan el nuevo tipo.
Es exactamente como añadir un nuevo valor a un enum.
Esos errores de compilación por switches no exhaustivos son útiles para los usuarios porque llaman su atención sobre lugares en su código donde necesitarán manejar el nuevo tipo.
Pero sí significa que cada vez que añades un nuevo subtipo, es un cambio disruptivo.
Si quieres la libertad de añadir nuevos subtipos de forma que no sea disruptiva,
entonces es mejor marcar el supertipo usando final en lugar de sealed.
Eso significa que cuando un usuario haga un switch sobre un valor de ese supertipo,
incluso si tiene casos para todos los subtipos,
el compilador lo obligará a añadir otro caso default.
Ese caso default será el que se ejecute si añades más subtipos más adelante.
Resumen
#Como diseñador de APIs, estos nuevos modificadores te otorgan control sobre cómo los usuarios trabajan con tu código y, a la inversa, cómo puedes evolucionar tu código sin romper el de ellos.
Pero estas opciones traen consigo complejidad: ahora tienes más decisiones que tomar como diseñador de APIs. Además, dado que estas características son nuevas, todavía no sabemos cuáles serán las mejores prácticas. El ecosistema de cada lenguaje es diferente y tiene necesidades distintas.
Afortunadamente, no necesitas resolverlo todo a la vez.
Elegimos los valores por defecto deliberadamente para que, incluso si no haces nada,
tus clases conserven en su mayoría las mismas posibilidades que tenían antes de la versión 3.0.
Si solo quieres mantener tu API tal como estaba,
pon mixin en las clases que ya lo soportaban y listo.
Con el tiempo, a medida que percibas dónde deseas un control más detallado, puedes considerar aplicar algunos de los otros modificadores:
-
Usa
interfacepara evitar que los usuarios reutilicen el código de tu clase, permitiéndoles al mismo tiempo volver a implementar su interfaz. -
Usa
basepara exigir a los usuarios que reutilicen el código de tu clase y asegurar que cada instancia del tipo de tu clase sea una instancia de esa clase real o de una subclase. Usa
finalpara evitar por completo que una clase sea extendida.Usa
sealedpara optar por la comprobación de exhaustividad en una familia de subtipos.
Cuando lo hagas, incrementa la versión mayor al publicar tu paquete, ya que todos estos modificadores implican restricciones que son cambios disruptivos.
A menos que se indique lo contrario, la documentación en este sitio refleja Dart 3.12.2. Página actualizada por última vez el 2026-05-15. Ver fuente oreportar un problema.