
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