【问题标题】:Change persistence layer dynamically (upon runtime) with as few changes as possible以尽可能少的更改动态(在运行时)更改持久层
【发布时间】:2015-11-25 23:17:09
【问题描述】:

我正在寻找一种设计模式/方式来动态交换我的应用程序的(持久性)层(最好是在运行时)。

为什么?

我希望能够决定是否将某些数据保存到 XML 或基于“每个实例”的数据库。所以我可能会决定一个项目使用 XML 作为后端,而另一个项目使用数据库。我想在这里灵活,并且能够轻松添加另一个“驱动程序”,例如Json 之类的。

现在假设以下设置:

我们有一个控制器,我们想管理一些数据。我们可以在 SQL 和 XML 实现之间进行选择。

一种可能的(可行的)解决方案:

BasicController.scala

val myPersistenceLayer: PersistenceLayer = SQLPersistenceLayer

val apples: Seq[Apple] = myPersistenceLayer.getApples()

trait PersistenceLayer
{
    def getApples(): Seq[Apple]
    def getBananas(): Seq[Banana]
}

object SQLPersistenceLayer extends PersistenceLayer
{
    override def getApples(): Seq[Apple] = {...}
    override def getBananas(): Seq[Banana] = {...}
}

这是一个相当讨厌的解决方案,因为必须为每个新模型添加方法(想想水果!;))不仅在特征中,而且在每个实现中。我喜欢我的单一职责,所以我宁愿将其委托给模型,例如:

trait PersistenceLayer
{
    def getAll(model: Model): Seq[Model] = { model.getAll() }
}

trait Model
{
    def getAll(): Seq[Model]
}

package "SQL"

class Apple extends Model
{
    def getAll(): Seq[Apple] = { // do some SQL magic here }
}

package "XML"

class Apple extends Model
{
    def getAll(): Seq[Apple] = { // do some XML magic here instead }
}

现在最大的问题是,即使我实现了一个具体的 PersistenceLayer,如下所示:

object SQLPersistenceLayer extends PersistenceLayer {}

我如何告诉应用程序使用正确包的模型?

如果我使用 SQLPersistenceLayer:

val apples = myPersistenceLayer.get(Apple) 

我需要导入正确的“Apple”类,这违背了整个目的,因为这样我就可以删除所有其他类,导入正确的类,然后在其上使用通用的“getAll()”方法。

所以我需要在多行更改实现,这是我想要避免的。

我想到了一些东西,比如给一个带有包名的字符串,比如

val package = "sql" 并在控制器中从正确的包中导入它,但这并不可行,也不容易实现,而且对于我显然缺少的东西来说,这是一个相当讨厌的 hack。

长话短说:我希望能够动态地切换包以用于我的持久性需求。在一些动态类型的语言中,我可以想出一个解决方案,但不是在 Scala 或任何静态类型的语言中,所以我想我在这里不知道某种设计模式

** 编辑 **

发生了一个想法(是的,有时它会发生;))现在我想知道这样的事情是否会导致我想要的:

namespace tld.app.persistence

trait PersistenceLayer
{
    proteced val models: mutable.HashMap[String, Model] = new mutable.HashMap[String, Model]

    def registerModel(key: String, model: Model): Unit =
    {
        models.remove(key)
        models.put(key, model)
    }

    def get(model: String): Seq[Future[Model]] =
    {
        val m: Model = models.getOrElse(model, throw new Exception("No such model found!"))
        m.get
    }   
}

trait Model
{
    def get(): Seq[Future[Model]]
}

namespace tld.app.persistence.sql

object SQLPersistenceLayer extends PersistenceLayer

class Person extends Model
{
    def get(): Seq[Future[Model]] =
    {
        // ... query the database
    }
}

namespace tld.app.persistence.xml

object XMLPersistenceLayer extends PersistenceLayer

class Person extends Model
{
    def get(): Seq[Future[Model]] =
    {
        // ... read in from the appropriate xml-file
    }
}

object Settings
{
    var persistenceLayer: PersistenceLayer = SQLPersistenceLayer // Default is SQLPersistenceLayer
}

Somewhere in the application:

Settings.persistenceLayer.get("person")

// Then a user-interaction happens

Settings.persistenceLayer = XMLPersistenceLayer

Settings.persistenceLayer.get("person")

persistenceLayer 通常保持不变,但用户可以决定更改它。一旦有时间,我将对其进行更深入的研究。但也许有人会立即发现这种方法存在问题。

【问题讨论】:

