设计问题 - 面向对象的食品应用程序

3
假设我有多个用户控件,每个用户控件都在一个选项卡项内,再放在一个窗口里。
例如,假设这是一个食品收藏应用程序。那么我们有水果、蔬菜和零食三个选项卡。每个选项卡将显示该主题的食品列表,并允许用户添加、删除、修改每个部分中的食品。食品存储在单独的文本文件中,即Fruit.txt、Vegetable.txt和Snack.txt。
实际的文本文件可能看起来像这样(vegetable.txt):
Name        Carbs    Fat
Eggplant    2        1.1
Cucumber    3        0.5
etc

现在有一个很大的列表,有一个load方法可以将所有蔬菜提取到List中。我的问题是这个loadVegetables方法在后台代码文件中,因为我还有其他屏幕,比如ReviewAllFood、AddVegetable等,以及水果和零食的所有load方法,所以我最终会在各个地方重复这个load方法。
这更像是一个设计问题,我想知道如何设置来避免重复的代码。我可以有一个VegetableManager(或其他名称)类,其中包含load方法,但这实际上是否意味着减少了重复的代码?然后,在每个屏幕中,我仍然需要创建VegetableManager对象并调用其load方法。所以从效率上讲,这并没有更好,但我确实实现了更好的设计。
我觉得我在这里缺少了什么。已经有一段时间没有学习过内聚性和耦合性,我觉得我现在正在混淆自己。如果有人能建议一种适合这种情况的设计,并解释为什么选择它以及为什么比我目前的做法更好,那就太感谢了。
感谢阅读。
4个回答

3
我可以创建一个VegetableManager(或其他)类,其中包含load方法,但这是否意味着代码重复更少?然后在每个屏幕上,我仍然需要创建VegetableManager对象并调用其load方法。

这样做的重点不是效率(即性能)。重点是将加载数据的细节封装到单个隔离对象中。例如,假设您的网站变得非常大,并且为了可扩展性和性能而决定将数据存储移动到数据库中。在现有代码中,正如您所描述的那样,您将不得不遍历每个用户控件或页面并更改load方法的逻辑。最好的情况下,这很痛苦,最坏的情况下,您会错过一些内容或错误地复制粘贴。如果逻辑被封装到一个专门的对象中,它的唯一责任是知道如何从某个地方加载数据,那么您只需要进行一次更改。

用户控件的代码后台:

protected void Page_Load(object sender, EventArgs e) {
  var veggieManager = new VegetableManager();
  VeggieListControl.DataSource = veggieManager.GetAll();
  VeggieListControl.DataBind();
}

VegetableManager.cs:

public class VegetableManager {
  private static Collection<Vegetable> _veggies;
  private static object _veggieLock;

  public ReadOnlyCollection<Vegetable> GetAll() {
    if (_veggies == null) {
      lock(_veggieLock) { //synchronize access to shared data
        if (_veggies == null) { // double-checked lock
          // logic to load the data into _veggies
        }
      }
    }

    return new ReadOnlyCollection(_veggies);
  }

  public void Add(Vegetable veggie) {
    GetAll(); // call this to ensure that the data is loaded into _veggies
    lock(_veggieLock) { //synchronize access to shared data
      _veggies.Add(veggie);
      // logic to write out the updated list of _veggies to the file
    }
  }
}

由于_veggiesstatic的,尽管有多个调用者实例化了VegetableManager,但在内存中只有一个蔬菜集合。但是,由于它是静态的,如果您有一个多线程应用程序(例如网站),则必须跨所有线程同步访问该字段(因此需要lock)。
这只是良好面向对象编程的冰山一角。我建议阅读UncleBob的SOLID原则领域驱动设计免费电子书)。
所以,是的,您正在重复某些内容,但您重复的只是一个方法调用,这是可以接受的。DRY意味着减少“逻辑”代码的重复,即决策和算法;简单的方法调用不属于此类。但是,如果您想要,您可以将逻辑整合到基类中,从而使用户控件不必知道VegetableManager,尽管我认为这是过度面向对象编程或OOO :-)。
public abstract class FoodUserControl : UserControl {
  protected List<Vegetable> GetVeggies() {
    return new VegetableManager().GetAll();
  }
}

那么您实际的控件应该从这个类中继承,而不是从UserControl继承。

更新

预加载VegetableManager.cs:

public class VegetableManager {
  private static Collection<Vegetable> _veggies;
  private static object _veggieLock;

  static VegetableManager() {
    // logic to load veggies from file
  }

  public ReadOnlyCollection<Vegetable> GetAll() {
    return new ReadOnlyCollection(_veggies);
  }

  public void Add(Vegetable veggie) {
    lock(_veggieLock) { //synchronize access to shared data
      _veggies.Add(veggie);
      // logic to write out the updated list of _veggies to the file
    }
  }
}

注意这个急切加载版本不需要在构造函数中对加载代码进行双重检查锁定。还要注意加载代码位于static构造函数中,因为此代码初始化了一个static字段(否则,您将在每次构造函数中重新加载相同的共享static字段中的数据)。由于素菜是急切加载的,您不需要在GetAll或Add中加载。


