如何创建多语言域模型

时间:2013-02-27 15:59:50

标签: domain-driven-design multilingual

我正在使用域驱动设计,我对我的域模型有一个非常清晰的画面。它包含120多个类,非常稳定。我们将在.NET 4和C#中实现它。问题是,我们需要模型多语言;某些属性需要以多种语言存储。例如,Person类具有类型string的Position属性,该属性应存储英语值(例如“Librarian”)和西班牙语(例如“Bibliotecario”)。此属性的getter应根据某些语言参数返回英语或西班牙语版本。

在这里开始我的问题。我不知道如何参数化。我已经开展了两种主要方法:

  1. 使用属性集合。位置不是string,而是Dictionary<Language, string>,可让客户按语言检索此人的位置。
  2. 保留简单的标量属性,但要根据全局已知的“当前语言”设置返回(或设置)一种或多种语言的值。客户端代码将设置工作语言,然后所有对象都将设置并以该语言获取值。
  3. 选项1避免了全局状态,但是它模糊了我模型中几乎每个类的接口。另一方面,选项2的表达力较差,因为如果不查看全局设置,您无法分辨出将要获得的语言。此外,它还在全局设置中为每个类引入了一个依赖项。

    请注意,我对数据库或ORM实现不感兴趣;我只在域模型级别工作。

    我有两个具体问题:

    • 哪个是实现多语言域模型目标的最佳选择(1或2)?
    • 还有其他我未考虑过的选项,它们是哪些?

    谢谢。

    修改即可。有人建议这是一个与用户界面相关的问题,因此可以通过.NET中的全球化/本地化支持来解决。我不同意。只有当您知道在编译时必须向用户显示的本地化文字时,UI本地化才有效,但这不是我们的情况。我的问题涉及在编译时未知的多语言数据,因为它将在运行时作为用户数据提供。这不是与UI相关的问题。

    编辑2 。请记住,Person.Position只是一个说明问题的玩具示例。它不是真实模型的一部分。不要试图批评或改进它;这样做没有意义。我们的业务需求涉及许多无法编码为枚举类型或类似的属性,并且必须保留为自由文本。因此困难。

7 个答案:

答案 0 :(得分:5)

鉴于以下内容:

  

某些用例涉及在所有对象中设置对象的值   支持的语言;其他人则涉及查看给定值中的值   语言。

我建议选择两个选项。这意味着Person和所有持有多语言内容的类应该将该内容保持在其状态中并且:

  • 位置属性应设置/获取此人的位置 当前用户的语言。

  • 所有应该有相应的属性或方法 语言设置/获取。

  • 应该有一种设置方法(如果需要,甚至可以切换) 用户语言。我会创建一个抽象类(例如 BaseMultilingualEntity)与抽象SetLanguage(语言 lang)方法和CurrentLanguage getter。你需要保持 跟踪从BaseMultilingualEntity派生的所有对象 在某种注册表中会暴露语言设置。

使用某些代码进行编辑

public enum Language {
    English,
    German
}

// all multilingual entity classes should derive from this one; this is practically a partly implemented observer
public abstract class BaseMultilingualEntity
{
    public Language CurrentLanguage { get; private set; }

    public void SetCurrentLanguage(Language lang)
    {
        this.CurrentLanguage = lang;
    }
}

// this is practically an observable and perhaps SRP is not fully respected here but you got the point i think
public class UserSettings
{
    private List<BaseMultilingualEntity> _multilingualEntities;

    public void SetCurrentLanguage(Language lang)
    {
        if (_multilingualEntities == null)
            return;

        foreach (BaseMultilingualEntity multiLingualEntity in _multilingualEntities)
            multiLingualEntity.SetCurrentLanguage(lang);
    }

    public void TrackMultilingualEntity(BaseMultilingualEntity multiLingualEntity)
    {
        if (_multilingualEntities == null)
            _multilingualEntities = new List<BaseMultilingualEntity>();

        _multilingualEntities.Add(multiLingualEntity);
    }
}

// the Person entity class is a multilingual entity; the intention is to keep the XXXX with the XXXXInAllLanguages property in sync
public class Person : BaseMultilingualEntity
{
    public string Position
    {
        set
        {
            _PositionInAllLanguages[this.CurrentLanguage] = value;
        }
        get
        {
            return _PositionInAllLanguages[this.CurrentLanguage];
        }
    }

    private Dictionary<Language, string> _PositionInAllLanguages;

    public Dictionary<Language, string> PositionInAllLanguages {
        get
        {
            return _PositionInAllLanguages;
        }
        set
        {
            _PositionInAllLanguages = value;
        }
    }
}

