如何设计用于读取内存中的X个配置文件的类?

huangapple go评论74阅读模式
英文:

How to design classes for X number of config files which needs to be read individually in memory?

问题

我正在处理许多配置文件。我需要读取每个配置文件中的struct,然后创建一个包含所有其他单独配置struct的巨大Config结构。

假设我正在处理3个配置文件。

  • ClientConfig 处理一个配置文件。
  • DataMapConfig 处理第二个配置文件。
  • ProcessDataConfig 处理第三个配置文件。

我为每个单独的配置文件创建了单独的类,并在它们中都有单独的Readxxxxx方法来读取各自的配置并返回struct。下面是我的 config.go 文件,通过在 main 函数中传递 pathlogger,通过 Init 方法调用它。

config.go

package config

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "github.com/david/internal/utilities"
)

type Config struct {
    ClientMapConfigs   ClientConfig
    DataMapConfigs     DataMapConfig
    ProcessDataConfigs ProcessDataConfig
}

func Init(path string, logger log.Logger) (*Config, error) {
    var err error
    clientConfig, err := ReadClientMapConfig(path, logger)
    dataMapConfig, err := ReadDataMapConfig(path, logger)
    processDataConfig, err := ReadProcessDataConfig(path, logger)
    if err != nil {
        return nil, err
    }
    return &Config{
        ClientMapConfigs:       *clientConfig,
        DataMapConfigs:         *dataMapConfig,
        ProcessDataConfigs:     *processDataConfig,
    }, nil
}

clientconfig.go

package config

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "github.com/david/internal/utilities"
)

type ClientConfig struct {
  .....
  .....
}

const (
    ClientConfigFile = "clientConfigMap.json"
)

func ReadClientMapConfig(path string, logger log.Logger) (*ClientConfig, error) {
    files, err := utilities.FindFiles(path, ClientConfigFile)
    // 读取所有文件
    // 对所有文件进行一些验证
    // 将其反序列化为 ClientConfig 结构
    // 返回 clientconfig 对象
}

datamapconfig.go

datamapconfig 的风格与 clientconfig.go 类似。完全复制了 clientconfig.go 文件,但操作的是不同的配置文件,并返回 DataMapConfig 结构。

processdataConfig.go

processdataConfig.go 文件与 clientconfig.go 文件相同。唯一的区别是它操作不同的配置文件并返回 ProcessDataConfig 结构。

问题陈述

我正在寻找改进上述设计的想法。在 golang 中是否有更好的方法?我们可以使用接口或其他任何可以改进上述设计的方法吗?

如果我有 10 个不同的文件而不是 3 个,那么我是否需要为剩下的 7 个文件继续做同样的事情?如果是的话,代码看起来会很丑陋。任何建议或想法都将对我有很大帮助。

更新

一切看起来都很好,但是我有几个问题,因为我对如何使用您当前的建议感到困惑。在我的大多数配置中,您的建议都很完美,但是有两种情况下,我对如何做到这一点感到困惑。

  • 情况 1:将 JSON 反序列化为与 JSON 格式匹配的原始结构后,我会根据该数据创建另一个不同的结构,然后将该结构返回。
  • 情况 2:我所有的配置都有一个文件,但是有一些配置中有多个文件,而且数量不固定。因此,我传递正则表达式文件名,然后找到以该正则表达式开头的所有文件,然后逐个遍历所有这些文件。在反序列化每个 JSON 文件后,我开始填充另一个对象,并一直填充,直到所有文件都被反序列化,然后使用这些对象创建一个新的结构,然后返回它。

上述情况的示例:

示例情况 1

package config

import (
  "encoding/json"
  "fmt"
  "io/ioutil"
  "github.com/david/internal/utilities"
)

type CustomerManifest struct {
  CustomerManifest map[int64]Site
}

type CustomerConfigs struct {
  CustomerConfigurations []Site `json:"customerConfigurations"`
}

type Site struct {
  ....
  ....
}

