
SOLID is five design principles: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion. They keep a codebase clean, testable, and easy to extend. Below, each one is applied to a Dart and Flutter fruit shop app, with a bad version and a good version of the code.
Not every app needs all five. What matters is knowing which one applies.
S: Single Responsibility Principle (SRP)
A class should have only one reason to change. In the fruit shop, a change to how fruit is displayed and a change to how an invoice is calculated should not land in the same class.
BAD:
class FruitAndInvoice {
final String fruitName;
final double fruitPrice;
final List<double> allFruitPrices;
FruitAndInvoice(this.fruitName, this.fruitPrice, this.allFruitPrices);
void displayFruit() {
print('Fruit: $fruitName, Price: $fruitPrice');
}
double calculateTotal() {
return allFruitPrices.fold(0, (total, price) => total + price);
}
void displayInvoice() {
displayFruit();
print('Total: ${calculateTotal()}');
}
}
In FruitAndInvoice, display and invoice calculation live in one class. A change to either one edits this class.
GOOD:
class Fruit {
final String name;
final double price;
Fruit(this.name, this.price);
void displayFruit() {
print('Fruit: $name, Price: $price');
}
}
class Invoice {
final List<Fruit> fruits;
Invoice(this.fruits);
double calculateTotal() {
return fruits.fold(0, (total, fruit) => total + fruit.price);
}
void displayInvoice() {
for (final fruit in fruits) {
fruit.displayFruit();
}
print('Total: ${calculateTotal()}');
}
}
Fruit holds the fruit data. Invoice holds the fruit list and calculates the total. Changing the display edits Fruit. Changing the total edits Invoice.
O: Open/Closed Principle (OCP)
A type should be open for extension and closed for modification. In the fruit shop, a new discount is a new class, not an edit to Fruit.
BAD:
class Fruit {
final String name;
final double price;
Fruit(this.name, this.price);
double discountPrice(double discount) {
return price - (price * discount);
}
}
class SeasonalFruit extends Fruit {
SeasonalFruit(String name, double price) : super(name, price);
// Override discountPrice method to offer a greater discount
@override
double discountPrice(double discount) {
return price - (price * discount * 1.5);
}
}
SeasonalFruit overrides discountPrice and changes the behaviour Fruit already had. That edits existing behaviour instead of extending it.
GOOD:
abstract class Fruit {
final String name;
final double price;
Fruit(this.name, this.price);
double discountPrice(double discount);
}
class RegularFruit extends Fruit {
RegularFruit(String name, double price) : super(name, price);
@override
double discountPrice(double discount) {
return price - (price * discount);
}
}
class SeasonalFruit extends Fruit {
SeasonalFruit(String name, double price) : super(name, price);
@override
double discountPrice(double discount) {
return price - (price * discount * 1.5);
}
}
Fruit is abstract and declares discountPrice. RegularFruit and SeasonalFruit extend Fruit and implement the method. A new discount is a new subclass. Fruit stays as it is.
L: Liskov Substitution Principle (LSP)
Objects of a derived class should be able to replace objects of the base class without affecting the correctness of the program. In the fruit shop, a new fruit type has to honour that discount contract.
BAD:
class Fruit {
final String name;
final double price;
Fruit(this.name, this.price);
double discountPrice(double discount) {
return price - (price * discount);
}
}
class SpecialFruit extends Fruit {
SpecialFruit(String name, double price) : super(name, price);
@override
double discountPrice(double discount) {
if (discount > 0.5) {
throw Exception("Discount for special fruit can't be more than 50%");
}
return price - (price * discount);
}
}
SpecialFruit overrides discountPrice and throws if the discount is more than 50%. A caller that had a Fruit cannot substitute SpecialFruit and keep the program correct.
GOOD:
abstract class Fruit {
final String name;
final double price;
Fruit(this.name, this.price);
double discountPrice(double discount);
}
class RegularFruit extends Fruit {
RegularFruit(String name, double price) : super(name, price);
@override
double discountPrice(double discount) {
return price - (price * discount);
}
}
class SpecialFruit extends Fruit {
SpecialFruit(String name, double price) : super(name, price);
@override
double discountPrice(double discount) {
final adjustedDiscount = discount > 0.5 ? 0.5 : discount;
return price - (price * adjustedDiscount);
}
}
SpecialFruit changes the discount math and still honours the contract from Fruit, so it can stand in for Fruit. RegularFruit and SpecialFruit each implement that contract. A new fruit type is another subclass of Fruit.
I: Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they do not use. In the fruit shop, display, discount, and inventory are separate interfaces.
BAD:
abstract class FruitOperations {
void displayFruit();
double calculateDiscountPrice(double discount);
void updateInventory(int quantity);
}
class Apple implements FruitOperations {
final String name = 'Apple';
final double price = 2.0;
@override
void displayFruit() {
print('Fruit: $name, Price: $price');
}
@override
double calculateDiscountPrice(double discount) {
return price - (price * discount);
}
/**
* code to update inventory
**/
@override
void updateInventory(int quantity) {
// no-op
}
}
FruitOperations bundles the operations, so Apple implements methods that do not apply to it.
GOOD:
abstract class FruitDisplay {
void displayFruit();
}
abstract class FruitDiscount {
double calculateDiscountPrice(double discount);
}
abstract class FruitInventory {
void updateInventory(int quantity);
}
The operations are split into FruitDisplay, FruitDiscount, and FruitInventory. Apple implements only the interfaces that apply to it.
class Apple implements FruitDisplay, FruitDiscount {
final String name = 'Apple';
final double price = 2.0;
@override
void displayFruit() {
print('Fruit: $name, Price: $price');
}
@override
double calculateDiscountPrice(double discount) {
return price - (price * discount);
}
}
class InventoryManager implements FruitInventory {
@override
void updateInventory(int quantity) {
// code to update inventory
}
}
Each interface is one contract. Apple and InventoryManager implement only the operations that apply to them.
D: Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules. Both should depend on abstractions. In the fruit shop, FruitShop depends on FruitListing, not on the concrete Inventory class.
BAD:
class Inventory {
final List<Fruit> fruits;
Inventory(this.fruits);
void listFruits() {
for (final fruit in fruits) {
print('Fruit: ${fruit.name}, Price: ${fruit.price}');
}
}
}
class FruitShop {
final Inventory inventory;
FruitShop(this.inventory);
void displayInventory() {
inventory.listFruits();
}
}
FruitShop is tied to Inventory. A change in Inventory forces a change in FruitShop.
GOOD:
abstract class FruitListing {
void listFruits();
}
Inventory implements the FruitListing abstraction. FruitShop depends on FruitListing, not on Inventory.
class Inventory implements FruitListing {
final List<Fruit> fruits;
Inventory(this.fruits);
@override
void listFruits() {
for (final fruit in fruits) {
print('Fruit: ${fruit.name}, Price: ${fruit.price}');
}
}
}
class FruitShop {
final FruitListing fruitListing;
FruitShop(this.fruitListing);
void displayInventory() {
fruitListing.listFruits();
}
}
FruitShop can use any implementation of FruitListing, not only Inventory.
You do not have to apply all five at once. If you want somewhere to start, pick the messiest file in your current Flutter project and ask which single responsibility it is failing to keep. That one question tends to pull the rest of SOLID along behind it.
Happy coding!
davthecoder
If you have enjoyed this blog post, remember to follow me and clap this post ;).

