C# 8.0: Miembros implementados por defecto en interfaces

Casi terminado ya (al menos oficialmente) el verano, continúo con la descripción de las nuevas características que se añadirán a C# 8.0. Esta entrada ya llevaba más de un mes “en el tintero”, pero para las próximas intentaré adaptarme al ritmo y contenidos de la serie que irá publicando mi buen amigo Jorge Serrano, tratando de no repetir, sino más bien de complementar lo que él presente allí.

Esta vez le tocó el turno a los miembros implementados por defecto en interfaces (default interface members). Esta característica, con toda seguridad una de las adiciones más importantes a C# 8.0, hace posible que el creador de una librería pueda añadir métodos, propiedades, etc. a una interfaz en versiones futuras sin romper la compatibilidad a nivel binario o de código fuente con las implementaciones de la interfaz que ya pudieran existir. Otros lenguajes modernos, como Java y Swift, ofrecen desde hace algún tiempo características similares.

Suponga que tenemos un sistema que parte de una librería básica en la que se define una interfaz IProduct para representar productos que se ponen a la venta en un momento cualquiera. La interfaz declara miembros que corresponden a las diferentes características de los productos, tales como marca, modelo, fabricante, país de fabricación y precio base, y a las funcionalidades requeridas, en este caso un método para calcular el precio de venta (sale price) de un producto. La fórmula para calcular el precio de venta, en el caso general, puede variar ampliamente; no se calcula de la misma manera en una tienda “Todo a cien” que en un establecimiento perteneciente a una gran cadena.

using System;

namespace DefaultInterfaceMemberImplementations
{
    public enum ManufacturingCountry
    {
        USA,
        Canada,
        UnitedKingdom,
        Germany,
        China,
        Japan,
        SouthKorea
    }

    public interface IProduct
    {
        string Name { get; }
        string Model { get; }
        string Manufacturer { get; }
        ManufacturingCountry ManufacturingCountry { get; }
        decimal BasePrice { get; }

        decimal GetSalePrice();
    }

Una tienda de electrodomésticos (appliances) que hiciera uso de la librería anterior podría apoyarse en ella para definir sus tipos y funcionalidades de la siguiente forma:

    public interface IAppliance : IProduct
    {
        ApplianceCategory ApplianceCategory { get; }
    }

    public enum ApplianceCategory
    {
        Televisor,
        Refrigerator,
        DishWasher,
        Washer,
        Dryer,
    }

    public abstract class ApplianceBase : IAppliance
    {
        public const decimal AppliancesMarkup = 0.1M;  // 10%

        public ApplianceCategory ApplianceCategory { getprivate set; }
        public string Name { getprivate set; }
        public string Model { getprivate set; }
        public string Manufacturer { getprivate set; }
        public ManufacturingCountry ManufacturingCountry { getprivate set; }
        public decimal BasePrice { getprivate set; }
        protected ApplianceBase(ApplianceCategory category,
            string name, string model, string manufacturer,
            ManufacturingCountry country, decimal basePrice)
        {
            ApplianceCategory = category;
            Name = name;
            Model = model;
            Manufacturer = manufacturer;
            ManufacturingCountry = country;
            BasePrice = basePrice;
        }

        public virtual decimal GetSalePrice() => 
            (1M + AppliancesMarkup) * BasePrice;
    }

    public class Televisor : ApplianceBase
    {
        public Televisor(string name, string model, string company, 
            ManufacturingCountry country, decimal basePrice) : 
            base(ApplianceCategory.Televisor, 
                name, model, company, country, basePrice) { }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var sms = new Televisor("4K TV", "QN55Q8FN", "Samsung", 
                ManufacturingCountry.SouthKorea, 1265M);
            Console.WriteLine($"Samsung price = {sms.GetSalePrice():N2}");
            var tcl = new Televisor("4K TV", "55R617", "TCL", 
                ManufacturingCountry.China, 529M);
            Console.WriteLine($"TCL price = {tcl.GetSalePrice():N2}");
        }
    }
}

En el código anterior, a los electrodomésticos se les marca con un 10% adicional.

Imagine ahora que el contexto internacional cambia y a un cierto personaje se le ocurre empezar a imponer tarifas de importación a los productos provenientes de países que no se plieguen a sus exigencias. En tal caso, se hará necesario subir los precios de venta a los productos para compensar la pérdida de beneficios causados por las tarifas. Sería conveniente extender la interfaz IProduct con un método para determinar la tarifa porcentual (tariff rate) a la que un producto cualquiera será sometido, a partir de las características del mismo. Esto es lo que hacen posible los miembros implementados por defecto en C# 8.0; básicamente, el creador de la interfaz IProduct podrá extenderla con un nuevo método GetTariffRate, siempre que suministre una implementación que se aplicará por defecto a las clases ya existentes que implementen la interfaz. Por ejemplo:

    public interface IProduct
    {
        string Name { get; }
        string Model { get; }
        string Manufacturer { get; }
        ManufacturingCountry ManufacturingCountry { get; }
        decimal BasePrice { get; }
        decimal GetSalePrice();

        // Miembro implementado por defecto
        public decimal GetTariffRate() => ManufacturingCountry switch
        {
            ManufacturingCountry.China => 0.15M,
            _ => 0,
        };
    }

Si la interfaz que contiene IProduct estuviese implementada en un ensamblado independiente, el ensamblado en el que estuviesen contenidos los electrodomésticos continuaría trabajando como antes contra la nueva versión binaria de aquél; y las aplicaciones que utilicen el ensamblado de electrodomésticos podrán llamar a GetTariffRate() aún cuando dicho ensamblado no haya sido modificado en modo alguno. En tal caso, se utilizaría la versión predefinida del método. Pero, por supuesto, el creador de IAppliance podrá cuando lo desee redefinir (override) GetTariffRate para adaptarlo a las condiciones específicas de ese tipo de productos.

Para que todo esto funcione no basta con el soporte lingüístico en C# 8.0, sino que además es imprescindible el soporte correspondiente en .NET Core 3.0 y posteriores. Puede encontrar mucha más información sobre la propuesta en la página de GitHub dedicada a la característica. Allí se la nombra también como métodos de extensión virtuales (virtual extension methods) por la similitud obvia entre la implementación de esta característica y la de los métodos virtuales; pero observe que esta nueva posibilidad no se limita exclusivamente a métodos, sino que además podrá aplicarse a propiedades, indexadores y eventos, ¡y no solo de instancia, sino también estáticos!

Octavio Hernandez

Desarrollador y consultor en tecnologías .NET. Microsoft C# MVP entre 2004 y 2010.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *