Define relaciones con clases
Aprende sobre programación orientada a objetos en Dart, incluyendo clases abstractas, herencia, overrides y enums. Construye un framework para aplicaciones de CLI bien estructuradas.
En este capítulo, explorarás el poder de la programación orientada a objetos (OOP) en Dart. Aprenderás a crear clases y definir relaciones entre ellas, incluyendo la herencia y las clases abstractas. También construirás las bases para crear aplicaciones de CLI bien estructuradas.
Qué lograrás
Prerrequisitos
#Antes de comenzar este capítulo, asegúrate de:
- Haber completado el Capítulo 4 y tener un
entorno de desarrollo de Dart en funcionamiento con el proyecto
dartpedia. - Estar familiarizado con conceptos básicos de programación como variables, funciones y flujo de control.
- Entender los conceptos de paquetes y librerías en Dart.
Tareas
#Una interfaz de línea de comandos (CLI) se define por los comandos, opciones y argumentos que un usuario puede escribir en su terminal.
Al final de esta lección, habrás construido un framework que puede entender un comando como este:
$ dartpedia help --verbose --command=search
Aquí tienes un desglose de cada parte:
dartpedia: Este es el ejecutable, el nombre de tu aplicación.help: Este es un comando, una acción que deseas que realice la aplicación.--verbose: Esta es una bandera (flag) (un tipo de opción que no toma un valor), que modifica el comportamiento del comando.--command=search: Esta es una opción que toma un valor. Aquí, laopciónse llamacommandy su valor essearch.
Las clases y la lógica que construyas en las siguientes tareas crean la base para parsear y ejecutar comandos como este.
Tarea 1: Definir la jerarquía de argumentos
#Primero, definirás una clase Argument,
una clase Option y una clase Command,
estableciendo una relación de herencia.
-
Crea el archivo
command_runner/lib/src/arguments.dart. Este archivo contendrá las definiciones para tus clasesArgument,Option,CommandyArgResultsclasses. -
Define un
enumllamadoOptionType.command_runner/lib/src/arguments.dartdartenum OptionType { flag, option }Este
enumrepresenta el tipo de opción, que puede ser unabandera(flag) (una opción booleana) o unaopciónnormal (una opción que toma un valor). Los enums son útiles para representar un conjunto fijo de valores posibles. -
Define una
clase abstractallamadaArgument.Comienza definiendo la estructura básica de tu clase
Argument. La declararás comoabstract, lo que significa que sirve como una clase base que otras clases pueden extender, pero no se puede instanciarInstanciarCrear una instancia concreta (objeto) de una clase. Más información por sí misma.Debajo del
enumque acabas de agregar, pega el siguiente código:command_runner/lib/src/arguments.dartdart// Paste this new class below the enum you added abstract class Argument { String get name; String? get help; // In the case of flags, the default value is a bool. // In other options and commands, the default value is a String. // NB: flags are just Option objects that don't take arguments Object? get defaultValue; String? get valueHelp; String get usage; }namees una cadena de texto (String) que identifica de manera única el argumento.helpes una cadena de texto (String) opcional que proporciona una descripción.defaultValuees de tipoObject?porque puede ser un booleano (bool) (para banderas) o unString.valueHelpes una cadena de texto (String) opcional para dar una pista sobre el valor esperado.- El método getterGetterUn método especial que proporciona acceso de lectura a las propiedades de un objeto. Más información de
usageproporcionará una cadena que muestra cómo usar el argumento.
Con la clase
Argumentcompletamente definida, tienes una interfaz común para todos los tipos de argumentos de línea de comandos. A continuación, construirás sobre esto definiendoOption, un tipo específico de argumento que extiendeArgument. -
Define una clase llamada
Optionque extienda (extends) deArgument.La clase
Optionrepresentará opciones de línea de comandos como--verboseo--output=file.txt. Heredará de tu claseArgument.Agrega la siguiente clase
Optionen la parte inferior de tu archivo:command_runner/lib/src/arguments.dartdartclass Option extends Argument { Option( this.name, { required this.type, this.help, this.abbr, this.defaultValue, this.valueHelp, }); @override final String name; final OptionType type; @override final String? help; final String? abbr; @override final Object? defaultValue; @override final String? valueHelp; @override String get usage { if (abbr != null) { return '-$abbr,--$name: $help'; } return '--$name: $help'; } }La palabra clave
extendsestablece la relación de herencia. La clase utiliza la anotación@overrideen sus propiedades y getters para indicar que está reemplazando los miembros de marcador de posición definidos enArgument.También añade
type(usando el enumOptionType) y unabbropcional para una forma abreviada de la opción. El getterusageestá implementado para proporcionar instrucciones claras al usuario.Con
Optioncompleto, tienes un tipo especializado de argumento. A continuación, definirás la claseCommand, otro tipo de argumento que representará las acciones principales que un usuario puede realizar en tu aplicación de CLI. -
Define una
clase abstractallamadaCommandque también extienda (extends) deArgument.La clase
Commandrepresentará una acción ejecutable. Dado que proporciona una plantilla para que la sigan otros comandos, la declararás comoabstract.command_runner/lib/src/arguments.dartdart// Add this class below the Option class abstract class Command extends Argument { // Properties and methods will go here }La palabra clave
abstractsignifica queCommandno se puede instanciar directamente. Sirve como una clase base para otras clases.Ahora, agrega las propiedades principales. Un comando necesita un
namey unadescription. También necesita una referencia de regreso alCommandRunnerque lo ejecuta.command_runner/lib/src/arguments.dartdartabstract class Command extends Argument { @override String get name; String get description; bool get requiresArgument => false; late CommandRunner runner; @override String? help; @override String? defaultValue; @override String? valueHelp; }La propiedad
runneres de tipoCommandRunner, que definirás más adelante encommand_runner_base.dart.Observa la palabra clave
late. Le dice a Dart que prometes inicializar esta variable antes de que se acceda a ella, lo que te permite declarar una variable no nulleable sin asignarla inmediatamente. Esto es útil cuando la inicialización de una variable depende de otros objetos (como cuando se agrega un comando a un runner).Para que Dart reconozca esta clase, debes importar el archivo que la define. Agrega la siguiente importación en la parte superior de
command_runner/lib/src/arguments.dart:dartimport '../command_runner.dart';A continuación, darás a los comandos su propio conjunto de opciones. Para evitar que otras partes de tu código modifiquen inesperadamente estas opciones, las guardarás en una lista privadaprivada de la libreríaAccesibilidad restringida a la [librería](#library) donde está definida. Saber más (
_options). En Dart, anteponer un guion bajo (_) al nombre de una variable o campo la hace privada de la librería.En lugar de permitir el acceso directo, expones las opciones a través de una vista de solo lectura no modificable (
UnmodifiableSetView). Este enfoque es una parte fundamental del encapsulamiento: la práctica de restringir el acceso directo al estado interno de una clase para evitar interferencias no deseadas.La clase
UnmodifiableSetViewes parte de la librería de colecciones principal de Dart. Para usarla, debes importar esa librería.Actualiza las importaciones en la parte superior de tu archivo para incluir
dart:collection:dartimport 'dart:collection'; // New import import '../command_runner.dart';Ahora, añade la lista
optionsy su getter a tu claseCommand:dartabstract class Command extends Argument { // ... existing properties ... @override String? valueHelp; // Add the following lines to the bottom of your Command class: final List<Option> _options = []; UnmodifiableSetView<Option> get options => UnmodifiableSetView(_options.toSet()); }Como
_optionses privado, el código externo no puede añadir opciones directamente. Para permitir que los comandos definan sus opciones, proporcionarás dos métodos de ayuda internos:addFlagyaddOption. Estos métodos instanciarán los objetosOptioncorrespondientes y los añadirán a la lista privada.dartabstract class Command extends Argument { // ... existing properties and getters ... UnmodifiableSetView<Option> get options => UnmodifiableSetView(_options.toSet()); // Add the following lines to the bottom of your Command class: // A flag is an [Option] that's treated as a boolean. void addFlag(String name, {String? help, String? abbr, String? valueHelp}) { _options.add( Option( name, help: help, abbr: abbr, defaultValue: false, valueHelp: valueHelp, type: OptionType.flag, ), ); } // An option is an [Option] that takes a value. void addOption( String name, { String? help, String? abbr, String? defaultValue, String? valueHelp, }) { _options.add( Option( name, help: help, abbr: abbr, defaultValue: defaultValue, valueHelp: valueHelp, type: OptionType.option, ), ); } }Finalmente, cada comando debe tener lógica para ejecutarse al ser llamado. Definirás un método abstracto
runque los comandos concretos deben implementar.Dado que un comando puede ser síncrono o asíncrono, su método
rundevuelve el tipoFutureOrdedart:async, lo que le permite devolver tanto un valor crudo como unFuture. Esta es tu última importación requerida.Actualiza las importaciones en la parte superior de tu archivo para incluir
dart:async:dartimport 'dart:async'; // New import import 'dart:collection'; import '../command_runner.dart';Ahora puedes añadir el método abstracto
runy proporcionar la implementación deusagepara completar la claseCommand.dartabstract class Command extends Argument { // ... existing properties, getters, and methods ... void addOption( String name, { String? help, String? abbr, String? defaultValue, String? valueHelp, }) { _options.add( Option( name, help: help, abbr: abbr, defaultValue: defaultValue, valueHelp: valueHelp, type: OptionType.option, ), ); } // Add the following lines to the bottom of your Command class: FutureOr<Object?> run(ArgResults args); @override String get usage { return '$name: $description'; } }run(ArgResults args): En este método abstracto es donde reside la lógica del comando. Las subclases concretas deben implementarlo.usage: Este getter proporciona una cadena de uso simple, combinando elnamey ladescriptiondel comando.
La clase
Commandahora proporciona una robusta base para todos los comandos de tu aplicación CLI. Con la jerarquía de clases establecida, estás listo para definirArgResultspara contener la entrada parseada. -
Define una clase llamada
ArgResults.command_runner/lib/src/arguments.dartdart// Add this class to the end of the file class ArgResults { Command? command; String? commandArg; Map<Option, Object?> options = {}; // Returns true if the flag exists. bool flag(String name) { // Only check flags, because we're sure that flags are booleans. for (var option in options.keys.where( (option) => option.type == OptionType.flag, )) { if (option.name == name) { return options[option] as bool; } } return false; } bool hasOption(String name) { return options.keys.any((option) => option.name == name); } ({Option option, Object? input}) getOption(String name) { var mapEntry = options.entries.firstWhere( (entry) => entry.key.name == name || entry.key.abbr == name, ); return (option: mapEntry.key, input: mapEntry.value); } }Esta clase representa los resultados del parseo de los argumentos de línea de comandos. Contiene el comando detectado, cualquier argumento para ese comando y un mapa de las opciones especificadas.
- El método
flag(): Itera sobre las claves del mapaoptions, filtrando aquellas dondetypeesOptionType.flag. Utiliza un casteo de tipo explícito (as bool) porque sabemos que las banderas son valores booleanos. - Tipos de registro (record types): El método
getOptiondevuelve un tipo de registro(option: ..., input: ...), lo que te permite agrupar múltiples valores sin necesidad de crear una clase completa.
Ahora has definido la estructura básica para manejar comandos, argumentos y opciones en tu aplicación de línea de comandos.
- El método
Tarea 2: Actualizar la clase CommandRunner
#A continuación, actualiza la clase CommandRunner para usar la nueva jerarquía de Argument.
Abre el archivo
command_runner/lib/src/command_runner_base.dart.-
Reemplaza la clase
CommandRunnerexistente con la siguiente:command_runner/lib/src/command_runner_base.dartdartimport 'dart:collection'; import 'dart:io'; import 'arguments.dart'; class CommandRunner { final Map<String, Command> _commands = <String, Command>{}; UnmodifiableSetView<Command> get commands => UnmodifiableSetView<Command>(<Command>{..._commands.values}); Future<void> run(List<String> input) async { final ArgResults results = parse(input); if (results.command != null) { Object? output = await results.command!.run(results); print(output.toString()); } } void addCommand(Command command) { // TODO: handle error (Commands can't have names that conflict) _commands[command.name] = command; command.runner = this; } ArgResults parse(List<String> input) { var results = ArgResults(); results.command = _commands[input.first]; return results; } // Returns usage for the executable only. // Should be overridden if you aren't using [HelpCommand] // or another means of printing usage. String get usage { final exeFile = Platform.script.path.split('/').last; return 'Usage: dart bin/$exeFile <command> [commandArg?] [...options?]'; } }Esta clase actualizada incorpora tu nueva estructura orientada a objetos. Utiliza el operador de propagación (spread operator) (
...) en el gettercommandspara desempaquetar los valores del mapa_commandsen un nuevo conjunto (set). Esto asegura que el valor devuelto sea una copia, evitando que el código externo modifique tus datos.En el método
run(),results.command!.run(...)usa el operador de aserción de no nulo (!) para decirle al analyzer de Dart que estás seguro de queresults.commandno es nulo. Es seguro aquí porque acabas de comprobar que no era nulo en la sentenciaifanterior.Aquí están los detalles clave de la implementación:
- Mapa
_commands: Un mapa privado que vincula nombres de comandos con sus instancias de objeto concretas deCommand. Sus valores se exponen a través de unUnmodifiableSetView. addCommand(): Registra un comando y asignaesterunner a la propiedadrunnerdel comando. Esto cumple la promesa de inicialización tardía (late) hecha anteriormente en la claseCommand.parse()yrun(): Evalúa la entrada del usuario, identifica elCommandcorrespondiente del mapa y usaawaitpara llamar al métodorun()del comando.
- Mapa
-
Abre
command_runner/lib/command_runner.darty añade las siguientes exportaciones:command_runner/lib/command_runner.dartdart/// Support for doing something awesome. /// /// More dartdocs go here. library; export 'src/arguments.dart'; export 'src/command_runner_base.dart'; export 'src/help_command.dart'; // TODO: Export any libraries intended for clients of this package.Esto hace que los archivos
arguments.dart,command_runner_base.dartyhelp_command.dartestén disponibles para otros paquetes que dependan decommand_runner.
Tarea 3: Crear un HelpCommand
#Crea un HelpCommand que extienda la clase Command e
imprima información de uso.
Crea el archivo
command_runner/lib/src/help_command.dart.-
Agrega el siguiente código a
command_runner/lib/src/help_command.dart:command_runner/lib/src/help_command.dartdartimport 'dart:async'; import 'arguments.dart'; // Prints program and argument usage. // // When given a command as an argument, it prints the usage of // that command only, including its options and other details. // When the flag 'verbose' is set, it prints options and details for all commands. // // This command isn't automatically added to CommandRunner instances. // Packages users should add it themselves with [CommandRunner.addCommand], // or create their own command that prints usage. class HelpCommand extends Command { HelpCommand() { addFlag( 'verbose', abbr: 'v', help: 'When true, this command will print each command and its options.', ); addOption( 'command', abbr: 'c', help: "When a command is passed as an argument, prints only that command's verbose usage.", ); } @override String get name => 'help'; @override String get description => 'Prints usage information to the command line.'; @override String? get help => 'Prints this usage information'; @override FutureOr<Object?> run(ArgResults args) async { var usage = runner.usage; for (var command in runner.commands) { usage += '\n ${command.usage}'; } return usage; } }La clase
HelpCommanddemuestra los beneficios de la herencia. Utiliza los métodos de su padre para configurar sus opciones, sobrescribe el método abstractoruny accede al estado derunnerpara generar el mensaje de uso.
Tarea 4: Actualizar cli.dart para usar el nuevo CommandRunner
#Modifica cli/bin/cli.dart para usar el nuevo CommandRunner y HelpCommand.
Abre el archivo
cli/bin/cli.dart.-
Reemplaza el código existente con el siguiente:
cli/bin/cli.dartdartimport 'package:command_runner/command_runner.dart'; const version = '0.0.1'; void main(List<String> arguments) { var commandRunner = CommandRunner()..addCommand(HelpCommand()); commandRunner.run(arguments); }Este código crea una instancia de
CommandRunner, le añade elHelpCommandusando una cascada de métodos (..addCommand) lo que te permite llamar a un método en un objeto directamente después de crearlo, y luego ejecuta el ejecutor de comandos (command runner) con los argumentos de línea de comandos.
Tarea 5: Ejecutar la aplicación
#Prueba los nuevos CommandRunner y HelpCommand.
Abre tu terminal y navega al directorio
cli.-
Ejecuta el comando
dart run bin/cli.dart help.Deberías ver la información de uso impresa en la consola:
bashUsage: dart bin/cli.dart <command> [commandArg?] [...options?] help: Prints usage information to the command line.Esto confirma que el
CommandRunnery elHelpCommandestán funcionando correctamente.
Revisión
#Qué lograste
Aquí tienes un resumen de lo que construiste y aprendiste en esta lección.Diseñaste y comprendiste clases abstractas
Creaste una clase abstracta Argument como una clase base que no se puede instanciar directamente. Las clases abstractas definen un contrato que las subclases deben cumplir, asegurando la consistencia en toda tu jerarquía de clases.
Extendiste clases padres y sobrescribiste métodos (overrides)
Usaste extends para crear las subclases Option y Command de la clase abstracta
Argument. Dentro de esas subclases, usaste @override para proporcionar implementaciones concretas de miembros abstractos.
Usaste enums para representar conjuntos fijos de valores
Definiste un enum OptionType para representar un conjunto fijo de valores. En Dart, los enums son seguros en cuanto al tipo (type-safe) y ayudan a evitar que se usen valores inválidos donde solo son válidas opciones específicas.
Continuaste construyendo tu framework de CLI
Aplicaste los principios de la programación orientada a objetos para implementar un framework robusto y extensible para parsear argumentos de línea de comandos.
Cuestionario
#Pon a prueba tu comprensión
1 / 3Option class, what is the purpose of the @override annotation?Crear un nuevo método que no existe en la clase padre.
No exactamente.
@overrideno crea nuevos métodos. Los nuevos métodos se crean simplemente definiéndolos en la clase.Indicar que un método es opcional.
No exactamente.
@overrideno afecta si los métodos son opcionales. Los parámetros opcionales utilizan una sintaxis completamente diferente.Proporcionar una implementación específica para un método o propiedad definido en una clase padre.
¡Correcto!
@overrideindica que estás proporcionando una implementación concreta para un miembro abstracto o reemplazando una implementación heredada.Hacer que una propiedad sea privada.
No exactamente.
La privacidad se indica mediante un guion bajo inicial (
_), no mediante anotaciones.@overridesirve para un propósito diferente relacionado con la herencia.
abstract class and a regular class in Dart?Una clase
abstractno puede tener ningún método.No exactamente.
Las clases abstractas pueden tener tanto métodos abstractos (sin implementación) como métodos concretos (con implementación).
Una clase
abstractsolo puede tener métodos privados.No exactamente.
Las clases abstractas pueden tener métodos con cualquier nivel de visibilidad. La palabra clave
abstractno restringe la visibilidad.No hay diferencia entre una clase
abstracty una clase normal.No exactamente.
Hay una diferencia significativa. Intenta escribir
var x = MyAbstractClass();y mira lo que sucede.Una clase
abstractno se puede instanciar directamente.¡Correcto!
Las clases abstractas sirven como plantillas que extienden otras clases. No puedes crear instancias de una clase abstracta directamente.
enum instead of a class or a set of constants?Cuando necesitas un tipo que solo puede ser uno de un conjunto fijo y conocido de valores.
¡Correcto!
Los enums son perfectos cuando tienes un conjunto cerrado de opciones, como
flagcontraoption, días de la semana o códigos de estado. El compilador asegura que manejes todos los casos.Cuando deseas crear múltiples instancias con diferentes propiedades.
No exactamente.
Para eso están las clases. Cada valor de enum es un singleton, solo hay una instancia para cada valor.
Cuando necesitas almacenar la entrada del usuario que podría ser cualquier cadena de texto.
No exactamente.
La entrada del usuario es impredecible y podría ser cualquier cosa. Los enums funcionan mejor cuando puedes listar cada valor posible por adelantado.
Cuando necesitas realizar operaciones aritméticas sobre los valores.
No exactamente.
Usa
intodoublepara la aritmética. Los enums representan categorías distintas, no valores numéricos (aunque sí tienen un índice).
Siguiente lección
#En la siguiente lección, aprenderás a
manejar errores y excepciones en tu código Dart.
Crearás una clase de excepción personalizada y
añadirás manejo de errores a tu CommandRunner para
hacer tu aplicación más robusta.
A menos que se indique lo contrario, la documentación de este sitio refleja Dart 3.12.2. Página actualizada por última vez el 09-04-2026. Ver fuente oreportar un problema.