Princípio de Substituição de Liskov (Liskov substitution principle – LSP)

O principio de substituição de Liskov define :

“Os subtipos devem ser substituíveis pelos seus tipos base”

Esse é um principio q confunde muitos programadores, porém é bem simples depois que se entende ele. Um termo muito utilizado no aprendizado do conceito de herança é o termo “É-um”. Esse termo é muito amplo para definição de um subtipo. A melhor forma de definição para subtipo é substituível, quando a substituição é definida por uma interface explicitamente ou implicitamente.

Desing by contract é uma pratica que ajuda a seguir esse principio. Ela define para que não seja criado variáveis de classes concretas, deve ser criado as variáveis a partir de contratos explícitos.

Esse principio alerta com o uso incorreto de polimorfismo, é necessário ter uma visão muito correta para generalizar ou especificar uma propriedade ou um parâmetro de um método em uma classe. Para deixar os termos que estou usando mais claros:

  • Generalizar: Quando a variável em questão é um tipo base, uma interface, uma classe abstrata ou qualquer outro tipo de objeto que é possível passar um objeto derivado dele.
  • Especializar: Quando a variável em questão é o objeto concreto, só é possível passar um objeto daquele determinado tipo.

A parte mais importante para entender esse principio, é que não se utilizar uma classe base quando alguma classe derivada dele não pode ser substituível por ele. Caso a classe derivada remova alguma funcionalidade da classe base ou ocasione um erro nas funcionalidades da classe base.

Exemplo:Vou tentar mostrar em uma cenário bem bestinha, mas é para entender realmente o conceito. Voce tem uma classe pessoa e ela tem dois métodos CalcarPeDireito e CalcarPeEsquerdo. Os dois metodos recebem dois parametros um Sapato(classe abstrata) e uma Meia(classe abstrata), fazendo o uso de generalização.  A estrutura de classes do Sapato e da Meia são essas:

    /// <summary>
    /// Meia
    /// </summary>
    public abstract class Sock
    {
        protected Sock()
        {

        }
        public abstract string Side { get; }

    }

    public  class RightSock : Sock
    {
        public RightSock()
        {

        }
        public override string Side
        {
            get { return "Right"; }
        }
    }

    public  class LeftSock : Sock
    {
        public LeftSock()
        {

        }
        public override string Side
        {
            get { return "Left"; }
        }
    }

    /// <summary>
    /// Sapato
    /// </summary>
    public abstract class Shoe
    {
        public abstract string Side { get; }

        protected Shoe()
        {

        }
    }

    public class LeftShoe : Shoe
    {
        public LeftShoe()
        {

        }
        public override string Side
        {
            get { return "Left"; }
        }
    }

    public class RightShoe : Shoe
    {
        public RightShoe()
        {

        }
        public override string Side
        {
            get { return "Right"; }
        }
    }

A classe pessoa está desenvolvida assim:

    public static class Person
    {
        /// <summary>
        /// Calçar Pé esquerdo
        /// </summary>
        /// <param name="shoe"> Can't be any show only the left</param>
        /// <param name="sock"> There is no diference</param>
        public static void PuttingOnLeftFootBreakingLSP(Shoe shoe, Sock sock)
        {
            Console.WriteLine("Putting socks {0} on the left foot!", sock.Side);
            if (shoe.Side != "Left")
            {
                Console.WriteLine("Wrong foot!");
            }
            else
            {
                Console.WriteLine("Putting shoes {0} on the left foot!", shoe.Side);
            }
        }

        /// <summary>
        /// Calçar Pé Direito
        /// </summary>
        /// <param name="shoe"></param>
        /// <param name="sock"> There is no diference</param>
        public static void PuttingOnRightFootBreakingLSP(Shoe shoe, Sock sock)
        {
            Console.WriteLine("Putting socks {0} on the righ foot!", sock.Side);
            if (shoe.Side != "Right")
            {
                Console.WriteLine("Wrong foot!");
            }
            else
            {
                Console.WriteLine("Putting shoes {0} on the righ foot!", shoe.Side);
            }
        }
    }