const (
  CustomerConfigFile = "abc.json"
)

func ReadCustomerConfig(path string, logger log.Logger) (*CustomerManifest, error) {
  // 使用下面的实用方法尝试找到所有文件。
  // 使用单个文件名和正则表达式名称进行操作
  files, err := utilities.FindFiles(path, CustomerConfigFile)
  if err != nil {
    return nil, err
  }
  var customerConfig CustomerConfigs
  // 只有一个文件适用于此配置,因此循环只会运行一次
  for _, file := range files {
    body, err := ioutil.ReadFile(file)
    if err != nil {
      return nil, err
    }

    err = json.Unmarshal(body, &customerConfig)
    if err != nil {
      return nil, err
    }
  }

  customerConfigIndex := BuildIndex(customerConfig, logger)
  return &CustomerManifest{CustomerManifest: customerConfigIndex}, nil
}

func BuildIndex(customerConfig CustomerConfigs, logger log.Logger) map[int64]Site {
  ...
  ...
}

如上所示,在示例情况 1中,我从 CustomerConfigs 结构创建了 CustomerManifest 结构,然后返回它,而不是直接返回 CustomerConfigs

示例情况 2

package config

import (
  "encoding/json"
  "fmt"
  "io/ioutil"
  "github.com/david/internal/utilities"
)

type StateManifest struct {
  NotionTemplates       NotionTemplates
  NotionIndex           map[int64]NotionTemplates
}

type NotionMapConfigs struct {
  NotionTemplates      []NotionTemplates      `json:"notionTemplates"`
  ...
}

const (
  // 以 "state-" 开头的文件有很多,数量不固定
  StateConfigFile = "state-*.json"
)

func ReadStateConfig(path string, logger log.Logger) (*StateManifest, error) {
  // 使用下面的实用方法尝试找到所有文件。
  // 使用单个文件名和正则表达式名称进行操作
  files, err := utilities.FindFiles(path, StateConfigFile)
  if err != nil {
    return nil, err
  }
  var defaultTemp NotionTemplates
  var idx = map[int64]NotionTemplates{}

  // 对于此配置,有很多配置文件,因此循环将运行多次
  for _, file := range files {
    var notionMapConfig NotionMapConfigs
    body, err := ioutil.ReadFile(file)
    if err != nil {
      return nil, err
    }
    err = json.Unmarshal(body, &notionMapConfig)
    if err != nil {
      return nil, err
    }

    for _, tt := range notionMapConfig.NotionTemplates {
      if tt.IsProcess {
        defaultTemp = tt
      } else if tt.ClientId > 0 {
        idx[tt.ClientId] = tt
      }
    }
  }

  stateManifest := StateManifest{
    NotionTemplates:       defaultTemp,
    NotionIndex:           idx,
  }
  return &stateManifest, nil
}

如上所示,在我的两个示例情况中,我在完成反序列化后创建了另一个不同的结构,然后将其返回,但是根据您当前的建议,我认为我无法以通用的方式做到这一点,因为对于每个配置,我都会执行不同类型的处理,然后返回这些结构。是否有办法以通用方式实现上述功能?基本上,对于每个配置,如果我想进行一些处理,那么我应该能够做到,并返回修改后的新结构,但是对于某些情况,如果我不想进行任何处理,那么我可以直接返回反序列化的 JSON 结构。是否可以以通用方式实现这一点?

由于某些配置中有多个文件,所以我使用了我的 utilities.FindFiles 方法,根据文件名或正则表达式名称给我提供了所有文件,然后我逐个遍历所有这些文件,要么返回原始结构,要么返回在处理原始结构数据后的新结构。

英文:

I am working with lot of config files. I need to read all those individual config file in their own struct and then make one giant Config struct which holds all other individual config struct in it.

Let's suppose if I am working with 3 config files.

  • ClientConfig deals with one config file.
  • DataMapConfig deals with second config file.
  • ProcessDataConfig deals with third config file.