public class Program
{
    public static void Main()
    {

        UserSettings us = new UserSettings();
        us.SetCurrentLanguage(Language.English);

        Person person1 = new Person();
        us.TrackMultilingualEntity(person1);

        // use case: set position in all languages
        person1.PositionInAllLanguages = new Dictionary<Language, string> {
            { Language.English, "Software Developer" }, 
            { Language.German, "Software Entwikcler" }
        };

        // use case: display a person's position in the user language
        Console.WriteLine(person1.Position);

        // use case: switch language
        us.SetCurrentLanguage(Language.German);
        Console.WriteLine(person1.Position);

        // use case: set position in the current user's language
        person1.Position = "Software Entwickler";

        // use case: display a person's position in all languages
        foreach (Language lang in person1.PositionInAllLanguages.Keys)
            Console.WriteLine(person1.PositionInAllLanguages[lang]);


        Console.ReadKey();

    }
}

public enum Language { English, German } // all multilingual entity classes should derive from this one; this is practically a partly implemented observer public abstract class BaseMultilingualEntity { public Language CurrentLanguage { get; private set; } public void SetCurrentLanguage(Language lang) { this.CurrentLanguage = lang; } } // this is practically an observable and perhaps SRP is not fully respected here but you got the point i think public class UserSettings { private List<BaseMultilingualEntity> _multilingualEntities; public void SetCurrentLanguage(Language lang) { if (_multilingualEntities == null) return; foreach (BaseMultilingualEntity multiLingualEntity in _multilingualEntities) multiLingualEntity.SetCurrentLanguage(lang); } public void TrackMultilingualEntity(BaseMultilingualEntity multiLingualEntity) { if (_multilingualEntities == null) _multilingualEntities = new List<BaseMultilingualEntity>(); _multilingualEntities.Add(multiLingualEntity); } } // the Person entity class is a multilingual entity; the intention is to keep the XXXX with the XXXXInAllLanguages property in sync public class Person : BaseMultilingualEntity { public string Position { set { _PositionInAllLanguages[this.CurrentLanguage] = value; } get { return _PositionInAllLanguages[this.CurrentLanguage]; } } private Dictionary<Language, string> _PositionInAllLanguages; public Dictionary<Language, string> PositionInAllLanguages { get { return _PositionInAllLanguages; } set { _PositionInAllLanguages = value; } } } public class Program { public static void Main() { UserSettings us = new UserSettings(); us.SetCurrentLanguage(Language.English); Person person1 = new Person(); us.TrackMultilingualEntity(person1); // use case: set position in all languages person1.PositionInAllLanguages = new Dictionary<Language, string> { { Language.English, "Software Developer" }, { Language.German, "Software Entwikcler" } }; // use case: display a person's position in the user language Console.WriteLine(person1.Position); // use case: switch language us.SetCurrentLanguage(Language.German); Console.WriteLine(person1.Position); // use case: set position in the current user's language person1.Position = "Software Entwickler"; // use case: display a person's position in all languages foreach (Language lang in person1.PositionInAllLanguages.Keys) Console.WriteLine(person1.PositionInAllLanguages[lang]); Console.ReadKey(); } }

答案 1 :(得分:3)

域模型是一种抽象 - 它模拟世界的特定部分,它捕获域的概念

模型存在,因此开发人员可以按照域专家沟通的方式在代码中进行通信 - 使用相同名称的相同概念。

现在,西班牙语专家和英语专家可能会对同一个概念使用不同的词语,但概念本身也是一样的(人们希望,虽然语言可能含糊不清,人们并不总是在同一个概念中理解相同的概念方式,但我离题了。)

代码应为这些概念选择一种人类语言并坚持下去。为了表示单个概念,模型绝对没有理由由不同的语言组成。

现在,您可能需要以他们的语言显示应用程序数据和元数据的用户,但概念不会改变。

在这方面,你的第二个选择是你应该做的事情 - 使用.NET,通常通过查看CurrentThread.CurrentCulture和/或CurrentThread.CurrentUICulture以及使用{{3}来完成}将包含本地化资源。

答案 2 :(得分:1)

  

我的问题涉及多语言数据

[...]

  

请注意,我对数据库或ORM不感兴趣   实现;

我可以在这两个陈述中看到一些矛盾。无论最终解决方案是什么,您都会在数据库中使用多语言特定的结构,以及查询它们进行翻译的机制,对吗?

问题是,除非您的域名关于翻译,否则我会尝试尽可能地远离多语言问题,原因与您尝试使域名持久性相同无知或UI无知。

因此,我至少会将多语言解析逻辑放在Infrastructure层中。然后,您可以使用aspects仅将多语言行为附加到某些属性,如果您确实需要实体中的多语言跟踪并且不希望持久层透明地处理所有这些:

public class Person
{
   [Multilingual]
   public string Position { get; set; }
}

答案 3 :(得分:0)

  

它包含120多个类,非常稳定。

