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

Сейчас покажу и поясню - и м.б. многосведущий ALL поделится как с этим в других проектах поступать решили.

Код

Функционал касается неких команд на манипуляции данными приходящих по сети / очереди и т.п. В общем ничего нового.

Проект на Go но это на суть не влияет (кроме отсутствия типов-перечислений, о чём ниже)

в одном пакете, он принадлежит библиотеке (нашей же) подключенной к проекту

package pkg1

type OperationType string

const (
	OpCreate  OperationType = "create"
	OpReplace OperationType = "replace"
	OpDelete  OperationType = "delete"
)

в другом пакете, уже непосредственно внутри данного проекта

type myOperation struct {
	Op     string
    // ... другие поля
}


func Convert(mop myOperation) (pkg1.Operation, error) {
    var op pkg1.Operation
  
	switch mop.Op {
	case "create":
		op.Op = pkg1.OpCreate
	case "replace":
		op.Op = pkg1.OpReplace
	case "delete":
		op.Op = pkg1.OpDelete
	default:
		return pkg1.Operation{}, fmt.Errorf("%w: %s", ErrUnknownOperation, mop.Op)
	}
  
    // ...
    return op, nil
  }

Проблема (?)

Нетрудно заметить что первые 7 строчек в функции Convert занимаются преобразованием строковых значений в значения типа pkg.OperationType - которые тоже являются строками (но объявлены как кастомный тип).

Хуже того "преобразование" в данном конкретном случае оказывается идентичным - потому что исходные и результирующие значения совпадают.

То есть по идее можно написать просто:

op.Op = pkg1.OperationType(mop.Op)

Да, нам ещё хочется сделать проверку на невалидное значение. Скорее всего это можно сделать там где операции выполняются - но при большом желании можно и здесь добавить:

ops := []pkg1.OperationType{pkg1.OpCreate, pkg1.OpReplace, pkg1.OpDelete}
if slices.Index(ops, mop.Op) == -1 {
  return  ... // error as above
}

По-хорошему этот массив допустимых значений можно объявить в pkg1, раз уж нет энумов (ах Go, ты такой Go)...

Почему так

Разумный довод: возможно "на входе" названия операций могут в будущем отличаться, например "create" и "delete" так и останутся, а "replace" кто-нибудь на "update" заменит.

А может вообще pkg1.OperationType переделают на числовые значения (идиоматический способ создания псевдо-перечислений в Go).

Ну, всякое бывает, но тут два соображения:

  • вот если в будущем будут отличаться, то может в будущем тогда и усложнять код, а сейчас ограничиться присваиванием?

  • как минимум можно использовать map вместо switch.

Конечно наш просветлённый разум продвинутого разработчика сопротивляется идее что "можно просто присвоить" - кажется что это потенциальное место для ошибки (опять же в будущем). Но наверняка функционал покрыт тестами и ошибку легко заметим.

Отдельно стоит соображение - может в структуре myOperation следовало использовать этот сторонний тип для поля Op - объявить его не string а такой же pkg1.OperationType?

Этот вариант вероятно мало найдёт сторонников т.к. получается весь внутренний функционал нашего пакета будет связан со значениями из пакета стороннего. Хотя может не так страшно?

Плюнуть на всё

Альтернативная позиция "зачем вообще заводить кастомные типы - аналоги string" - ведь их множество значений всё равно не ограничено. Сэкономили бы несколько строк в pkg1. Мы получаем только небольшую помощь от компилятора если где-то по ошибке радикально неправильную строчку передаём. Ну и подсказку о том что мы здесь ждём.

Но это ведь ловится при "проверке" в рантайме. Половина мира пишет на языках без строгой типизации, ловит такие ошибки в рантайме (желательно при тестировании а не в проде) - правит их и не заостряет внимания.


Я предпочитаю исходить из принципа KISS и делать как можно проще. Ошибки могут случаться и городить многослойные обёртки и кастомные типы в надежде изредка какую-то ошибку поймать при компиляции а не на тестах - это на мой вкус не очень хорошо. Проверки при компиляции всё равно не заменят тестирования в деле валидации поведения программы.

Но я также знаю что у сторонников строгой типизации и языков исповедующих эту идею - обычно мнение близкое к противоположному.

Поэтому прошу, пожалуйста - делитесь предпочтениями принятыми в ваших проектах (желательно с указанием языка).

Комментарии (4)


  1. AllFiction
    20.02.2026 06:22

    я энумы всегда описываю через генераторы. Как раз полностью решается проблема ручных ошибок в таком случае. Есть приведение к строкам для ошибок и выводов, и поиск по мапам для схождений, и все другое что понадобится в конкретном случае. И при этом всем оно 100% валидно потому что руками я задаю один список в yml а уже все остальное делает генератор, то есть в коде я работаю с предопределенными константами и методами пакета


    1. RodionGork Автор
      20.02.2026 06:22

      ну тут проблема-то идеологическая останется для программиста который хочет энум в "своём" пакете конвертировать в энум в "чужом" пакете


  1. AndrewBond
    20.02.2026 06:22

    Для маленькой проги можно использовать строки. Для большой - объекты поддерживают целостность программы.

    Скажем, к объекту "темература воды", у которого внутри инт, нельзя просто прибавить инт 10, нужно использовать метод "нагреть воду", который будет принимать еще и давление и следить, чтобы результат не превышал, например, 100.

    А просто "температура воды" + инт = ошибка.


  1. foggy-f
    20.02.2026 06:22

    pkg1 должен предоставлять валидацию, незачем в другом пакете таким заниматься, пакеты на то и есть чтобы изолировать в себе.
    Но если мы не можем в нем ничего поменять, то неплохо было бы внутри пакета с myOperation завести свой собственный

    package pkg2
    type MyOperationType pkg1.Operation

    и использовать его, чтобы было однозначное соответствие. Ну или


    type myOperation struct {

    Op pkg1.OperationType

    // ... другие поля}


    Может, если посмотреть на весь код станет понятней зачем это делается, но пока что выглядит явно излишним.