I created separate class for each of those individual config file and have separate Readxxxxx method in them to read their own individual config and return struct back. Below is my config.go file which is called via Init method from main function after passing path and logger.

config.go

package config

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "github.com/david/internal/utilities"
)

type Config struct {
    ClientMapConfigs   ClientConfig
    DataMapConfigs     DataMapConfig
    ProcessDataConfigs ProcessDataConfig
}

func Init(path string, logger log.Logger) (*Config, error) {
	var err error
	clientConfig, err := ReadClientMapConfig(path, logger)
	dataMapConfig, err := ReadDataMapConfig(path, logger)
	processDataConfig, err := ReadProcessDataConfig(path, logger)
	if err != nil {
		return nil, err
	}
	return &Config{
		ClientMapConfigs:       *clientConfig,
		DataMapConfigs:         *dataMapConfig,
		ProcessDataConfigs:     *processDataConfig,
	}, nil
}

clientconfig.go

package config

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "github.com/david/internal/utilities"
)

type ClientConfig struct {
  .....
  .....
}

const (
	ClientConfigFile = "clientConfigMap.json"
)

func ReadClientMapConfig(path string, logger log.Logger) (*ClientConfig, error) {
  files, err := utilities.FindFiles(path, ClientConfigFile)
  // read all the files
  // do some validation on all those files
  // deserialize them into ClientConfig struct
  // return clientconfig object back
}

datamapconfig.go

Similar style I have for datamapconfig too. Exactly replica of clientconfig.go file but operating on different config file name and will return DataMapConfig struct back.

processdataConfig.go

Same thing as clientconfig.go file. Only difference is it will operate on different config file and return ProcessDataConfig struct back.

Problem Statement

I am looking for ideas where this above design can be improved? Is there any better way to do this in golang? Can we use interface or anything else which can improve the above design?

If I have let's say 10 different files instead of 3, then do I need to keep doing above same thing for remaining 7 files? If yes, then the code will look ugly. Any suggestions or ideas will greatly help me.

Update

Everything looks good but I have few questions as I am confuse on how can I achieve those with your current suggestion. On majority of my configs, your suggestion is perfect but there are two cases on two different configs where I am confuse on how to do it.

  • Case 1 After deserializing json into original struct which matches json format, I make another different struct after massaging that data and then I return that struct back.
  • Case 2 All my configs have one file but there are few configs which have multiple files in them and the number isn't fixed. So I pass regex file name and then I find all the files starting with that regex and then loop over all those files one by one. After deserializing each json file, I start populating another object and keep populating it until all files have been deserialized and then make a new struct with those objects and then return it.

Example of above scenarios:

Sample case 1

package config

import (
  "encoding/json"
  "fmt"
  "io/ioutil"
  "github.com/david/internal/utilities"
)

type CustomerManifest struct {
  CustomerManifest map[int64]Site
}

type CustomerConfigs struct {
  CustomerConfigurations []Site `json:"customerConfigurations"`
}

type Site struct {
  ....
  ....
}

const (
  CustomerConfigFile = "abc.json"
)

func ReadCustomerConfig(path string, logger log.Logger) (*CustomerManifest, error) {
  // I try to find all the files with my below utility method.
  // Work with single file name and also with regex name
  files, err := utilities.FindFiles(path, CustomerConfigFile)
  if err != nil {
    return nil, err
  }
  var customerConfig CustomerConfigs
  // there is only file for this config so loop will run once
  for _, file := range files {
    body, err := ioutil.ReadFile(file)
    if err != nil {
      return nil, err
    }

    err = json.Unmarshal(body, &customerConfig)
    if err != nil {
      return nil, err
    }
  }

  customerConfigIndex := BuildIndex(customerConfig, logger)
  return &CustomerManifest{CustomerManifest: customerConfigIndex}, nil
}

func BuildIndex(customerConfig CustomerConfigs, logger log.Logger) map[int64]Site {
  ...
  ...
}