  • 你的需求中PersistenceLayer的接口看起来很奇怪,getAll的参数是什么?
  • 来自 sql 或 xml 命名空间的具体模型必须扩展另一个特征(当然是“模型”),以便将实际逻辑委托给(具体)模型。
  • getAll() 方法的语义并不意味着输入参数,您对类型感兴趣。这向我表明您正在泄漏实现细节(反射以决定方法内的适当实现)。此外,当getAll 返回Seq[Model] 时,您甚至会失去这种类型。
  • > 在一些动态类型语言中... 1. Rapture 确实使用language.dynamics 来实现类似的机制。 2. 隐含(使用Type Classes 或不使用)也可能对您有所帮助,如前所述 3. 在 FP 中,您可以将实现特定行为的函数作为参数传递给平淡无奇的方法/函数(甚至返回一个函数)。所以我会尝试在设置中选择这样一个(一组)功能;即关于苹果或橙子的行为(同样适用于xml或sql)。
  • 关于您的编辑:具有可变的全局持久性切换机制将不可避免地在多线程上下文中产生令人惊讶的结果。再说一次,你读的是Model,而不是Person

标签: java scala design-patterns dependency-injection persistence


【解决方案1】:

DI 允许您在编译时连接实现。在 Scala 中进行 DI 的方法有很多(Cake Pattern、Reader Monad、DI 框架等)。

如果你想在应用程序启动时连接依赖,那么常规的依赖机制就可以工作。您只需根据某些条件创建所需依赖项(SQL、XML)的实例并将其传递给代码。

如果您想在应用程序执行期间不断在依赖项之间切换,即有时保存到 SQL,有时保存到 XML,那么您可以使用类似于 Lift Injector 的内容,另请参阅我的回答 here - 选项 2。

【讨论】:

  • 是的,我想要运行时的 DI(能够在应用程序执行期间切换依赖项)。我会看看如何手动实现这一点。不急于加载到框架中。
【解决方案2】:

您可以使用运行时反射来完成它。您需要在运行时指定并创建类/对象,然后将其传递给 Persistency 层,然后调用泛型 getAll 方法。

反射库详情->http://docs.scala-lang.org/overviews/reflection/overview.html

最好让伴生对象Apple 具有getAll 方法,为每个持久层实现不同的方法。

然后使用完整的包名通过反射访问 Apple 对象

val apple:sql.Apple = //Reflection library object access
val apple:xml.Apple = //Reflection library object access


val apples = myPersistenceLayer.get(apple)

【讨论】:

  • 然后我需要将所有出现的 "apple:xml.Apple=" 更改为适当的类型。我只想更改一行或在运行时从配置文件中加载它并完成它。
  • 您的评论表明您对编译时实现布线很好,不一定是运行时?
  • 我想做什么:用户可以在任何给定时间更改持久性的类型。标准是sql,但如果他愿意,可以切换到XML。那时所有的逻辑都必须仍然有效。请看我上面的更新。
【解决方案3】:

我认为你可以通过隐式 + TypeTags 实现基于模块的包含

object SqlPersistence {
  implicit def getAll[T: TypeTag](): Seq[T] = {/* type-based sql implementation*/}
}

object JsonPersistence {
  implicit def getAll[T: TypeTag](): Seq[T] = {/* type-based json implementation*/}
}

object PersistenceLayer {
  def getAll[T](implicit getter: Unit => Seq[T]): Seq[T] = getter
}

// somewhere else ...
import SqlPersistence._

PersistenceLayer.getAll[Apple]

优点是您可以通过引入相应的导入来当场决定您的持久层。主要的缺点是相同的:您需要在每次调用时决定您的持久层,并确保它是您的想法。此外,根据我的个人经验,编译器对棘手的隐式极端情况的帮助较小,因此有可能会花费更多时间进行调试。

如果您为应用程序设置一次持久层,那么 DI 就可以了,例如cake pattern。但是话又说回来,您要么需要每个类都有一个方法,要么求助于反射。如果没有反射,它可能看起来像这样:

trait PersistenceLayer {
  def getApples(): Apples
}

trait SqlPersistenceLayer extends PersistenceLayer {
  override def getApples() = // sql to get apples 
}

trait Controller {
  this: PersistenceLayer =>

  def doMyAppleStuff = getApples()
}

// somewhere in the main ...
val controller = new Controller with SqlPersistence {}
controller.doMyAppleStuff

【讨论】:

    【解决方案4】:

    如果有帮助的话,类似的东西是strategy pattern

    【讨论】:

