Предотвращение дублирования кода в F# при создании приложения на основе форм для нескольких типов данных
В настоящее время я делаю приложение на F# с архитектурой fable elmish, в котором типы записей следующие (сокращено, чтобы сэкономить место, но, надеюсь, вы поняли идею).
type NewOriginMerdEntry =
| AddOriginMerdID of string
| AddMerdNumber of int
| AddAverageWeight of float
| AddPD of int
type NewTreatmentEntry =
| AddTreatmentID of string
type NewDestMerdEntry =
| AddDestMerdID of string
....etc
Теперь я скомпилировал их в различный тип объединения, такой как этот
type NewEntry =
| NewOriginMerdEntry of NewOriginMerdEntry
| NewTreatmentEntry of NewTreatmentEntry
| NewDestMerdEntry of NewDestMerdEntry
...etc
наконец, основной тип сообщения выглядит следующим образом:
type Msg = {
NewEntry of NewEntry
}
Что достаточно чисто, однако в функции представления мне нужно создать новую функцию для каждого типа, символизирующую представление и конкретные сообщения, которые необходимо отправлять при изменении ввода текста.
Что-то вроде этого:
let originMerdView (dispatch : Msg -> unit) (model : Model) =
let dispatch' = NewOriginMerdEntry >> NewEntry >> dispatch
let form = match model.form with
| OriginMerd o -> o
| _ -> None
R.scrollView[
P.ViewProperties.Style [
P.FlexStyle.FlexGrow 1.
P.BackgroundColor "#000000"
]
][
//these functions are simply calls to various input text boxes
inputText "ID" AddOriginMerdID dispatch'
numinputText "MerdNumber" AddMerdNumber dispatch'
floatinputText "average Weight" AddAverageWeight dispatch'
numinputText "PD" AddPD dispatch'
button "save" form SaveOriginMerd (SaveEntry >> dispatch)
]
let inputText label msg dispatch =
R.textInput[
P.TextInput.OnChangeText ( msg >> dispatch )
]
Поэтому первый вопрос заключается в том, можно ли как-то обобщить это, так как основное решение решит, какие из этих функций запускать, основываясь на состоянии модели. Это работает нормально, но количество повторений кода довольно болезненно.
Также каждая новая запись будет отправлена этой функции:
let handleNewEntry (model : Model) (entry : NewEntry) =
match entry with
| NewOriginMerdEntry e -> handleNewOriginMerdEntry model e
... etc
let handleNewOriginMerdEntry (model : Model) (entry : NewOriginMerdEntry) =
let form =
match model.form with
| OriginMerd o -> match o with
| Some f -> f
| None -> OriginMerd.New
| _ -> failwithf "expected origin type got something else handleNewOriginMerd"
let entry =
match entry with
| AddOriginMerdID i -> {form with originMerdID = i}
| AddMerdNumber n -> {form with merdNumber = n}
| AddPD p -> {form with pD = p}
| AddAverageWeight w -> {form with averageWeight = w}
{model with form = OriginMerd (Some entry)}, Cmd.none
Все специфические дескрипторы новых функций ввода одинаковы, за исключением, очевидно, разных записей. Это работает нормально, но повторное использование кода очень болезненно. Есть ли более элегантный способ достижения того же результата с меньшим количеством повторений кода?
1 ответ
Мне кажется, что эта часть, по крайней мере, на ваш взгляд, будет распространена:
let form = match model.form with
| OriginMerd o -> o // With a different match target each time
| _ -> None
R.scrollView[
P.ViewProperties.Style [
P.FlexStyle.FlexGrow 1.
P.BackgroundColor "#000000"
]
]
Я думаю, вы могли бы вытащить это в свою собственную функцию, и просто список полей ввода будет другим. И, что самое важное, передайте части вашей модели этим функциям. Например,
let originMerdForm (dispatch : Msg -> unit) (OriginMerd form) =
let dispatch' = NewOriginMerdEntry >> NewEntry >> dispatch
[
//these functions are simply calls to various input text boxes
inputText "ID" AddOriginMerdID dispatch'
numinputText "MerdNumber" AddMerdNumber dispatch'
floatinputText "average Weight" AddAverageWeight dispatch'
numinputText "PD" AddPD dispatch'
button "save" form SaveOriginMerd (SaveEntry >> dispatch)
]
let destMerdForm (dispatch : Msg -> unit) (DestMerd form) =
let dispatch' = NewDestMerdEntry >> NewEntry >> dispatch
[
inputText "ID" AddDestMerdID dispatch'
button "save" form SaveDestMerd (SaveEntry >> dispatch)
]
let getFormFields (model : Model) =
match model.form with
| OriginMerd _ -> originMerdForm model.form
| DestMerd _ -> destMerdForm model.form
// etc.
| _ -> []
let commonView (dispatch : Msg -> unit) (model : Model) =
R.scrollView[
P.ViewProperties.Style [
P.FlexStyle.FlexGrow 1.
P.BackgroundColor "#000000"
]
] (getFormFields model)
Обратите внимание, что, как я написал это, вы получите предупреждение "неполное совпадение" на OriginMerd form
а также DestMerd form
части соответствующих функций. Я действительно хотел, чтобы у них был тип записи (o
в вашей OriginMerd o
строка вашего исходного кода), но я не знаю, как вы это назвали. Единственное изменение, которое должно произойти, это то, что вы хотите извлечь button
призыв к общему мнению, например,
let commonView (dispatch : Msg -> unit) (model : Model) =
let formFields, saveMsg = getFormFields model
R.scrollView[
P.ViewProperties.Style [
P.FlexStyle.FlexGrow 1.
P.BackgroundColor "#000000"
]
] (formFields @ [button "save" model.form saveMsg (SaveEntry >> dispatch))
а потом твой originMerdForm
, destMerdForm
вернет кортеж (form fields, msg)
где msg
было бы SaveOriginMerd
, SaveDestMerd
, и так далее.
Твой handleNewFooEntry
Функции также могут извлечь выгоду из аналогичного изменения входных параметров: вместо передачи всей модели вы можете передать только соответствующий тип записи (и переименовать ваш entry
параметр для msg
пожалуйста, чтобы не путать себя). Т.е. это будет выглядеть примерно так:
let handleNewEntry (model : Model) (msg : NewEntry) =
let form' =
match msg, model.form with
| NewOriginMerdEntry m, OriginMerd o -> handleNewOriginMerdEntry o m
| NewOriginMerdEntry m, _ -> failwithf "expected origin type got something else"
| NewDestMerdEntry m, DestMerd d -> handleNewDestMerdEntry d m
| NewDestMerdEntry m, _ -> failwithf "expected dest type got something else"
{model with form = form'}, Cmd.none
let handleNewOriginMerdEntry (formOpt : OriginMerdEntry option) (msg : NewOriginMerdEntry) =
let form = formOpt |> Option.defaultValue OriginMerd.New
let result =
match msg with
| AddOriginMerdID i -> {form with originMerdID = i}
| AddMerdNumber n -> {form with merdNumber = n}
| AddPD p -> {form with pD = p}
| AddAverageWeight w -> {form with averageWeight = w}
OriginMerd (Some result)
let handleNewDestMerdEntry (formOpt : DestMerdEntry option) (msg : NewDestMerdEntry) =
let form = formOpt |> Option.defaultValue DestMerd.New
let result =
match msg with
| AddDestMerdID i -> {form with destMerdID = i}
DestMerd (Some result)
Каждый раз, когда вы говорите: "Эй, здесь много повторений", обычно есть способ извлечь его из общей функции. Система типа F# - ваш друг здесь: когда вы глубоко погружены в рефакторинг, подобный этому, вы не всегда будете помнить, какие функции вы уже изменили, а какие нет. Просто посмотрите на красные волнистые линии, и вы поймете, над какими функциями вам еще нужно работать. Надеемся, что этот пример вдохновит вас на поиск другого распространенного кода, который можно извлечь в его собственную функцию.