As you can see above in sample case 1, I am making CustomerManifest struct from CustomerConfigs struct and then return it instead of returning CustomerConfigs directly.

Sample case 2

package config

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
  "github.com/david/internal/utilities"
)

type StateManifest struct {
	NotionTemplates       NotionTemplates
	NotionIndex           map[int64]NotionTemplates
}

type NotionMapConfigs struct {
	NotionTemplates      []NotionTemplates      `json:"notionTemplates"`
  ...
}

const (
  // there are many files starting with "state-", it's not fixed number
	StateConfigFile = "state-*.json"
)

func ReadStateConfig(path string, logger log.Logger) (*StateManifest, error) {
  // I try to find all the files with my below utility method.
  // Work with single file name and also with regex name
	files, err := utilities.FindFiles(path, StateConfigFile)
	if err != nil {
		return nil, err
	}
	var defaultTemp NotionTemplates
	var idx = map[int64]NotionTemplates{}

  // there are lot of config files for this config so loop will run multiple times
	for _, file := range files {
		var notionMapConfig NotionMapConfigs
		body, err := ioutil.ReadFile(file)
		if err != nil {
			return nil, err
		}
		err = json.Unmarshal(body, &notionMapConfig)
		if err != nil {
			return nil, err
		}

		for _, tt := range notionMapConfig.NotionTemplates {
			if tt.IsProcess {
				defaultTemp = tt
			} else if tt.ClientId > 0 {
				idx[tt.ClientId] = tt
			}
		}
	}

	stateManifest := StateManifest{
		NotionTemplates:       defaultTemp,
		NotionIndex:           idx,
	}
	return &stateManifest, nil
}

As you can see above in my both the cases, I am making another different struct after deserializing is done and then I return that struct back but as of now in your current suggestion I think I won't be able to do this generically because for each config I do different type of massaging and then return those struct back. Is there any way to achieve above functionality with your current suggestion? Basically for each config if I want to do some massaging, then I should be able to do it and return new modified struct back but for some cases if I don't want to do any massaging then I can return direct deserialize json struct back. Can this be done generically?

Since there are config which has multiple files in them so that is why I was using my utilities.FindFiles method to give me all files basis on file name or regex name and then I loop over all those files to either return original struct back or return new struct back after massaging original struct data.

答案1

得分: 2

你可以使用一个通用函数来加载所有的配置文件。

假设你有以下配置结构体:

type Config1 struct {...}
type Config2 struct {...}
type Config3 struct {...}

你为那些需要的配置定义了配置验证器:

func (c Config1) Validate() error {...}
func (c Config2) Validate() error {...}

注意,它们实现了一个 Validatable 接口:

type Validatable interface {
   Validate() error
}

有一种配置类型包含了所有这些配置:

type Config struct {
   C1 Config1
   C2 Config2
   C3 Config3
   ...
}

然后,你可以定义一个简单的配置加载函数:

func LoadConfig(fname string, out interface{}) error {
    data, err := ioutil.ReadFile(fname)
    if err != nil {
       return err
    }
    if err := json.Unmarshal(data, out); err != nil {
       return err
    }
    // 如果需要,验证配置
    if validator, ok := out.(Validatable); ok {
       if err := validator.Validate(); err != nil {
         return err
       }
    }
    return nil
}

然后,你可以加载这些文件:

var c Config
if err := LoadConfig("file1", &c.C1); err != nil {
   return err
}
if err := LoadConfig("file2", &c.C2); err != nil {
   return err
}
...

如果有多个文件加载同一个结构体的不同部分,你可以这样做:

LoadConfig("file1", &c.C3)
LoadConfig("file2", &c.C3)
...

你可以通过定义一个切片来进一步简化这个过程:

type cfgInfo struct {
   fileName string
   getCfg   func(*Config) interface{}
}

var configs = []cfgInfo{
   {
     fileName: "file1",
     getCfg: func(c *Config) interface{} { return &c.C1 },
   },
   {
     fileName: "file2",
     getCfg: func(c *Config) interface{} { return &c.C2 },
   },
   {
     fileName: "file3",
     getCfg: func(c *Config) interface{} { return &c.C3 },
   },
   ...
}

