class Animal { constructor(name: string){ } getAnimalName() { } saveAnimal(a: Animal) { } }
Animal
class presented here describes some kind of animal. This class violates the principle of sole responsibility. How exactly is this principle violated?saveAnimal
method and manipulating the properties of the object in the constructor and in the getAnimalName
method.Animal
class in it: class Animal { constructor(name: string){ } getAnimalName() { } } class AnimalDB { getAnimal(a: Animal) { } saveAnimal(a: Animal) { } }
Animal
. class Animal { constructor(name: string){ } getAnimalName() { } }
Animal
, and find out what sounds they make. Imagine that we are solving this problem using the AnimalSounds
function: //... const animals: Array<Animal> = [ new Animal('lion'), new Animal('mouse') ]; function AnimalSound(a: Array<Animal>) { for(int i = 0; i <= a.length; i++) { if(a[i].name == 'lion') return 'roar'; if(a[i].name == 'mouse') return 'squeak'; } } AnimalSound(animals);
AnimalSound
function AnimalSound
not comply with the principle of openness-closeness, since, for example, when new types of animals appear, we will have to change it in order to recognize the sounds made by them. //... const animals: Array<Animal> = [ new Animal('lion'), new Animal('mouse'), new Animal('snake') ] //...
AnimalSound
function: //... function AnimalSound(a: Array<Animal>) { for(int i = 0; i <= a.length; i++) { if(a[i].name == 'lion') return 'roar'; if(a[i].name == 'mouse') return 'squeak'; if(a[i].name == 'snake') return 'hiss'; } } AnimalSound(animals);
if
expressions to it.AnimalSound
function in accordance with the principle of openness-closeness? For example - so: class Animal { makeSound(); //... } class Lion extends Animal { makeSound() { return 'roar'; } } class Squirrel extends Animal { makeSound() { return 'squeak'; } } class Snake extends Animal { makeSound() { return 'hiss'; } } //... function AnimalSound(a: Array<Animal>) { for(int i = 0; i <= a.length; i++) { a[i].makeSound(); } } AnimalSound(animals);
Animal
class now has a virtual method, makeSound
. With this approach, it is necessary that classes designed to describe specific animals expand the Animal
class and implement this method.makeSound
method, and when makeSound
through an array with animals in the AnimalSound
function, AnimalSound
will be enough to call this method for each element of the array.AnimalSound
will not have to change. We have aligned it with the principle of openness-closeness. class Discount { giveDiscount() { return this.price * 0.2 } }
fav
) customers receive a 20% discount, and VIP customers ( vip
) receive a double discount, that is, 40%. In order to implement this logic, it was decided to modify the class as follows: class Discount { giveDiscount() { if(this.customer == 'fav') { return this.price * 0.2; } if(this.customer == 'vip') { return this.price * 0.4; } } }
Discount
class. In this new class, we are implementing a new mechanism: class VIPDiscount: Discount { getDiscount() { return super.getDiscount() * 2; } }
class SuperVIPDiscount: VIPDiscount { getDiscount() { return super.getDiscount() * 2; } }
Animal
. Write a function designed to return information about the quantities of limbs of an animal. //... function AnimalLegCount(a: Array<Animal>) { for(int i = 0; i <= a.length; i++) { if(typeof a[i] == Lion) return LionLegCount(a[i]); if(typeof a[i] == Mouse) return MouseLegCount(a[i]); if(typeof a[i] == Snake) return SnakeLegCount(a[i]); } } AnimalLegCount(animals);
//... class Pigeon extends Animal { } const animals[]: Array<Animal> = [ //..., new Pigeon(); ] function AnimalLegCount(a: Array<Animal>) { for(int i = 0; i <= a.length; i++) { if(typeof a[i] == Lion) return LionLegCount(a[i]); if(typeof a[i] == Mouse) return MouseLegCount(a[i]); if(typeof a[i] == Snake) return SnakeLegCount(a[i]); if(typeof a[i] == Pigeon) return PigeonLegCount(a[i]); } } AnimalLegCount(animals);
Animal
in our case) must also accept and return values ​​whose types are its subclasses ( Pigeon
).AnimalLegCount
function: function AnimalLegCount(a: Array<Animal>) { for(let i = 0; i <= a.length; i++) { a[i].LegCount(); } } AnimalLegCount(animals);
LegCount
methods. All she knows about types is that the objects she processes should belong to the Animal
class or its subclasses.LegCount
method should appear in the Animal
class: class Animal { //... LegCount(); }
//... class Lion extends Animal{ //... LegCount() { //... } } //...
LegCount
method for an instance of the Lion
class, the method implemented in this class is called, and exactly what can be expected from calling this method is returned.AnimalLegCount
function AnimalLegCount
not need to know about the object of which subclass of the Animal
class it processes in order to get information about the number of limbs in an animal represented by this object. The function simply calls the LegCount
method of the Animal
class, since subclasses of this class must implement this method so that they can be used instead, without disrupting the correctness of the program.Shape
: interface Shape { drawCircle(); drawSquare(); drawRectangle(); }
drawCircle
), squares ( drawSquare
) and rectangles ( drawRectangle
). As a result, classes that implement this interface and represent separate geometric shapes, such as a circle (Circle), a square (Square), and a rectangle (Rectangle), must contain an implementation of all these methods. It looks like this: class Circle implements Shape { drawCircle(){ //... } drawSquare(){ //... } drawRectangle(){ //... } } class Square implements Shape { drawCircle(){ //... } drawSquare(){ //... } drawRectangle(){ //... } } class Rectangle implements Shape { drawCircle(){ //... } drawSquare(){ //... } drawRectangle(){ //... } }
Rectangle
class, representing a rectangle, implements methods ( drawCircle
and drawSquare
), which it does not need at all. The same can be noticed when analyzing the code of the two other classes.Shape
interface, drawTriangle
, for drawing triangles: interface Shape { drawCircle(); drawSquare(); drawRectangle(); drawTriangle(); }
drawTriangle
method. Otherwise, an error will occur.Shape
from our example. Clients (we have the classes Circle
, Square
and Rectangle
) should not implement methods that they do not need to use. In addition, this principle indicates that the interface should solve only one task (in this it is similar to the principle of sole responsibility), therefore everything that goes beyond the scope of this task should be transferred to another interface or interfaces.Shape
interface solves the problems for the solution of which it is necessary to create separate interfaces. Following this idea, we will rework the code, creating separate interfaces for solving various highly specialized tasks: interface Shape { draw(); } interface ICircle { drawCircle(); } interface ISquare { drawSquare(); } interface IRectangle { drawRectangle(); } interface ITriangle { drawTriangle(); } class Circle implements ICircle { drawCircle() { //... } } class Square implements ISquare { drawSquare() { //... } } class Rectangle implements IRectangle { drawRectangle() { //... } } class Triangle implements ITriangle { drawTriangle() { //... } } class CustomShape implements Shape { draw(){ //... } }
ICircle
interface ICircle
used only for drawing circles, as well as other specialized interfaces for drawing other shapes. The Shape
interface can be used as a universal interface. class XMLHttpService extends XMLHttpRequestService {} class Http { constructor(private xmlhttpService: XMLHttpService) { } get(url: string , options: any) { this.xmlhttpService.request(url,'GET'); } post() { this.xmlhttpService.request(url,'POST'); } //... }
Http
class is a high-level component, and XMLHttpService
is a low-level component. This architecture violates clause A of the principle of dependency inversion: “The modules of the upper levels should not depend on the modules of the lower levels. Both types of modules should depend on abstractions. ”Http
class is dependent on the XMLHttpService
class. If we decide to change the mechanism used by the Http
class to interact with the network - say, it will be a Node.js service or, for example, a service stub used for testing purposes, we will have to edit all instances of the Http
class by changing the corresponding code. This violates the principle of openness-closeness.Http
class does not need to know exactly what is used for networking. Therefore, we will create the Connection
interface: interface Connection { request(url: string, opts:any); }
Connection
interface contains the description of the request
method and we pass an argument of type Connection
Http
class: class Http { constructor(private httpConnection: Connection) { } get(url: string , options: any) { this.httpConnection.request(url,'GET'); } post() { this.httpConnection.request(url,'POST'); } //... }
Http
class can use what it has been passed on without worrying about what is hidden behind the Connection
interface.XMLHttpService
class XMLHttpService
such a way that it implements this interface: class XMLHttpService implements Connection { const xhr = new XMLHttpRequest(); //... request(url: string, opts:any) { xhr.open(); xhr.send(); } }
Connection
interface and are suitable for use in the Http
class for organizing data exchange over the network: class NodeHttpService implements Connection { request(url: string, opts:any) { //... } } class MockHttpService implements Connection { request(url: string, opts:any) { //... } }
Http
(high-level module) depends on the interface Connection
(abstraction). The XMLHttpService
, NodeHttpService
and MockHttpService
(low-level modules) also depend on the Connection
interface.XMLHttpService
, NodeHttpService
and MockHttpService
can serve as a replacement for the base Connection
type.Source: https://habr.com/ru/post/426413/
All Articles