    • 请考虑在您的答案中添加更多详细信息。仅仅指向另一个网站的链接不被认为是一个好的答案。不过,谢谢!
    • 当然。我相信维基百科有它的详细信息,它只是在这里重复同样的事情。不过我会试一试。在您的情况下,我认为您可以拥有一个抽象持久层和两个或多个实现,一个用于 DB 持久性,另一个用于 XML 持久性。可以在运行时选择您需要的任何层。这是来自维基百科的 Java sn-p-
    • 对不起,例子有点大,但请看链接上的例子。
    【解决方案5】:

    我认为存储库模式是您的解决方案。

    编辑:

    好的。感谢“-1”,没关系,因为我没有解释我的想法……

    我的例子只是众多例子之一。所以我希望这对那里的人有用

    我将尝试解释我关于使用存储库和工厂模式的想法。

    为此,我使用示例代码创建了一个 github 存储库:https://github.com/StefanHeimberg/stackoverflow-32319416

    我的设置与您的问题几乎相同。但区别如下:

    • 我没有使用 scala。但概念是一样的......
    • 我的设置仅包含存储库工厂的“标志”。
    • “模型”对象是持久性无知的。这意味着不知道如何持续存在。这是存储库的关注点
    • 我手动进行了依赖注入,因为这对于示例来说应该足够了
    • 我没有“控制器”但我有“应用程序服务”...

    每次调用 create() 方法时,工厂内部都会对所使用的实现做出决定。

    领域层对使用的基础设施实现一无所知。应用层正在编排域服务和基础设施服务(在我的示例中只有存储库)

    如果您有任何 DI 容器,那么工厂可以由 Producer 或其他...取决于 DI 容器

    包结构:

    我也做了一个简单的集成测试

    public class AppleServiceIT {
    
        private Settings settings;
        private AppleService appleService;
    
        @Before
        public void injectDependencies() {
            settings = new Settings();
            final JdbcAppleRepository jdbcAppleRepository = new JdbcAppleRepository();
            final JsonAppleRepository jsonAppleRepository = new JsonAppleRepository();
            final AppleRepositoryFactory appleRepositoryFactory = new AppleRepositoryFactory(jdbcAppleRepository, jsonAppleRepository);
            appleService = new AppleService(settings, appleRepositoryFactory);
        }
    
        @Test
        public void test_findAppleById() {
            // test with jdbc
            settings.setRepositoryType(RepositoryTypeEnum.JDBC);
            assertEquals("JDBC-135", appleService.findAppleById(135l).getMessage());
    
            // test with json
            settings.setRepositoryType(RepositoryTypeEnum.JSON);
            assertEquals("JSON-243", appleService.findAppleById(243l).getMessage());
        }
    
        @Test
        public void test_getApples() {
            // test with jdbc
            settings.setRepositoryType(RepositoryTypeEnum.JDBC);
            assertEquals(2, appleService.getApples().size());
    
            // test with json
            settings.setRepositoryType(RepositoryTypeEnum.JSON);
            assertEquals(3, appleService.getApples().size());
        }
    
    }
    

    【讨论】:

    • 存储库模式是个好主意,但如果我想通过插件添加另一个持久性选项,它会涉及太多更改。整个工厂都需要重写,或者他们需要一些集合,其中一种新的持久性(想想 XML 或某个 NoSQL 数据库)可以“注册”自己。但是谢谢你,我考虑了存储库模式很长一段时间,但事实证明它不像我希望的那样灵活。不过还是谢谢!
    • 我认为如果你在java中使用像CDI这样的依赖注入容器,那么你可以用生产者替换工厂。然后在里面你可以找到所有实现特定接口的bean。有了这个,你可以为不同的实现创建“插件”。那么不需要集合,因为容器可以自己创建集合,其中包含具有特定接口的类路径中的所有找到的 bean.. 例如...
    • 我觉得这很有趣。如果我有时间,我会在我的 GitHub 存储库中使用 WELD CDI 进行尝试。正如我对您的理解正确,目标必须是 1. 更改位置以将持久性实现更改为可作为“插件”使用的一种。其次,要创建一个新的持久性插件,必须尽可能少的步骤才能使其对应用程序可用。 (可由 switch 属性使用)
    • 是的,基本上就是你所说的。我希望用户能够更改持久层。所以他可能会说“这个项目现在在 sqlite 但现在我希望它保存在 .xml 中”。当然,那时必须迁移旧数据,但这不是问题。问题是,他应该能够在运行时更改它。我可能不会使用 bean 或我真正鄙视的少数 Java 结构中的任何其他结构,但感谢您的努力。我想我仍然可以在 Java 中学习一些东西,而且我经常会找到在 Scala 中做同样事情的好方法:D(你的努力你会得到赏金!)
    猜你喜欢
    • 2013-09-06
    • 2014-09-25
    • 2021-11-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-08-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多