与问题没有直接关系,但您可能需要考虑域中存在多个有界上下文。

我同意Oded的观点,在你的场景中,语言是一个UI问题。当然,域名可以通过C#和英语的组合来声明,它代表的是抽象的。用户界面将使用CultureInfo.CurrentCulture处理语言 - 有效选项#2。

具有Position属性的Person实体不管理用于表示该位置的自然语言。您可能有一个用例,您希望在一种语言中显示最初存储在另一种语言中的位置。在这种情况下,您可以将翻译器作为UI的一部分。这类似于将货币表示为一对金额和货币,然后在货币之间进行转换。

修改

  

此属性的getter应返回英语或西班牙语   版本取决于某些语言参数。

什么决定了这个语言参数?什么负责确保以多种语言存储,比如位置?或者翻译是在飞行中进行的?谁是该物业的客户?如果客户端确定语言参数,为什么客户端不能在不涉及域的情况下执行转换?是否存在与多种语言相关的行为,或者这只是阅读目的的一个问题? DDD的目的是提炼您的核心行为领域,并将与查询数据相关的方面转移到其他责任领域。例如,您可以使用read-model pattern访问具有特定语言的聚合的位置属性。

答案 4 :(得分:0)

明确用户!
我已经遇到过域,用户的文化是域中的一等公民,但在这种情况下,我建模一个正确的值对象(在您的示例中,我将使用实现IEquatable<Position>的位置类properly)和the User能够表达这些价值观。

坚持你的榜样,例如:

public sealed class VATIN : IEquatable<VATIN> { // implementation here... }
public sealed class Position : IEquatable<Position> { // implementation here... }
public sealed class Person 
{ 
    // a few constructors here...

    // a Person's identifier from the domain expert, since it's an entity
    public VATIN Identifier { get { // implementation here } }

    // some more properties if you need them...
    public Position CurrentPosition { get { // implementation here } }

    // some commands
    public void PromoteTo(Position newPosition) { // implementation here }
}
public sealed class User
{
    // <summary>Express the position provided according to the culture of the user.</summary>
    // <param name="position">Position to express.</param>
    // <exception cref="ArgumentNullException"><paramref name="position"/> is null.</exception>
    // <exception cref="UnknownPositionException"><paramref name="position"/> is unknown.</exception>
    public string Express(Position position) { // implementation here }

    // <summary>Returns the <see cref="Position"/> expressed from the user.</summary>
    // <param name="positionName">Name of the position in the culture of the user.</param>
    // <exception cref="ArgumentNullException"><paramref name="positionName"/> is null or empty.</exception>
    // <exception cref="UnknownPositionNameException"><paramref name="positionName"/> is unknown.</exception>
    public Position ParsePosition(string positionName) { // implementation here }
}

不要忘记文档和设计合理exceptions

警告
您提供的样本模型中至少有两个巨大的设计气味:

  • 公共制定者(位置属性)
  • 包含业务价值的System.String

公共设置器意味着您的实体将其自己的状态公开给客户端而不管其自身的不变量,或者这样的属性对于实体没有业务价值,因此根本不应该是实体的一部分。实际上,可变实体应始终separate commands (that can change the state) and queries (that cannot)

具有业务语义的System.String总是闻到隐含的域概念,通常是具有相等操作的值对象(实现IEquatable,我的意思)。

请注意,可重用域模型的获取非常具有挑战性,因为它需要两位以上的领域专家和ddd建模方面的丰富经验。我在我的carreer中遇到的最糟糕的“领域模型”是由一位具有巨大OOP技能但没有以前的建模经验的高级程序员设计的:它是GoF模式和数据结构的混合,希望真正灵活,证明了没用花费200k欧元后,我们不得不扔掉它并从头开始重新启动。

可能你需要一个好的数据模型直接映射到C#中的一组简单数据结构:如果你真的不需要它,你将永远不会从域模型的前期投资中获得任何投资回报率!

答案 5 :(得分:0)

值得一提的是Apache的 MultiViews 功能以及它根据浏览器的 Accept-Language 标头提供不同内容的方式。

因此,例如,如果用户请求'content.xml',Apache将根据某些优先级规则提供content.en.xml或content.sp.xl或content.fr.xml或任何可用的内容。

答案 6 :(得分:0)

根据要求,我可能会尝试将该位置建模为实体/价值。此对象不是翻译词典,而是可用作domainDictionary的键。

// IDomainDictionary would be resolved based on CurrentThread.CurrentUICulture
var domainDict = container.Resolve<IDomainDictionary<Position>>();
var position = person.Position;
Debug.Writeline(domainDict.NameFor(position, pluralForm: 1));

现在假设您需要在不存在合适的同义词时动态创建新职位,您可以通过使用IDomainDictionary作为UI中自动完成建议的来源来保持数据有点整洁。