Журналирование / репозитории структуры приложения F # и т. Д. - PullRequest
5 голосов
/ 20 сентября 2011

Я постепенно переключаюсь на F # для многих моих домашних проектов, но я немного озадачен тем, как соединять вместе готовые приложения, и, в частности, сквозные вопросы.

В C #, если я хочу регистрировать вещи, я бы использовал внедрение зависимостей для передачи ILogger в каждый класс, и тогда это можно легко и просто назвать из кода. В своих тестах я могу проверить, что для конкретной ситуации записываются журналы, передав имитирование и проверяя его.

public class MyClass
{
    readonly ILogger _logger;
    public MyClass(ILogger logger)
    {
        _logger = logger;
    }

    public int Divide(int x, int y)
    {
        if(y == 0)
        {
            _logger.Warn("y was 0");
            return 0;
        }
        return x / y;
    }
}

В F # я использую модули гораздо больше, поэтому вышеприведенное станет

module Stuff

let divde x y =
    match y with 
    | 0 -> 0
    | _ -> x / y

Теперь, если бы у меня был модуль, называемый Logging, я мог бы просто открыть его и использовать функцию log оттуда в случае, если y равно 0, но как бы я внедрил это для модульного тестирования?

Я мог бы заставить каждую функцию взять функцию журнала (string -> unit), а затем использовать частичное приложение, чтобы соединить их, но это кажется огромной работой, как если бы я создал новую функцию, которая оборачивает реальный вызов внутри входящий звонок. Есть ли какой-то конкретный шаблон или немного F #, который мне не хватает, который может это сделать? (Я видел функцию kprintf, но до сих пор не знаю, как бы вы указали функцию для различных тестовых сценариев, используя конкретную реализацию для всего приложения)

Точно так же, как бы вы заглушили хранилище, которое извлекало данные? Нужно ли создавать какой-либо класс и устанавливать на него функции CRUD, или есть способ внедрить, какие модули вы открываете (кроме #define)

Ответы [ 3 ]

3 голосов
/ 20 сентября 2011

Если вам не нужно менять регистратор во время выполнения, то использование директивы компилятора или #if для выбора между двумя реализациями регистратора (как предложил Дэниел), вероятно, является лучшим и самым простым способом.

С функциональной точки зрения внедрение зависимостей означает то же самое, что параметризация всего вашего кода с помощью функции регистрации.Плохо то, что вам нужно распространять функцию повсюду (и это делает код немного грязным).Вы также можете просто создать глобальную изменяемую переменную в модуле и установить для нее экземпляр некоторого интерфейса ILogger - я думаю, что это на самом деле вполне приемлемое решение для F #, потому что вам нужно изменять эту переменную только в нескольких местах.

Другая (более «чистая») альтернатива - определить рабочий процесс (он же монада) для ведения журнала.Это хороший выбор, только если вы пишете весь свой код на F #.Этот пример обсуждается в главе 12 моей книги, которая доступна в качестве бесплатного образца .Затем вы можете написать что-то вроде этого:

let write(s) = log {
  do! logMessage("writing: " + s)
  Console.Write(s) }

let read() = log {
  do! logMessage("reading")
  return Console.ReadLine() }

let testIt() = log {
  do! logMessage("starting")
  do! write("Enter name: ")
  let! name = read()
  return "Hello " + name + "!" }

Хорошая вещь в этом подходе состоит в том, что это хороший функциональный прием.Тестирование кода должно быть простым, потому что функция, которая возвращает Log<'TResult>, по существу дает вам значение, а также запись его побочных эффектов, так что вы можете просто сравнить результаты!Тем не менее, это может быть излишним, потому что вам придется обернуть все вычисления, которые используют ведение журнала в блоке log { .. }.

2 голосов
/ 20 сентября 2011

Это основной ответ.Во-первых, кажется, вы думаете о классах и модулях как о взаимозаменяемых.Классы инкапсулируют данные и в этом смысле более аналогичны записям и DU.Модули, с другой стороны, инкапсулируют функциональность (они скомпилированы в статические классы).Итак, я думаю, что вы уже упомянули свои варианты: частичное применение функции, передача функций в виде данных или ... внедрение зависимостей.В вашем конкретном случае проще всего сохранить то, что у вас есть.

Альтернативой является использование директив препроцессора для включения различных модулей.

#if TESTING 
open LogA
#else
open LogB
#endif

DI не обязательно плохо подходит для функционального языка.Стоит отметить, что F # делает определение и выполнение интерфейсов еще проще, чем, скажем, C #.

0 голосов
/ 20 сентября 2011

Вот реализация вашего кода C # на F #, которая похожа на исходную C #

module stuff

type MyClass(logger:#Ilogger) =
    member x.Divide a b =
        if b=0 then 
            logger.Warn("y was 0")
            0
        else
            x/y

это должно позволить вам использовать те же методы, которые вы знаете из C # для ведения журнала

...