Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
C# es un lenguaje de programación orientado a objetos. Los cuatro principios básicos de la programación orientada a objetos son:
- Abstracción Modelado de los atributos e interacciones pertinentes de las entidades como clases para definir una representación abstracta de un sistema.
- Encapsulación Ocultar el estado interno y la funcionalidad de un objeto y permitir solo el acceso a través de un conjunto público de funciones.
- Herencia Capacidad de crear nuevas abstracciones basadas en abstracciones existentes.
- Polimorfismo Capacidad de implementar propiedades o métodos heredados de diferentes maneras en varias abstracciones.
En el tutorial anterior, introducción a las clases, usted vio tanto abstracción como encapsulación. La BankAccount
clase proporcionó una abstracción para el concepto de una cuenta bancaria. Puede modificar su implementación sin afectar a ninguno de los código que usó la BankAccount
clase . Tanto las clases BankAccount
como Transaction
proporcionan encapsulación de los componentes necesarios para describir esos conceptos en el código.
En este tutorial, ampliará esa aplicación para usar la herencia y el polimorfismo para agregar nuevas características. También agregará características a la clase BankAccount
, aprovechando las técnicas de abstracción y encapsulación que aprendió en el tutorial anterior.
Crear diferentes tipos de cuentas
Después de compilar este programa, obtendrá solicitudes para agregarle características. Funciona bien en la situación en la que solo hay un tipo de cuenta bancaria. Con el tiempo, las necesidades cambian y se solicitan tipos de cuenta relacionados.
- Una cuenta de ganancias de intereses que acumula intereses al final de cada mes.
- Una línea de crédito que puede tener un saldo negativo, pero cuando hay un saldo, hay un cargo de interés cada mes.
- Una cuenta de tarjeta de regalo de prepago que comienza con un único depósito y solo se puede liquidar. Se puede rellenar una vez al principio de cada mes.
Todas estas cuentas diferentes son similares a la BankAccount
clase definida en el tutorial anterior. Puede copiar ese código, cambiar el nombre de las clases y realizar modificaciones. Esa técnica funcionaría a corto plazo, pero sería más trabajo a lo largo del tiempo. Los cambios se copiarían en todas las clases afectadas.
En su lugar, puede crear nuevos tipos de cuenta bancaria que hereden métodos y datos de la BankAccount
clase creada en el tutorial anterior. Estas nuevas clases pueden ampliar la BankAccount
clase con el comportamiento específico necesario para cada tipo:
public class InterestEarningAccount : BankAccount
{
}
public class LineOfCreditAccount : BankAccount
{
}
public class GiftCardAccount : BankAccount
{
}
Cada una de estas clases hereda el comportamiento compartido de su clase base compartida, la BankAccount
clase . Escriba las implementaciones para una funcionalidad nueva y diferente en cada una de las clases derivadas. Estas clases derivadas ya tienen todo el comportamiento definido en la BankAccount
clase .
Se recomienda crear cada nueva clase en un archivo de origen diferente. En Visual Studio, puede hacer clic con el botón derecho en el proyecto y seleccionar Agregar clase para agregar una nueva clase en un nuevo archivo. En Visual Studio Code, seleccione Archivo y , después, Nuevo para crear un nuevo archivo de código fuente. En cualquiera de las herramientas, asigne un nombre al archivo para que coincida con la clase : InterestEarningAccount.cs, LineOfCreditAccount.cs y GiftCardAccount.cs.
Al crear las clases como se muestra en el ejemplo anterior, encontrará que ninguna de las clases derivadas se compila. Un constructor es responsable de inicializar un objeto. Un constructor de clase derivada debe inicializar la clase derivada y proporcionar instrucciones sobre cómo inicializar el objeto de clase base incluido en la clase derivada. Normalmente, la inicialización adecuada se produce sin código adicional. La BankAccount
clase declara un constructor público con la siguiente firma:
public BankAccount(string name, decimal initialBalance)
El compilador no genera un constructor predeterminado al definir un constructor usted mismo. Esto significa que cada clase derivada debe llamar explícitamente a este constructor. Declara un constructor que puede pasar argumentos al constructor de clase base. El código siguiente muestra el constructor para :InterestEarningAccount
public InterestEarningAccount(string name, decimal initialBalance) : base(name, initialBalance)
{
}
Los parámetros de este nuevo constructor coinciden con el tipo de parámetro y los nombres del constructor de clase base. Usas la sintaxis : base()
para indicar una llamada a un constructor de clase base. Algunas clases definen varios constructores y esta sintaxis le permite elegir a qué constructor de clase base se llama. Una vez que haya actualizado los constructores, puede desarrollar el código para cada una de las clases derivadas. Los requisitos para las nuevas clases se pueden indicar de la siguiente manera:
- Una cuenta de ganancias de intereses:
- obtendrá un crédito del 2 % del saldo a final de mes.
- Una línea de crédito:
- Puede tener un saldo negativo, pero no ser mayor en valor absoluto que el límite de crédito.
- Generará un cargo por intereses cada mes en el que el saldo final del mes no sea 0.
- Incurrirá en un cargo por cada retiro que supere el límite de crédito.
- Una cuenta de tarjeta de regalo:
- Se puede rellenar con una cantidad especificada una vez al mes, el último día del mes.
Puede ver que los tres tipos de cuenta tienen una acción que tiene lugar al final de cada mes. Sin embargo, cada tipo de cuenta realiza tareas diferentes. El polimorfismo se usa para implementar este código. Cree un único virtual
método en la BankAccount
clase :
public virtual void PerformMonthEndTransactions() { }
El código anterior muestra cómo se usa la virtual
palabra clave para declarar un método en la clase base para la que una clase derivada puede proporcionar una implementación diferente. Un virtual
método es un método en el que cualquier clase derivada puede optar por volver a implementar. Las clases derivadas usan la override
palabra clave para definir la nueva implementación. Normalmente, se hace referencia a esto como "invalidación de la implementación de la clase base". La virtual
palabra clave especifica que las clases derivadas pueden invalidar el comportamiento. También puede declarar abstract
métodos en los que las clases derivadas deben invalidar el comportamiento. La clase base no proporciona una implementación para un abstract
método . A continuación, debe definir la implementación de dos de las nuevas clases que ha creado. Comience con :InterestEarningAccount
public override void PerformMonthEndTransactions()
{
if (Balance > 500m)
{
decimal interest = Balance * 0.02m;
MakeDeposit(interest, DateTime.Now, "apply monthly interest");
}
}
Agregue el código siguiente al LineOfCreditAccount
. El código niega el saldo para calcular un cargo de interés positivo que se retira de la cuenta:
public override void PerformMonthEndTransactions()
{
if (Balance < 0)
{
// Negate the balance to get a positive interest charge:
decimal interest = -Balance * 0.07m;
MakeWithdrawal(interest, DateTime.Now, "Charge monthly interest");
}
}
La GiftCardAccount
clase necesita dos cambios para implementar su funcionalidad de fin de mes. En primer lugar, modifique el constructor para incluir una cantidad opcional para agregar cada mes:
private readonly decimal _monthlyDeposit = 0m;
public GiftCardAccount(string name, decimal initialBalance, decimal monthlyDeposit = 0) : base(name, initialBalance)
=> _monthlyDeposit = monthlyDeposit;
El constructor proporciona un valor predeterminado para el valor de monthlyDeposit
, por lo que los usuarios pueden omitir 0
para no realizar un depósito mensual. A continuación, invalide el PerformMonthEndTransactions
método para agregar el depósito mensual, si se estableció en un valor distinto de cero en el constructor:
public override void PerformMonthEndTransactions()
{
if (_monthlyDeposit != 0)
{
MakeDeposit(_monthlyDeposit, DateTime.Now, "Add monthly deposit");
}
}
La invalidación aplica el conjunto de depósitos mensual en el constructor. Agregue el código siguiente al Main
método para probar estos cambios para GiftCardAccount
y :InterestEarningAccount
var giftCard = new GiftCardAccount("gift card", 100, 50);
giftCard.MakeWithdrawal(20, DateTime.Now, "get expensive coffee");
giftCard.MakeWithdrawal(50, DateTime.Now, "buy groceries");
giftCard.PerformMonthEndTransactions();
// can make additional deposits:
giftCard.MakeDeposit(27.50m, DateTime.Now, "add some additional spending money");
Console.WriteLine(giftCard.GetAccountHistory());
var savings = new InterestEarningAccount("savings account", 10000);
savings.MakeDeposit(750, DateTime.Now, "save some money");
savings.MakeDeposit(1250, DateTime.Now, "Add more savings");
savings.MakeWithdrawal(250, DateTime.Now, "Needed to pay monthly bills");
savings.PerformMonthEndTransactions();
Console.WriteLine(savings.GetAccountHistory());
Compruebe los resultados. Ahora, agregue un conjunto similar de código de prueba para :LineOfCreditAccount
var lineOfCredit = new LineOfCreditAccount("line of credit", 0);
// How much is too much to borrow?
lineOfCredit.MakeWithdrawal(1000m, DateTime.Now, "Take out monthly advance");
lineOfCredit.MakeDeposit(50m, DateTime.Now, "Pay back small amount");
lineOfCredit.MakeWithdrawal(5000m, DateTime.Now, "Emergency funds for repairs");
lineOfCredit.MakeDeposit(150m, DateTime.Now, "Partial restoration on repairs");
lineOfCredit.PerformMonthEndTransactions();
Console.WriteLine(lineOfCredit.GetAccountHistory());
Al agregar el código anterior y ejecutar el programa, verá algo parecido al siguiente error:
Unhandled exception. System.ArgumentOutOfRangeException: Amount of deposit must be positive (Parameter 'amount')
at OOProgramming.BankAccount.MakeDeposit(Decimal amount, DateTime date, String note) in BankAccount.cs:line 42
at OOProgramming.BankAccount..ctor(String name, Decimal initialBalance) in BankAccount.cs:line 31
at OOProgramming.LineOfCreditAccount..ctor(String name, Decimal initialBalance) in LineOfCreditAccount.cs:line 9
at OOProgramming.Program.Main(String[] args) in Program.cs:line 29
Nota:
La salida real incluye la ruta de acceso completa a la carpeta con el proyecto. Los nombres de carpeta se omiten para mayor brevedad. Además, dependiendo del formato de código, los números de línea pueden ser ligeramente diferentes.
Este código produce un error porque BankAccount
supone que el saldo inicial debe ser mayor que 0. Otra suposición incorporada en la BankAccount
clase es que el saldo no puede ser negativo. Lo que sucede que es se rechazan las retiradas que provocan un descubierto en la cuenta. Ambas suposiciones deben cambiar. La línea de cuenta de crédito comienza en 0 y, por lo general, tendrá un saldo negativo. Además, si un cliente toma demasiado dinero, incurre en una cuota. La transacción se acepta, solo cuesta más. La primera regla se puede implementar agregando un argumento opcional al BankAccount
constructor que especifica el saldo mínimo. El valor predeterminado es 0
. La segunda regla requiere un mecanismo que permita a las clases derivadas modificar el algoritmo predeterminado. En cierto sentido, la clase base "consulta" al tipo derivado qué debería ocurrir cuando hay un sobregiro. El comportamiento predeterminado es rechazar la transacción iniciando una excepción.
Comencemos agregando un segundo constructor que incluye un parámetro opcional minimumBalance
. Este nuevo constructor realiza todas las acciones realizadas por el constructor existente. Además, establece la propiedad del saldo mínimo. Puede copiar el cuerpo del constructor existente, pero eso significa que dos ubicaciones cambiarán en el futuro. En su lugar, puede usar el encadenamiento de constructores para que un constructor llame a otro. El código siguiente muestra los dos constructores y el nuevo campo adicional:
private readonly decimal _minimumBalance;
public BankAccount(string name, decimal initialBalance) : this(name, initialBalance, 0) { }
public BankAccount(string name, decimal initialBalance, decimal minimumBalance)
{
Number = s_accountNumberSeed.ToString();
s_accountNumberSeed++;
Owner = name;
_minimumBalance = minimumBalance;
if (initialBalance > 0)
MakeDeposit(initialBalance, DateTime.Now, "Initial balance");
}
El código anterior muestra dos técnicas nuevas. En primer lugar, el minimumBalance
campo se marca como readonly
. Esto significa que el valor no se puede cambiar después de construir el objeto. Una vez que se crea un BankAccount
, el minimumBalance
no se puede cambiar. En segundo lugar, el constructor que toma dos parámetros usa : this(name, initialBalance, 0) { }
como implementación. La : this()
expresión llama al otro constructor, el que tiene tres parámetros. Esta técnica permite tener una sola implementación para inicializar un objeto aunque el código de cliente pueda elegir uno de muchos constructores.
Esta implementación solo llama MakeDeposit
si el saldo inicial es mayor que 0
. Esto conserva la regla que los depósitos deben ser positivos, pero permite que la cuenta de crédito se abra con un 0
saldo.
Ahora que la BankAccount
clase tiene un campo de solo lectura para el saldo mínimo, el cambio final es cambiar el código 0
fijo a minimumBalance
en el método MakeWithdrawal
:
if (Balance - amount < _minimumBalance)
Después de extender la BankAccount
clase, puede modificar el LineOfCreditAccount
constructor para llamar al nuevo constructor base, como se muestra en el código siguiente:
public LineOfCreditAccount(string name, decimal initialBalance, decimal creditLimit) : base(name, initialBalance, -creditLimit)
{
}
Observe que el LineOfCreditAccount
constructor cambia el signo del creditLimit
parámetro para que coincida con el significado del minimumBalance
parámetro.
Diferentes reglas de sobregiro
La última característica que se va a agregar permite LineOfCreditAccount
cobrar una tarifa por superar el límite de crédito en lugar de rechazar la transacción.
Una técnica consiste en definir una función virtual en la que se implementa el comportamiento necesario. La BankAccount
clase refactoriza el MakeWithdrawal
método en dos métodos. El nuevo método realiza la acción especificada cuando la retirada toma el saldo por debajo del mínimo. El método existente MakeWithdrawal
tiene el código siguiente:
public void MakeWithdrawal(decimal amount, DateTime date, string note)
{
if (amount <= 0)
{
throw new ArgumentOutOfRangeException(nameof(amount), "Amount of withdrawal must be positive");
}
if (Balance - amount < _minimumBalance)
{
throw new InvalidOperationException("Not sufficient funds for this withdrawal");
}
var withdrawal = new Transaction(-amount, date, note);
_allTransactions.Add(withdrawal);
}
Reemplácelo por el código siguiente:
public void MakeWithdrawal(decimal amount, DateTime date, string note)
{
if (amount <= 0)
{
throw new ArgumentOutOfRangeException(nameof(amount), "Amount of withdrawal must be positive");
}
Transaction? overdraftTransaction = CheckWithdrawalLimit(Balance - amount < _minimumBalance);
Transaction? withdrawal = new(-amount, date, note);
_allTransactions.Add(withdrawal);
if (overdraftTransaction != null)
_allTransactions.Add(overdraftTransaction);
}
protected virtual Transaction? CheckWithdrawalLimit(bool isOverdrawn)
{
if (isOverdrawn)
{
throw new InvalidOperationException("Not sufficient funds for this withdrawal");
}
else
{
return default;
}
}
El método agregado es protected
, lo que significa que solo se puede llamar desde clases derivadas. Esa declaración impide que otros clientes llamen al método . También es para que las clases derivadas puedan cambiar el comportamiento virtual
. El tipo de valor devuelto es .Transaction?
La ?
anotación indica que el método puede devolver null
. Agregue la siguiente implementación en el LineOfCreditAccount
para cobrar una tarifa cuando se supere el límite de retiro.
protected override Transaction? CheckWithdrawalLimit(bool isOverdrawn) =>
isOverdrawn
? new Transaction(-20, DateTime.Now, "Apply overdraft fee")
: default;
El reemplazo devuelve una transacción de cuota cuando en la cuenta se produce un descubierto. Si el retiro no supera el límite, el método devuelve una null
transacción. Eso indica que no hay ninguna tarifa. Pruebe estos cambios agregando el código siguiente al Main
método en la Program
clase :
var lineOfCredit = new LineOfCreditAccount("line of credit", 0, 2000);
// How much is too much to borrow?
lineOfCredit.MakeWithdrawal(1000m, DateTime.Now, "Take out monthly advance");
lineOfCredit.MakeDeposit(50m, DateTime.Now, "Pay back small amount");
lineOfCredit.MakeWithdrawal(5000m, DateTime.Now, "Emergency funds for repairs");
lineOfCredit.MakeDeposit(150m, DateTime.Now, "Partial restoration on repairs");
lineOfCredit.PerformMonthEndTransactions();
Console.WriteLine(lineOfCredit.GetAccountHistory());
Ejecute el programa y compruebe los resultados.
Resumen
Si se ha quedado bloqueado, puede consultar el origen de este tutorial en el repositorio de GitHub.
En este tutorial se demostraron muchas de las técnicas utilizadas en la programación con Object-Oriented:
- Usó la abstracción cuando definió clases para cada uno de los distintos tipos de cuenta. Esas clases describen el comportamiento de ese tipo de cuenta.
- Usó la encapsulación cuando mantuvo muchos detalles
private
en cada clase. - Usó la herencia cuando aprovechó la implementación ya creada en la clase
BankAccount
para guardar el código. - Ha usado polimorfismo al crear
virtual
métodos que las clases derivadas podrían invalidar para crear un comportamiento específico para ese tipo de cuenta.