为什么要为每种食物单独设置一个管理器?这样你最终会得到很多不同的管理器。然后,如果有东西想要访问水果和蔬菜,它将需要同时拥有蔬菜和水果管理器。 - Jeff Storey
谢谢你们俩,现在更清楚了,我明白我应该以完全不同的方式构建事物。 - baron
1
@gWiz,简化起来很有道理,但您不需要为不同的文件编写switch语句。策略模式可以通过向通用存储库注入对象类型到文件编写器类型的映射来选择文件编写器,这是一个不错的选择。但是,这实际上取决于要写出多少种不同对象类型(显然,该模式比其他解决方案更具可扩展性,但对于较小的问题可能过于复杂)。 - Jeff Storey
好的,构造函数可以调用它。为了最大程度地实现DRY原则,加载逻辑应该被提取到一个新方法中,然后GetAll、Add和构造函数将调用该新方法。我会更新我的答案。 - G-Wiz
我更新了我的答案。与其像我之前的评论提到的那样将逻辑提取到一个新方法中,我将加载逻辑放置在静态构造函数中。这是因为在构造函数中急切加载时,您不需要在任何其他地方执行加载例程(因此您不一定需要将加载逻辑重构为DRYness的方法-尽管这可能有助于可读性)。 - G-Wiz
显示剩余5条评论

2

我建议在读取文件时将蔬菜(或者你正在加载的任何其他内容)提取出来。然后将它们存储在某个基础数据模型中。您可以将列表和您需要的任何其他控件绑定到基础数据模型上。数据只需加载一次,但各种视图可以显示它。

编辑:添加代码

List<T> loadObjects(File file, ILineConversionStrategy strategy) {
   // read eaqch line of the file
   // for each line
   T object = strategy.readLine(line);
   list.add(object);
   return listOfObjects;
}

编辑2:数据模型

class FoodModel {
   List<Vegetable> getVegetables();
   List<Fruit> getFruit();
   // etc
}

底层数据模型会是什么(建议)?我想另一个问题是,因为作为应用程序的一部分,用户可以添加蔬菜等...目前这些都是直接写回文件的,所以我不断地重新加载,以便潜在的更改已经被保存。 - baron
你的模型可以存储应用程序所需的任何数据。它可以是一个简单的对象,具有一堆绑定属性。我建议不要在每次更改时写回文件,而是写入模型,因为I/O速度较慢。当应用程序关闭时(或者如果需要的话,定期地),您可以将模型刷新到文件中。 - Jeff Storey
我不太确定我理解你的意思。目前,它们存储在多个List <Vegetable>,List <Fruit>,List <Snack>中 - 但这在很多地方都被重复编写。我可以像我之前提到的那样创建一个单独的类,但那样我只是在各处创建一个该类的对象。您所说的它会是什么样子?您能展示一些示例/样本代码吗? - baron
所以我认为我们在谈论两个问题。我已经发布了一些示例代码,展示了如何避免在每种类型的对象中重复加载代码。它使用策略模式确定如何将文件中的行转换为适当的对象-蔬菜、水果等。这样相同的代码可以用于加载每种类型的文件,只是将文本行转换为对象的方式不同(代码类似于Java,因为我是Java开发人员)。 对于其他调用代码的问题,它应该只在从文件中加载时调用一次。 - Jeff Storey
那么你如何从其他类访问该集合? - baron
当调用load方法时,它会将食物加载到模型中。其他类将引用该模型。 - Jeff Storey

0
    public interface IEatable {}

    class Vegitable : IEatable 
    { string Name { get; set; } }
    class Fruit : IEatable 
    { string Name { get; set; } }

    public interface IEatableManager
    {
        List<Vegitables> LoadEatables(string filePath);
    }
    public class VetabaleManager : IEatableManager
    {
        #region IEatableManagerMembers    
        public List<Vegitable> LoadVegs(string filePath)
        {
            throw new NotImplementedException();
        }    
        #endregion
    }
    .
    .
    .

使用上述设计需要考虑以下几点:

必读:


0
我会使用仓储模式来实现这个功能。首先,创建一个类包含从每个文本文件中检索对象的方法:
public class FoodRepository
{
    public IList<Vegetable> GetVegetables() { ... }
    public IList<Fruit> GetFruit() { ... }
    // etc.
}

这个类应该是你的应用程序中唯一知道食物实际上存储在文本文件中的类。

一旦你搞定了这个,你可能想考虑缓存频繁使用的数据以提高性能。


AddVegetable方法应该放在哪里?这样我就可以从其他类中访问它,比如说,如果我想要遍历蔬菜-foreach(Vegetable in FoodRepository.Vegetables)。实际上,我想这个方法会放在那个类中。我想我正在努力解决的问题是如何拥有一个主要的集合,而不是在每个地方创建FoodRepository的实例-每个实例都有不同的集合等等。比如说我们有foodviewer和addfood用户控件。在addfood中,我将食物添加到该实例的集合中,但在foodviewer中,没有将食物添加到该实例的集合中。 - baron
除非我告诉它重新加载食品并告诉addfood方法写回文件,否则不会发生任何事情。(希望这样说有意义) - baron
有多种建模持久性的方式,而这只是其中之一。但我认为提问者需要一个能够直接解释为什么任何持久性建模都是有益的解释。 - G-Wiz
@gWiz -- 我对此有相反的看法,并且在重新阅读后仍然如此。这个网站的问题在于对简单问题给出了复杂的答案。问题是关于在小型 Web 应用程序中采取第一步集中数据访问,而被接受的答案提到了 SOLID、DRY、多线程和那本令人痛苦的 DDD 书。 - Jamie Ide
@baron -- 作为第一步,我建议将所有数据访问,包括AddVegetable,放入一个仓储类中,除非这变得难以处理。我不确定我是否理解问题的其余部分,但您可以使该类静态,并使用缓存,以便只有一个实例。 - Jamie Ide
@Jamie,我明白你的想法:提供一个已知好的解决方案来满足需求。但在我看来,问题不仅仅是关于需求,而是关于如何实际应用基本原则(SOLID和DRY)。在评论中,OP问如何重复使用单个管理器类而不为每个请求实例化/加载列表。因此,共享状态得到了解释,这需要提及并发性。在我看来,OP想要了解大局的细节,以便他可以正确地弥合理论和实践之间的差距,解决这个问题和未来的问题 - G-Wiz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接