Com isso nada impede de eu fazer isso no codigo:

     Person.PuttingOnLeftFootBreakingLSP(new RightShoe(), new RightSock());
     Person.PuttingOnRightFootBreakingLSP(new LeftShoe(), new RightSock());

Ok, agora para resolver esse problema e seguir o principio de Substituição de Liskov como podemos alterar isso? Muito simples, utilizaremos a generalização ainda é possível, no caso no paramento da meia, pois a meia não tem problema se colocar invertido. E usaremos a especialização onde é necessário, no caso no sapato, utilizaremos em em seus respectivos  métodos o sapato do lado correto, como mostra o código a seguir:

 public static class Person
    {
        /// <summary>
        /// Calçar Pé esquerdo
        /// </summary>
        /// <param name="shoe"></param>
        /// <param name="sock"></param>
        public static void PuttingOnLeftFootUsingLSP(LeftShoe shoe, Sock sock)
        {
            Console.WriteLine("Putting socks {0} on the left foot!", sock.Side);
            Console.WriteLine("Putting shoes {0} on the left foot!", shoe.Side);
        }

        /// <summary>
        /// Calçar Pé Direito
        /// </summary>
        /// <param name="shoe"></param>
        /// <param name="sock"></param>
        public static void PuttingOnRightFootUsingLSP(RightShoe shoe, Sock sock)
        {
            Console.WriteLine("Putting socks {0} on the righ foot!", sock.Side);
            Console.WriteLine("Putting shoes {0} on the righ foot!", shoe.Side);
        }

    }

Assim quando um desenvolvedor utilizar essa classe ele pode utilizar o polimorfismo com os parâmetros possíveis, mais os que não é possível generalizar ele é obrigado a passar a classe concreta corretamente, isso da muito mais segurança na codificação. Isso faz com que aquele código de forma incorreta na classe Pessoa não seja mais permitido.

Todo o código deste post está no github neste link.

Anúncios

Criar eventos ao adicionar item em List C#

Isso é algo realmente chato as vezes, você precisa fazer uma ação antes de que qualquer item seja inserido em determinada lista. Isso as vezes se torna bem chato se o sistema não tem um encapsulamento das propriedades bem organizado.

Tem um projeto no Github(HAL – Help a lot) exatamente com uma coleção que faz isso, eu iniciei esse projeto agrupar funções corriqueiras, que as vezes é necessário criar em diversos sistemas, ou cada empresa tem sua DLL mágica que tem as funções básicas. O código está totalmente aberto quem quiser olhar o código, tem alguma sugestão ou quer ajudar no desenvolvimento só entrar em contato comigo e pode começar a desenvolver.  Caso queria fazer o download Apenas  DLL.

