Como dividir o complexo se as condições incluírem a ligação opcional?

5

Eu tenho lido recentemente o Código Limpo e havia um exemplo muito bom em java de quando é benéfico dividir uma complexa condição de instrução if em uma função em vez de usar um comentário.

// Check to see if the employee is eligible for full benefits
if ((employee.flags && HOURLY_FLAG) && (employee.age > 65))

vs

if (employee.isEligibleForFullBenefits() )

Gostaria de poder utilizar isso rapidamente, mas a natureza da ligação opcional torna isso difícil.

if let employeeFlags = employee.flags,
    let hourlyFlag = self.hourlyFlag,
    employee.age > 65 {
        //Do stuff with unwrapped optionals
    }

Não consigo agrupar a condição em uma função bem nomeada sem desdobrar a força, pois os opcionais que eu desembrulhei não estão mais no escopo. Isso é melhor para ler, mas alguém deve alterar isEligibleForFullBenefits , poderíamos falhar quando a força desembrulhar um valor nulo.

if employee.isEligibleForFullBenefits {
    //Do something with:
    employee.flags!
    hourlyFlags!
}

Outra tentativa minha foi criar uma função para a condição e o bloco de código da instrução if:

tryToDoSomethingWithEmpolyeeBenefits() ou doSomethingWithEmployeeBenefitsIfElligible()

Eu evito o desdobramento de força, mas talvez eu esteja quebrando o princípio de responsabilidade única, uma vez que minha função verifica uma condição e executa uma ação com base nessa condição?

Uma delas é uma boa solução? Existem soluções melhores? Ou simplesmente não é uma boa ideia usar swift?

    
por Declan McKenna 17.03.2018 / 16:28
fonte

1 resposta

2

Existem algumas abordagens diferentes que podemos seguir para simplificar o seu código de exemplo.

Torne-os não opcionais (também conhecidos como objetos nulos e valores padrão)

A maneira mais direta de não se preocupar em desembrulhar um tipo opcional seria não ter tipos opcionais. Se houver padrões sãos, poderemos escrever acessadores que sempre retornarão um valor e não um opcional. Valores padrão são apropriados para tipos simples (como booleans, ints, etc) onde o código já está assumindo um padrão se o opcional não estiver presente e o mesmo valor padrão fizer sentido em todos os lugares em que tentarmos usar o valor. Objetos nulos desempenham um papel semelhante para tipos mais complexos. Objetos nulos implementam a mesma interface que o objeto real, mas "não fazem nada". Os acessadores podem retornar padrões sãos e os métodos de comando podem ter implementações sem operação. Se você estiver usando um tipo de coleção (uma matriz, conjunto, dicionário, etc), uma coleção vazia é um objeto nulo efetivo.

Se não houver valores padrão saudáveis, você não poderá usar essa estratégia. Se você precisar representar objetos nulos grandes e complexos, manter uma classe grande de "não fazer nada" pode ser um fardo pesado.

Parâmetro da função Lambda

Em vez de um método employee.isEligibleForFullBenefits que retorna um booleano, poderíamos ter um método employee.whenEligibleForFullBenefits que usa uma função lambda. A função lambda levaria os valores desembrulhados de que precisa. Só seria chamado se esses valores existissem. Então, o código ficaria assim:

class Employee {
    func whenEligibleForFullBenefits(
            hourlyFlags optionalHourlyFlags: HourlyFlagsType?,
            thenDo: (EmployeeFlagsType, HourlyFlagsType) -> void) {
        if let employeeFlags = self.flags,
            let hourlyFlags = optionalHourlyFlags,
            self.age > 65 {
            thenDo(employeeFlags, hourlyFlags)
        }
    }
}

Isso faz sentido se esta é uma condição comum que você tem, mas o que você faz com base na condição varia muito. A condicional é encapsulada e o comportamento é injetado.

Se em vez disso, esta é apenas uma condicional única entre muitos, você rapidamente encontrará métodos como este proliferando com menos benefícios. Esse pode ser o caso, mesmo se a condicional for a mesma, mas os parâmetros de entrada ou os valores necessários diferirem. Ou, se essa condição é comumente vinculada ao mesmo comportamento, não queremos transmiti-la, pois estaríamos duplicando essa lógica em todos os lugares.

Diga não pergunte

Seu exemplo parece muito interessado nos diferentes valores dentro do funcionário. Em vez de interrogar o funcionário sobre diferentes informações, informe ao funcionário o que você deseja fazer. Isso retorna ao ponto original que eles estavam fazendo em código limpo . Também é o que você está tentando fazer, colocando o código em um método doSomethingWithEmployeeBenefitsIfEligible() .

Sem ver seu código exato, é difícil dizer se essa é uma solução ideal. Se houver muitas algumas coisas diferentes que poderíamos fazer, acabaríamos com muitos métodos em Employee , o que se tornaria difícil de manter.

Torne-o invariante

Você está preocupado que uma alteração no método isEligibleForFullBenefits() faça com que você desdobre um valor nulo. Você pode declarar que o método isEligibleForFullBenefits() que retorna true garante implicitamente que os valores não são nulos como parte de seu contrato. Quando alguém altera o método, eles precisam garantir que as invariantes ainda sejam válidas. Para verificar isso automaticamente, escreva testes automatizados detalhados para verificar as várias combinações de estados e que sua invariante é correta. Se alguém fizer uma alteração que viole suas invariantes, os testes falharão.

"Declarar" uma invariante muitas vezes não pode ser verificado por um compilador, mas pode ser verificado em tempo de execução, seja através de testes ou através de asserções. Isso significa que há algum atraso no feedback se alguém fizer uma alteração incorreta. Eles também podem fazer uma alteração que não é verificada pelos testes automatizados, permitindo que a invariante seja quebrada de maneira não intencional. Algumas invariantes não podem ser verificadas pelo código, mas podem ser especificadas apenas nos comentários. A diligência do desenvolvedor é a única salvaguarda na época.

    
por 18.03.2018 / 22:51
fonte