Hola Mario,
Cita:
Es por eso que los nulos se consideran el "error del billon de dolares". La unica forma (sana) es ELIMINAR POR COMPLETO los nullos.
Eso no sirve en Delphi, ya tiene demasiada historia.
Igual, el resto de cosas son mas de lenguajes funcionales, y una vez que un lenguaje se estabiliza en un paradigma, intentar alterarlo solo hace todo mas confuso.
|
Estoy de acuerdo con el error de billon de dolares. El tema es que para ciertos casos puede ser util. Muchos diran que se puede resolver usando clases (el
null object), es decir crear una clase en donde este definido el comportamiento "nulo" (muchas veces es "no hacer nada"). Esto me parece muy bien y de hecho lo he usado muchisimo. Pero en ciertos casos resulta engorroso de escribir, leer, entender: (esto tambien toma ideas de
primitive obsession)
Código Delphi
[-]
procedure DoQuery;
var
DateForm, DateTo: TDateTimeClass;
begin
DateFrom := TDateTimeClass.CreateOrDefault(Edit1.Text);
DateTo := TDateTimeClass.CreateOrDefault(Edit2.Text);
Query.SQL.Text := 'SELECT * FROM Orders WHERE Date BETWEEN :From AND: :To ';
Query.ParamByName('From').AsDateTime := DateFrom.Value;
Query.ParamByName('To').AsDateTime := DateTo.Value;
end;
TDateTimeClass parseara el string y creara la instancia de TDateTime, y la hace accesible a partir de la propiedad .Value. Una instancia de TDateTimeClass garantiza que el valor de TDateTime es correcto dentro de las reglas de negocio.
Esto lleva muchos problemas como determinar cual es el valor mas adecuado para representar el "nulo". Ademas, la idea del nulo es asignar un valor para que "de manera conveniente" cuando se use Value me de un valor que me sirva como "nulo". En caso de fechas podria devolver la minima fecha representable. Entonces en el SQL me quedaria un between 31/12/1899 and 31/12/1899, lo que no me devuelve el resultado esperado. Entonces tengo que meter otro constructor especificamente para las "FechaHasta" que asigne como nulo por ej la fecha 31/12/2200 23:59:59:999.
Ya no son una sino dos clases para una "pavada". Ademas tengo que usar try-finally cuando antes no era necesario, para liberar memoria. Si usara interfaces es cierto, me ahorro el try-finally, pero la complejidad sigue aumentando y ademas eso es cada vez mas y mas overhead. Y por ultimo el .Value "contamina la API", ya que en Delphi no podemos sobrecargar los operadores en clases (ok,
se puede usando features que "no existen")
El Nullable<T> como "first type citizen" soluciona este problema ya que no contamina la API y ademas como es un record y con sus operadores sobrecargados, lo hace tan limpio que no se nota. Simplemente te ayuda a prevenir errores tontos y dificiles de encontrar (bueno quiza con un Unit Test lo detectes en segundos) porque te olvidaste de asignar un valor primitivo, o justo de lo contrario, de "limparlo"
Aca tambien tenemos mas influencias de programacion funcional en Delphi (por ejemplo:
"Monads" o el
"Either")
Yo creo que la era en la que OOP debe ser la respuesta a todo ha llegado a su fin. Si un lenguaje queda "atado" o estabilizado a un paradigma no podria considerarlo un lenguaje "moderno". Por suerte no es el caso de Delphi. En Delphi podes programar estructurado, procedimental, orientado a objetos, orientado a aspectos, y "emular" o tomar ideas de funcional
Funcional realmente hace el codigo mas facil de entender, de leer, de mantener y de extender porque separa el "que se hace" del "como se hace". Un ejemplo pavo, dadas estas declaraciones:
Código PHP:
uses
System.SysUtils,
Spring,
Spring.Collections;
type
Customer = record
private
FName: string;
FAge: Integer;
public
class function Create(const Age: Integer; const Name: string): Customer; static;
property Age: Integer read FAge;
property Name: string read FName;
end;
function Customers: Spring.Collections.IEnumerable<Customer>;
var
I: Integer;
List: IList<Customer> absolute Result;
begin
List := TCollections.CreateList<Customer>;
for I := 0 to Random(100) do
begin
if Odd(I) then
List.Add(Customer.Create(I, 'Cliente # ' + I.ToString))
else
List.Add(Customer.Create(I, 'Customer # ' + I.ToString))
end;
end;
La forma "tradicional" imperativa de mostrar en pantalla los clientes con edad entre 9 y 23 podria ser:
Código Delphi
[-]
procedure Main;
var
Each: Customer;
begin
Writeln('Clientes con Edad entre 9 y 23');
for Each in Customers do
begin
if (Each.Age > 9) and (Each.Age < 23) then
Writeln(Format('%s - Edad: %d', [Each.Name, Each.Age]));
end;
end;
Si ahora tambien tengo que incluir en el resultado aquellos que tienen Edad 4, tengo que modificar el condicional, haciendolo cada vez mas complejo:
Código Delphi
[-]
procedure Main;
var
Each: Customer;
begin
Writeln('Clientes con Edad entre 9 y 23, o bien Edad = 4');
for Each in Customers do
begin
if ((Each.Age > 9) and (Each.Age < 23)) or (Age = 4) then
Writeln(Format('%s - Edad: %d', [Each.Name, Each.Age]));
end;
end;
Y si ahora necesito tambien solo los que el nombre es "ingles" ("Customer en lugar de "Cliente")
Código Delphi
[-]
procedure Main;
var
Each: Customer;
begin
Writeln('Clientes con Edad entre 9 y 23, o bien Edad = 4, y nombre ingles');
for Each in Customers do
begin
if ((Each.Age > 9) and (Each.Age < 23)) or (Age = 4) and (Each.Name.StartsWith('Customer')) then
Writeln(Format('%s - Edad: %d', [Each.Name, Each.Age]));
end;
end;
Y esta todo mezclado: la data con las transformaciones filtros y operaciones esta todo entremezclado entre si. Y por mas que refactorize agregando condiciones, no cambia tanto porque tendria muchas condiciones.
Por suerte la interface IEnumerable<T> define un metodo Where que recibe un predicado<T>. Un predicado<T> es metodo anonimo que retorna Boolean y recibe como argumento un "T". Basicamente es esto:
Código Delphi
[-]
function(const Arg: T): Boolean;
begin
Result := evaluar Arg
end;
Podemos escribir entonces..
Código Delphi
[-]
procedure Main;
begin
Writeln('Clientes con Edad entre 9 y 23');
Customers
.Where(
function(const c: Customer): Boolean
begin
Result := (c.Age > 9) and (c.Age < 23);
end)
.ForEach(
procedure(const c: Customer)
begin
Writeln(Format('%s - Edad: %d', [c.Name, c.Age]));
end);
end;
Ja. No cambio mucho, no? Muchos me diran, ok cambiaste el for in, escribiste un monton de codigo mas y es lo mismo: las condiciones sigen juntas. Bueno, bien se pueden concatenar las invocaciones a Where si quisiera. Pero el problema sigue igual: datos y operaciones mezcladas. Pero que tal esto:..
Código PHP:
function OlderThan(const Age: Integer): TPredicate<Customer>;
begin
Result := function(const c: Customer): Boolean
begin
Result := c.Age > Age;
end
end;
function YoungerThan(const Age: Integer): TPredicate<Customer>;
begin
Result := function(const c: Customer): Boolean
begin
Result := c.Age < Age;
end
end;
function Aged(const Age: Integer): TPredicate<Customer>;
begin
Result := function(const c: Customer): Boolean
begin
Result := c.Age = Age;
end
end;
function NameStartsWith(const Text: string): TPredicate<Customer>;
begin
Result := function(const c: Customer): Boolean
begin
Result := c.Name.StartsWith(Text, True);
end
end;
function NameIsEnglish: TPredicate<Customer>;
begin
Result := NameStartsWith('Customer');
end;
procedure PrettyPrint(const c: Customer);
begin
Writeln(Format('%s - Edad: %d', [c.Name, c.Age]));
end;
Procedimientos o funciones "sencillas", separadas, reusables, faciles de entender, "parametrizables" si es necesario, y se pueden "componer", asi:
Código Delphi
[-]
procedure Main;
begin
Writeln('Clientes con Edad entre 9 y 23');
Customers
.Where(OlderThan(9))
.Where(YoungerThan(23))
.ForEach(PrettyPrint);
end;
Quiero filtrar tambien solo los que tienen nombre "en ingles"? No hay problema:
Código Delphi
[-]procedure Main;
begin
Writeln('Clientes con Edad entre 9 y 23');
Customers
.Where(OlderThan(9))
.Where(YoungerThan(23))
.Where(NameStartsWith('Customer')) .Where(NameIsEnglish()) .ForEach(PrettyPrint);
end;
Y si quiero meter todo dentro de un solo predicado, se pueden usar patrones:
Specification Pattern
Código PHP:
uses
...,
Spring.DesignPatterns;
...
function CustomerFilter: TSpecification<Customer>;
var
s: TSpecification<Customer>;
begin
s := OlderThan(9);
Result := (s and YoungerThan(23) or Aged(4)) and NameIsEnglish;
end;
procedure Main;
begin
Writeln('Clientes con Edad entre 9 y 23 o Edad = 4, y nombre en ingles');
Customers
.Where(CustomerFilter())
.ForEach(PrettyPrint);
end;
En Spring otra vez se "abusa" de la sobrecarga de operadores para poder usar la sintaxis "and", "or", "not". Aun asi, el lenguaje limita un poco y por eso es necesaria la variable "s" para iniciar "la cadena" en la funcion CustomerFilter. Como TSpecification es un record que tiene sobrecargado el operador Implicit para convertirlo directamente a un TPredicate, la llamada al Where es posible. Esto tambien se puede hacer:
Código PHP:
procedure Main;
var
s: TSpecification<Customer>;
begin
Writeln('Clientes con Edad entre 9 y 23 o Edad = 4, y nombre en español');
s := TSpecification<Customer>(OlderThan(9));
Customers
.Where(s and YoungerThan(23) or Aged(4))
.Where(not TSpecification<Customer>(NameIsEnglish()))
.ForEach(PrettyPrint);
De nuevo, la sintaxis demasiado "verbose" de Pascal, y el tipificado fuerte me obliga a "mucha ceremonia" pero creo que la idea se entiende