Bom vamos ao problema, precisamos de uma lista que precisa interceptar a ação dos métodos Add, Insert, Remove e RemoveAt de uma lista para poder colocar o evento antes ou depois dessas ações. O que vou fazer é criar uma classe genérica que implementa IList<T> e aplicar o padrão de projetos Proxy para interceptar as ações necessárias. O código ficara assim:

    public class HalList<T> : IList<T>
    {
        public delegate void AddItemDelegate(T item);
        public delegate void RemoveItemDelegate(T item);

        public delegate void InsertItemDelegate(int index, T item);
        public delegate void RemoveAtItemDelegate(int index, T item);

        public event InsertItemDelegate BeforeInsertItem;
        public event InsertItemDelegate AfterInsertItem;

        public event RemoveAtItemDelegate BeforeRemoveAtItem;
        public event RemoveAtItemDelegate AfterRemoveAtItem;

        public event AddItemDelegate BeforeAddItem;
        public event AddItemDelegate AfterAddItem;

        public event RemoveItemDelegate BeforeRemoveItem;
        public event RemoveItemDelegate AfterRemoveItem;

        private IList<T> _list;

        public HalList()
        {
            _list = new List<T>();
        }

        public IEnumerator<T> GetEnumerator()
        {
            return _list.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        public void Add(T item)
        {
            if (BeforeAddItem != null)
                BeforeAddItem(item);

            _list.Add(item);

            if (AfterAddItem != null)
                AfterAddItem(item);
        }

        public void Clear()
        {
            _list.Clear();
        }

        public bool Contains(T item)
        {
            return _list.Contains(item);
        }

        public void CopyTo(T[] array, int arrayIndex)
        {
            _list.CopyTo(array, arrayIndex);
        }

        public bool Remove(T item)
        {
            if (BeforeRemoveItem != null)
                BeforeRemoveItem(item);

            bool ret = _list.Remove(item);

            if (AfterRemoveItem != null)
                AfterRemoveItem(item);
            return ret;
        }

        public int Count
        {
            get { return _list.Count(); }
        }

        public bool IsReadOnly
        {
            get { return false; }
        }

        public int IndexOf(T item)
        {
            return _list.IndexOf(item);
        }

        public void Insert(int index, T item)
        {
            if (BeforeInsertItem != null)
                BeforeInsertItem(index, item);

            _list.Insert(index, item);

            if (AfterInsertItem != null)
                AfterInsertItem(index, item);
        }

        public void RemoveAt(int index)
        {

            if (_list.Count <= index)
                throw new HalException("Index out of range on the List!");

            T item = _list[index];

            if (BeforeRemoveAtItem != null)
                BeforeRemoveAtItem(index, item);

            _list.RemoveAt(index);

            if (AfterRemoveAtItem != null)
                AfterRemoveAtItem(index, item);
        }

        public T this[int index]
        {
            get { return _list[index]; }
            set { _list[index] = value; }
        }
    }

Pode ser questionado o motivo de deixar tudo separa, mas estou ainda pensando se vou implementar alguma coisa diferente para cada um ou colocar em todos, esse ainda é o código inicial no projeto e pode ser alterado, mas esse código já está testado.

Princípio de Segregação de Interface(Interface segregation principle – ISP)

O principio de segregação de interface e:

“Os clientes não devem ser obrigados a depender de métodos

que não utilizam

Esse principio ajuda a evitar a criação de fat interfaces(interfaces gordas), termo utilizado para interfaces com mais funcionalidades do que o necessário. Classes que implementam uma interface assim não são coesas. As interfaces podem ser divididas em grupos de métodos, e cada grupo  atende uma conjunto diferentes de classes, cada classe pode implementar apenas as funcionalidades que fazem sentido;

Uma dos indicadores para identificar a quebra deste principio é no seguinte cenário você ter uma interface com 4 funcionalidades porém ao implementar essa interface em uma classe faz sentido todas os métodos. Porém em outra classes uma funcionalidade ou outra não faz sentido ser implementada.

Uma boa dica para evitar a quebra desse principio é sempre que adicionar um método em uma interface, analise quem implementa essa interface e se aquele método faz sentido para todas as classes que implementam. Se tiver sentido adicione nessa interface sem nenhum problema, caso não faça sentido crie um outra interface(ou verifique se faz sentido em outra interface já existente) para adicionar o método que você precisa implementar.

Indicadores de quebra do principio:

  • Métodos de classes, implementados com base em uma interface, retornando valores padrões ou jogando exceções
  • Implementações de métodos que não fazem sentido para a classe
  • Pouco sentido nas interfaces que existem no sistema
  • Muita alterações no código para adicionar um novo método na interface.
  • Ao chamar um método em uma classe não ter certeza se ele foi realmente implementado(isso acontece e bastante)

Exemplo: Digamos que você tenha uma interface IVeiculo e uma classe Carro, como descrito abaixo e agora é necessário implementar no sistema um objeto moto. Nesse exemplo a criação do objeto moto deve  implementar a interface IVeiculo. O Exemplo que quebra o principio ISP é assim:

    public interface IVehicleBreakingISP
    {
        void TurnOn();
        void TurnOff();

        void OpenDoor();
        void CloseDoor();
    }

    public class CarBreakingISP : IVehicleBreakingISP
    {

        public void TurnOn()
        {
            Console.WriteLine("Turn ON the car!!!!");
        }

        public void OpenDoor()
        {
            Console.WriteLine("Door is OPEN!!!!");
        }

        public void CloseDoor()
        {
            Console.WriteLine("Door is CLOSE!!!!");

        }

        public void TurnOff()
        {
            Console.WriteLine("Turn OFF the car!!!!");
        }
    }

    public class MotorcycleBreakingISP : IVehicleBreakingISP
    {
        public void TurnOn()
        {
            Console.WriteLine("Turn ON the Motorcycle!!!!");
        }

        public void TurnOff()
        {
            Console.WriteLine("Turn OFF the Motorcycle!!!!");

        }

        /// <summary>
        /// Do Not Call this
        /// </summary>
        public void OpenDoor()
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Do Not Call this
        /// </summary>
        public void CloseDoor()
        {
            throw new NotImplementedException();
        }
    }

Uma refatoração viável para esse necessário ficar de acordo com o principio é assim:

    public interface IHasDoors
    {
        void OpenDoor();
        void CloseDoor();
    }
    public interface IVehicle
    {
        void TurnOn();
        void TurnOff();
    }

    public class Car : IVehicle, IHasDoors
    {

        public void TurnOn()
        {
            Console.WriteLine("Turn ON the car!!!!");
        }

        public void OpenDoor()
        {
            Console.WriteLine("Door is OPEN!!!!");
        }

        public void CloseDoor()
        {
            Console.WriteLine("Door is CLOSE!!!!");

        }

        public void TurnOff()
        {
            Console.WriteLine("Turn OFF the car!!!!");
        }
    }

    public class Motorcycle : IVehicle
    {
        public void TurnOn()
        {
            Console.WriteLine("Turn ON the Motorcycle!!!!");
        }

        public void TurnOff()
        {
            Console.WriteLine("Turn OFF the Motorcycle!!!!");

        }

    }

Foi separado o grupo de responsabilidades de veículos e veículos com portas, assim o veiculo moto apenas implementa a interface de IVeiculo, enquanto que o carro precisa implementar a interface IVeiculo e ITemPortas, pois faz sentido.

Caso tenha curiosidade de como pensar assim separando as interfaces, tente implementar essas duas funcionalidades:

  • Quero poder abrir o porta malas no meu objeto carro.
  • Quero que seja implantando outro veiculo com nome de avião e colocado as devidas responsabilidades nele a partir de interface.

Apenas uma dica para o uso desse principio, ele ajuda muito a organizar o código, porém é necessário fazer o máximo para programar para a interface. Isso quer dizer que você não deve declarar uma variável com o tipo final e sempre tentar usar as interfaces para diminuir o acoplamento do código. Isso deve ser feito quando possível, tem casos que isso pode não parecer valido.

Todo o código deste post está no github neste link.

Princípio de Aberto e Fechado(Open/closed principle – OCP)

O Princípio de Aberto e Fechado define:

“As entidades de software(classes, módulos, funções etc) devem

ser abertas para ampliação, mas fechadas para modificação”

A palavra chave deste princípio é a abstração quer qualquer linguagem de programação orientada a objetos possui esse recurso.Imagine o seguinte cenário você tem dois sistemas o Sistema A e o Sistema B, eles possuem uma integração aonde o Sistema A chama um determinado método de um determinado objeto do Sistema B. Se existir uma abstração dessa integração(interface), é possível implementar uma nova funcionalidade no Sistema B sem que seja alterado a chamada do Sistema A, pois a alteração feita no sistema B irá seguir a mesma interface de comunicação.

Um sistema que quebra esse princípio se torna um sistema dificilmente modificado, pois ao alterar uma parte do sistema pode afetar outras partes, isso mostra muita rigidez nas relações do sistema.

É necessário muito cuidado com ao seguir esse princípio, muitas classes devem sofrer uma refatoração e deverão ser alteradas para uma abstração do problema, mas não é recomendável aplicar abstração desenfreada em todas as partes do aplicativo. É necessário muita dedicação para aplicar abstração só nas partes do sistema que exibem alterações frequentes.Segundo o Robert C Martin “Resistir à abstração precipitada é tão importante quando a abstração em si”.

Sintomas mais aparentes da quebra deste principio:

  • Dificuldade nas alterações
  • Relações feitas sem abstração
  • Alterações em código funcionado para implementação de novas funcionalidades
  • Rigidez do sistema

Exemplos de casos e sua refatoração:

Exemplo: Digamos que tenha uma funcionalidade que desenha formas geométricas em uma aplicação console,  e já existe um código porém ele está difícil de adicionar novas formas para serem desenhadas assim é necessário uma refatoração para ficar de forma mais fácil a implementação de novas formas para desenhas. O código atual esta assim:

public class DrawingShapesWithoutOcp
    {
        public static void DrawShape(string shape)
        {
            switch (shape)
            {
                case "square":
                    Console.WriteLine("SQUARE");
                    Console.WriteLine(" -----------");
                    Console.WriteLine("|           |");
                    Console.WriteLine("|           |");
                    Console.WriteLine("|           |");
                    Console.WriteLine("|           |");
                    Console.WriteLine(" -----------");
                    break;

                case "rectangle":
                    Console.WriteLine("RETANGLE");
                    Console.WriteLine("     -------------");
                    Console.WriteLine("    /              \\");
                    Console.WriteLine("  /                  \\");
                    Console.WriteLine("/                      \\");
                    Console.WriteLine("------------------------");
                    break;
            }
        }
    }

Um código dessa forma que tem previsão de crescimento é muito importante que seja refatorado para nao sofrer dificuldade ou problemas no futuro, isso um exemplo que acontece muito em sistemas existentes. Isso é realmente um indicio para iniciar a refatorar o sistema!
Esse código até pode parecer simples rápido e pode ser facilmente implementado novas formas, porém imagina você criar mais de 100 formas com essa estrutura de código, ai se torna inviável manter um código assim. Esse código também quebra totalmente o principio de aberto e fechado, pois ele esta fechado para extensão, todas vez é necessário alterar o código que já esta funcionando para adicionar um novo comportamento ou funcionalidade.
Primeiro vamos separar o comportamento então criaremos uma abstração do comportamento que queremos com a interface IShape e depois criaremos as implementações dessa interface para cada forma que precisamos que precisamos:

    public interface IShape
    {
        //behavior
        void DrawShape();
    }

    public class RectangleShape : IShape
    {
        public void DrawShape()
        {
            Console.WriteLine("RETANGLE");
            Console.WriteLine("     -------------");
            Console.WriteLine("    /              \\");
            Console.WriteLine("  /                  \\");
            Console.WriteLine("/                      \\");
            Console.WriteLine("------------------------");
        }
    }

    public class SquareShape : IShape
    {
        public void DrawShape()
        {
            Console.WriteLine("SQUARE");
            Console.WriteLine(" -----------");
            Console.WriteLine("|           |");
            Console.WriteLine("|           |");
            Console.WriteLine("|           |");
            Console.WriteLine("|           |");
            Console.WriteLine(" -----------");
        }
    }
Depois disso vamos criar uma classe para executar esse comportamento, ela irá utilizar a interface para garantir uma abstração do problema, assim deve ficar a classe.
    public class DrawingShapes
    {
        IList<IShape> _shapes = new List<IShape>();

        public void AddShape(IShape shape)
        {
            _shapes.Add(shape);
        }
        public void RemoveShape(IShape shape)
        {
            _shapes.Remove(shape);
        }

        public void DrawImages()
        {
            foreach (var shape in _shapes)
            {
                shape.DrawShape();
            }
        }
    }
Apos isso você pode utilizar a classe para ver como ficaria a chamada como mostrado nesse código:
 static void Main(string[] args)
        {
            SampleOfOCP();
        }

        private static void SampleOfOCP()
        {
            //Create shapes
            IShape rectangle = new RectangleShape();
            IShape square = new SquareShape();
            DrawingShapes d = new DrawingShapes();

            //add feature to draw rectangle
            d.AddShape(rectangle);
            //drawing images
            d.DrawImages();

            Console.WriteLine("Press Any key to Contunue the sample of OCP");
            Console.ReadKey();

            //add feature to draw square
            d.AddShape(square);
            d.DrawImages();
            Console.ReadKey();
        }

Isso não traz uma complexidade muito alta de forma desnecessária?

Não, por dois motivos, mantendo o código da mesma forma, efetuar testes automatizados se torna realmente difícil efetuar testes e também é necessário alterar código em uma classe que esta funcionando para adicionar nova funcionalidade.

Se quiser fazer algo para entender melhor, implemente outras formas nos dois códigos para entender as dificuldade em uma forma e outra.

Todo o código deste post está no github neste link.