func loadConfigs(cfg *Config) error {
   for _, f := range configs {
     if err := loadConfig(f.fileName, f.getCfg(cfg)); err != nil {
         return err
     }
   }
   return nil
}

然后,loadConfigs 函数会将所有的配置文件加载到 cfg 中。

func main() {
   var cfg Config
   if err := loadConfigs(&cfg); err != nil {
      panic(err)
   }
   ...
}

任何不符合这种模式的配置可以使用 LoadConfig 处理:

var customConfig1 CustomConfigStruct1
if err := LoadConfig("customConfigFile1", &customConfig1); err != nil {
   panic(err)
}
cfg.CustomConfig1 = processCustomConfig1(customConfig1)

var customConfig2 CustomConfigStruct2
if err := LoadConfig("customConfigFile2", &customConfig2); err != nil {
   panic(err)
}
cfg.CustomConfig2 = processCustomConfig2(customConfig2)
英文:

You can use a common function to load all the configuration files.

Assume you have config structures:

type Config1 struct {...}
type Config2 struct {...}
type Config3 struct {...}

You define configuration validators for those who need it:

func (c Config1) Validate() error {...}
func (c Config2) Validate() error {...}

Note that these implement a Validatable interface:

type Validatable interface {
   Validate() error
}

There is one config type that includes all these configurations:

type Config struct {
   C1 Config1
   C2 Config2
   C3 Config3
   ...
}

Then, you can define a simple configuration loader function:

func LoadConfig(fname string, out interface{}) error {
    data, err:=ioutil.ReadFile(fname)
    if err!=nil {
       return err
    }
    if err:=json.Unmarshal(data,out); err!=nil {
       return err
    }
    // Validate the config if necessary
    if validator, ok:=out.(Validatable); ok {
       if err:=validator.Validate(); err!=nil {
         return err
       }
    }
    return nil
}

Then, you can load the files:

var c Config
if err:=LoadConfig("file1",&c.C1); err!=nil {
   return err
}
if err:=LoadConfig("file2",&c.C2); err!=nil {
   return err
}
...

If there are multiple files loading different parts of the same struct, you can do:

LoadConfig("file1",&c.C3)
LoadConfig("file2",&c.C3)
...

You can simplify this further by defining a slice:

type cfgInfo struct {
   fileName string
   getCfg func(*Config) interface{}
}

var configs=[]cfgInfo {
   {
     fileName: "file1",
     getCfg: func(c *Config) interface{} {return &c.C1},
   },
   {
     fileName: "file2",
     getCfg: func(c *Config) interface{} {return &c.C2},
   },
   {
     fileName: "file3",
     getCfg: func(c *Config) interface{} {return &c.C3},
   },
   ...
}

func loadConfigs(cfg *Config) error {
   for _,f:=range configs {
     if err:=loadConfig(f.fileName,f.getCfg(cfg)); err!=nil {
         return err
     }
   }
  return nil
}

Then, loadConfigs would load all the configuration files into cfg.

func main() {
   var cfg Config
   if err:=loadConfigs(&cfg); err!=nil {
      panic(err)
   }
   ...
}

Any configuration that doesn't match this pattern can be dealt with using LoadConfig:

var customConfig1 CustomConfigStruct1
if err:=LoadConfig("customConfigFile1",&customConfig1); err!=nil {
   panic(err)
}
cfg.CustomConfig1 = processCustomConfig1(customConfig1)

var customConfig2 CustomConfigStruct2
if err:=LoadConfig("customConfigFile2",&customConfig2); err!=nil {
   panic(err)
}
cfg.CustomConfig2 = processCustomConfig2(customConfig2)

huangapple
  • 本文由 发表于 2022年2月1日 15:16:43
  • 转载请务必保留本文链接:https://go.coder-hub.com/70936770.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定