SOLID son cinco principios de diseño: responsabilidad única, abierto/cerrado, sustitución de Liskov, segregación de interfaces e inversión de dependencias. Mantienen una codebase limpia, fácil de probar y fácil de ampliar. Abajo, cada uno se aplica a una app de frutería en Dart y Flutter, con una versión mala y una versión buena del código.
No todas las apps necesitan los cinco. Lo que importa es saber cuál aplica.
S: principio de responsabilidad única (SRP)
Una clase debería tener un solo motivo para cambiar. En la frutería, un cambio en cómo se muestra la fruta y un cambio en cómo se calcula una factura no deberían caer en la misma clase.
MAL:
class FruitAndInvoice {
final String fruitName;
final double fruitPrice;
final List<double> allFruitPrices;
FruitAndInvoice(this.fruitName, this.fruitPrice, this.allFruitPrices);
void displayFruit() {
print('Fruit: $fruitName, Price: $fruitPrice');
}
double calculateTotal() {
return allFruitPrices.fold(0, (total, price) => total + price);
}
void displayInvoice() {
displayFruit();
print('Total: ${calculateTotal()}');
}
}
En FruitAndInvoice, la presentación y el cálculo de la factura viven en una sola clase. Un cambio en cualquiera de las dos edita esta clase.
BIEN:
class Fruit {
final String name;
final double price;
Fruit(this.name, this.price);
void displayFruit() {
print('Fruit: $name, Price: $price');
}
}
class Invoice {
final List<Fruit> fruits;
Invoice(this.fruits);
double calculateTotal() {
return fruits.fold(0, (total, fruit) => total + fruit.price);
}
void displayInvoice() {
for (final fruit in fruits) {
fruit.displayFruit();
}
print('Total: ${calculateTotal()}');
}
}
Fruit guarda los datos de la fruta. Invoice guarda la lista de frutas y calcula el total. Cambiar la presentación edita Fruit. Cambiar el total edita Invoice.
O: principio de abierto/cerrado (OCP)
Un tipo debería estar abierto a la ampliación y cerrado a la modificación. En la frutería, un descuento nuevo es una clase nueva, no una edición de Fruit.
MAL:
class Fruit {
final String name;
final double price;
Fruit(this.name, this.price);
double discountPrice(double discount) {
return price - (price * discount);
}
}
class SeasonalFruit extends Fruit {
SeasonalFruit(String name, double price) : super(name, price);
// Override discountPrice method to offer a greater discount
@override
double discountPrice(double discount) {
return price - (price * discount * 1.5);
}
}
SeasonalFruit sobrescribe discountPrice y cambia el comportamiento que Fruit ya tenía. Eso edita el comportamiento existente en lugar de ampliarlo.
BIEN:
abstract class Fruit {
final String name;
final double price;
Fruit(this.name, this.price);
double discountPrice(double discount);
}
class RegularFruit extends Fruit {
RegularFruit(String name, double price) : super(name, price);
@override
double discountPrice(double discount) {
return price - (price * discount);
}
}
class SeasonalFruit extends Fruit {
SeasonalFruit(String name, double price) : super(name, price);
@override
double discountPrice(double discount) {
return price - (price * discount * 1.5);
}
}
Fruit es abstracta y declara discountPrice. RegularFruit y SeasonalFruit extienden Fruit e implementan el método. Un descuento nuevo es una subclase nueva. Fruit se queda como está.
L: principio de sustitución de Liskov (LSP)
Los objetos de una clase derivada deberían poder sustituir a los objetos de la clase base sin afectar a la corrección del programa. En la frutería, un tipo nuevo de fruta tiene que respetar ese contrato de descuento.
MAL:
class Fruit {
final String name;
final double price;
Fruit(this.name, this.price);
double discountPrice(double discount) {
return price - (price * discount);
}
}
class SpecialFruit extends Fruit {
SpecialFruit(String name, double price) : super(name, price);
@override
double discountPrice(double discount) {
if (discount > 0.5) {
throw Exception("Discount for special fruit can't be more than 50%");
}
return price - (price * discount);
}
}
SpecialFruit sobrescribe discountPrice y lanza una excepción si el descuento pasa del 50 %. Quien tenía un Fruit no puede sustituirlo por SpecialFruit y mantener el programa correcto.
BIEN:
abstract class Fruit {
final String name;
final double price;
Fruit(this.name, this.price);
double discountPrice(double discount);
}
class RegularFruit extends Fruit {
RegularFruit(String name, double price) : super(name, price);
@override
double discountPrice(double discount) {
return price - (price * discount);
}
}
class SpecialFruit extends Fruit {
SpecialFruit(String name, double price) : super(name, price);
@override
double discountPrice(double discount) {
final adjustedDiscount = discount > 0.5 ? 0.5 : discount;
return price - (price * adjustedDiscount);
}
}
SpecialFruit cambia el cálculo del descuento y sigue respetando el contrato de Fruit, así que puede ocupar su sitio. RegularFruit y SpecialFruit implementan ese contrato. Un tipo nuevo de fruta es otra subclase de Fruit.
I: principio de segregación de interfaces (ISP)
No se debería obligar a los clientes a depender de interfaces que no usan. En la frutería, presentación, descuento e inventario son interfaces distintas.
MAL:
abstract class FruitOperations {
void displayFruit();
double calculateDiscountPrice(double discount);
void updateInventory(int quantity);
}
class Apple implements FruitOperations {
final String name = 'Apple';
final double price = 2.0;
@override
void displayFruit() {
print('Fruit: $name, Price: $price');
}
@override
double calculateDiscountPrice(double discount) {
return price - (price * discount);
}
/**
* code to update inventory
**/
@override
void updateInventory(int quantity) {
// no-op
}
}
FruitOperations junta las operaciones, así que Apple implementa métodos que no le aplican.
BIEN:
abstract class FruitDisplay {
void displayFruit();
}
abstract class FruitDiscount {
double calculateDiscountPrice(double discount);
}
abstract class FruitInventory {
void updateInventory(int quantity);
}
Las operaciones se parten en FruitDisplay, FruitDiscount y FruitInventory. Apple implementa solo las interfaces que le aplican.
class Apple implements FruitDisplay, FruitDiscount {
final String name = 'Apple';
final double price = 2.0;
@override
void displayFruit() {
print('Fruit: $name, Price: $price');
}
@override
double calculateDiscountPrice(double discount) {
return price - (price * discount);
}
}
class InventoryManager implements FruitInventory {
@override
void updateInventory(int quantity) {
// code to update inventory
}
}
Cada interfaz es un contrato. Apple e InventoryManager implementan solo las operaciones que les aplican.
D: principio de inversión de dependencias (DIP)
Los módulos de alto nivel no deberían depender de los de bajo nivel. Los dos deberían depender de abstracciones. En la frutería, FruitShop depende de FruitListing, no de la clase concreta Inventory.
MAL:
class Inventory {
final List<Fruit> fruits;
Inventory(this.fruits);
void listFruits() {
for (final fruit in fruits) {
print('Fruit: ${fruit.name}, Price: ${fruit.price}');
}
}
}
class FruitShop {
final Inventory inventory;
FruitShop(this.inventory);
void displayInventory() {
inventory.listFruits();
}
}
FruitShop está atada a Inventory. Un cambio en Inventory obliga a cambiar FruitShop.
BIEN:
abstract class FruitListing {
void listFruits();
}
Inventory implementa la abstracción FruitListing. FruitShop depende de FruitListing, no de Inventory.
class Inventory implements FruitListing {
final List<Fruit> fruits;
Inventory(this.fruits);
@override
void listFruits() {
for (final fruit in fruits) {
print('Fruit: ${fruit.name}, Price: ${fruit.price}');
}
}
}
class FruitShop {
final FruitListing fruitListing;
FruitShop(this.fruitListing);
void displayInventory() {
fruitListing.listFruits();
}
}
FruitShop puede usar cualquier implementación de FruitListing, no solo Inventory.
No hace falta aplicar los cinco a la vez. Si quieres un sitio por donde empezar, coge el archivo más caótico de tu proyecto Flutter actual y pregúntate qué responsabilidad única está dejando de cumplir. Esa sola pregunta suele arrastrar detrás el resto de SOLID.
¡Feliz programación!
davthecoder
Si te ha gustado esta entrada, acuérdate de seguirme y de aplaudir el post ;).
Loading comments…Cargando comentarios…