6

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

这里开始我的问题。我不确定如何参数化它。我已经探索了两种主要的方法:

  1. 使用属性集合。Position 不是 astring而是 a Dictionary<Language, string>,它可以让客户通过语言检索该人的位置。
  2. 保持简单的标量属性,但让它们根据全球已知的“当前语言”设置返回(或设置)一种语言或另一种语言的值。客户端代码将设置工作语言,然后所有对象将设置并获取该语言的值。

选项 1 避免了全局状态,但它弄乱了我模型中几乎每个类的接口。另一方面,选项 2 的表现力较差,因为如果不查看全局设置,您将无法判断您将获得哪种语言。此外,它在全局设置的每个类中引入了依赖关系。

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

那么我有两个具体问题:

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

谢谢你。

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

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

4

7 回答 7

5

鉴于以下情况:

一些用例涉及以所有支持的语言设置对象的值;其他涉及查看一种给定语言的值。

我建议两种选择都去。这意味着持有多语言内容的 Person 和所有类都应保持该内容处于其状态,并且:

  • Position 属性应该以当前用户的语言设置/获取人员的位置 。

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

  • 应该有一种方法来设置(甚至在需要时切换)用户语言。我将创建一个带有抽象 SetLanguage(Language lang) 方法和 CurrentLanguage getter 的抽象类(例如 BaseMultilingualEntity)。您需要在某种会公开语言设置的注册表中跟踪从 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();

    }
}

于 2013-03-03T09:23:33.207 回答
3

域模型是一种抽象——它为世界的特定部分建模,它捕获了域的概念

该模型的存在使开发人员可以像领域专家的交流方式在代码中进行交流——对相同的概念使用相同的名称。

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

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

现在,您可能需要用他们的语言向用户展示应用程序数据和元数据,但这个概念并没有改变。

在这方面,您的第二个选择是您应该做的事情 - 对于 .NET,这通常是通过查看CurrentThread.CurrentCulture和/或CurrentThread.CurrentUICulture以及使用包含本地化资源的附属程序集来完成的。

于 2013-02-27T20:47:48.007 回答
1

我的问题涉及多语言数据

[...]

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

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

问题是,除非你的领域真的是关于翻译的,否则我会尽量避免它与多语言问题有关,原因与你试图让你的领域持久性无知或 UI 无知的原因相同。

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

public class Person
{
   [Multilingual]
   public string Position { get; set; }
}
于 2013-03-06T10:05:33.423 回答
0

使用户明确!
我已经遇到过用户的文化是该域中的一等公民的域,但是在这种情况下,我建模了一个适当的值对象(在您的示例中,我将使用正确实现的 Position 类IEquatable<Position>)和能够表达此类的 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 }
}

并且不要忘记文档和正确设计的异常

警告
您提供的示例模型中至少有两种巨大的设计气味:

  • 公共设置器(Position 属性)
  • 持有业务价值的 System.String

公共设置器意味着您的实体将其自己的状态暴露给客户,而不管其自身的不变量如何,或者此类属性对实体没有商业价值,因此根本不应该成为实体的一部分。实际上,可变实体应该始终将命令(可以改变状态)和查询(不能)分开

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

请注意,要获得可重用的领域模型非常具有挑战性,因为它需要两名以上的领域专家和丰富的 ddd 建模经验。我在职业生涯中遇到的最糟糕的“领域模型”是由一位具有丰富 OOP 技能但之前没有建模经验的高级程序员设计的:它是 GoF 模式和数据结构的混合体,希望非常灵活,事实证明没用。在花费了 20 万欧元之后,我们不得不把它扔掉并从头开始。

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

于 2013-03-04T23:57:06.447 回答
0

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

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

于 2013-03-08T01:19:37.847 回答
0

它包含超过 120 个类,并且非常稳定。

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

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

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

编辑

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

是什么决定了这个语言参数?什么负责确保以多种语言存储,比如位置?还是即时进行翻译?谁是物业的客户?如果客户端确定了语言参数,为什么客户端不能在不涉及域的情况下进行翻译?是否存在与多种语言相关的任何行为,或者这只是出于阅读目的的考虑?DDD 的重点是提炼您的核心行为领域,并将与查询数据相关的方面转移到其他责任领域。例如,您可以使用读取模型模式来访问具有特定语言的聚合的 Position 属性。

于 2013-02-27T18:38:05.087 回答
0

鉴于要求,我可能会尝试将职位本身建模为实体/价值。该对象不是翻译字典,而只是可用作域字典的键。

// 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 中自动完成建议的来源来保持数据的整洁。

于 2014-02-24T15:33:59.467 回答