如何创建一个允许许多客户使用具有不同功能的网站的架构?

时间:2012-10-19 13:13:29

标签: database-design web-applications configuration coldfusion

我继承了一个应用程序的怪物。这是一个许多客户用于订单处理的网站。它陈旧,过时,需要一些认真的更新。最困难的部分是它最初是在创建一种类型的客户端时创建的,然后,随着新客户端的添加,代码被大量的IF语句混淆,这些IF语句基本上为每个客户端打开或关闭功能。想象一下这样的事情(用ColdFusion编写):

<cfif clientId EQ "MIKE">
    <a href="cart1.cfm">Shopping Cart</a>
<cfelseif clientId EQ "JOE">
    <a href="cart2.cfm">Shopping Cart</a>
<cfelseif clientId EQ "BILL"
    OR clientId EQ "JILL"
    OR clientId EQ "RAY">
    <a href="cart3.cfm">Shopping Cart</a>
<cfelse>
    <a href="cart.cfm">Shopping Cart</a>
</cfif>

以上是我在整个网站上处理的一个很好的CLEAN版本。

所以,我正在尝试重新考虑网站因素,以便更容易地为客户端配置 - 基本上为每个客户提供或隐藏功能。

我从我认为简单,干净的解决方案开始,但我担心它可能会成为一个需要维护的熊。

基本上,我将给每个客户端一个目录,从web-root存储他们的文件 - 也许是网站上使用的文件等。这个目录就像C:\clients\MIKE\。在这个目录中,我存储了一个xml文件 - 让我们称之为config.xml。我创建的第一个config.xml文件包含以下内容:

<?xml version="1.0" encoding="UTF-8"?>
<root>
    <cart>
        <url>cart1.cfm</url>
    </cart>
</root>

因此,对于每个页面请求,我都会查找xml。如果存在,我将每个值复制到Client类实例的属性中:

<cfcomponent
    hint="Represents configurable per-Client settings stored in a local Xml file">
    <cfscript>
        VARIABLES.CartLink = "";
    </cfscript>

    <cffunction name="init" return="Client" output="false">
        <cfargument name="clientId" type="String" required="true" hint="i.e. 'MIKE'"/>

        <cfscript>
            var _clientXml = XmlNew();

            THIS = setClientId(ARGUMENTS.clientId);

            _clientXml = read();

            if  (   StructKeyExists(_clientXml.XmlRoot, "cart")
                &&  StructKeyExists(_clientXml.XmlRoot["cart"], "url")
                )
                setCartLink(_clientXml.XmlRoot["cart"]["url"].XmlText);

            return THIS;
        </cfscript>
    </cffunction>

    <cffunction name="getCartLink" returntype="String" output="false">
        <cfreturn VARIABLES.CartLink />
    </cffunction>

    <cffunction name="setCartLink" returntype="Void" output="false">
        <cfargument name="cartLink" type="String" required="true" />

        <cfset VARIABLES.CartLink = Trim(ARGUMENTS.cartLink) />
    </cffunction>

    <cfscript>
        function getXmlFilePath() {
            return APPLICATION.ClientFilePath
                & "\" & getClientId() & "\config.xml";
        }
    </cfscript>

    <cffunction name="read" access="public" output="false" returntype="xml">
        <cfscript>
            var _clientXml = XmlNew();
            var _fileContents = "";

            _clientXml.XmlRoot = XmlElemNew(_clientXml, "root");

            if  (FileExists(getXmlFilePath()))
                _fileContents = FileRead(getXmlFilePath());

            if  (IsXml(_fileContents))
                _clientXml = XmlParse(_fileContents);

            return _clientXml;
        </cfscript>
    </cffunction>
</cfcomponent>

使用上面的示例xml,当用户在“MIKE”客户端下登录时,REQUEST范围内的Client实例将具有属性cartLink的值“cart1.cfm”。

现在我可以简单地查找该值并使用它来填充锚标记:

<a href="#REQUEST.Client.getCartLink()#">Shopping Cart</a>

我的目标是保持代码清洁,并避免在需要编辑的地方发布数百个IF语句。

但是,当我现在看这个设计时,我意识到这可能会成为更多的维护噩梦。该网站目前有大约30-40个客户。所以,现在,我需要维护30-40个xml文件,每个配置属性都有任意数量的节点。此外,每次添加新功能时,我都必须使用新的getter / setter方法为新属性更新客户端类。

我不想让这更糟。任何想法都将不胜感激。

5 个答案:

答案 0 :(得分:4)

首先,我不知道ColdFusion,所以我不能给你代码。

但是,您所描述的问题通常称为“多租户”架构。应用程序通过您描述的过程并不常见,追溯性地破解这些功能;它很少结束!不过,你可能会在谷歌上搜索这个词。

根据我的经验,你必须选择你希望复杂性存在的地方 - 目前,它存在于代码中,而这是它生活的最糟糕的地方。复杂的代码难以维护,包含更多错误,更难更改/扩展,并使开发人员生气。

管理复杂性作为数据/配置是一种更好的方式 - 它可以显着降低代码库的复杂性,并且通常更易于管理。你迈出了这条道路的第一步,但我会稍微改进一下。

首先,我将介绍“convention over configuration”的概念。因此,在您使用的购物车示例中,我会考虑使用客户端ID命名解决方案中的所有可变元素。因此,它不是“cart3.cfm”,而是“cartMike.cfm”。这应该会大大减少XML的数量。

其次,您可能想要介绍默认值的概念。在大多数情况下,80%的设置在客户端是通用的,只有20%需要客户端特定的设置。而不是必须管理所有这些设置,引入默认值;如果没有特定于客户端的默认值,请使用默认文件中的默认值。

第三,您可能想介绍客户类型 - 金/银/铜,免费/中小企业/企业,等等。这需要为每个客户端类型创建默认配置,但可能会提高默认配置文件的命中率。

最后,您需要考虑应用程序生命周期 - 新客户端的配置过程应该是什么?它主要是一项技术任务吗?在这种情况下,我将继续使用XML文件,因为它们可以在版本控制中轻松管理。如果它更像是一个“业务”任务,您希望将其包装到更加用户友好的过程中,但您需要能够验证配置。例如,如果“cart”的配置设置为“/Mike/cart.cfm”,则需要确保该文件实际存在。您可能还需要版本控制和从开发人员转移到测试环境的机制。

答案 1 :(得分:1)

我会将客户端配置设置存储在数据库中。这将使更容易更改设置,并允许他们更改设置,如果他们想要选择不同的配置(现在可能不是这样,但提前计划)。想象一下,有不同的功能层,每个功能成本不同。允许客户维护自己的帐户并相应支付。我知道这超出了你的问题范围,只是一个如何解决不断变化的需求的例子。

通常我会在用户登录或访问网站的某个部分后从数据库中提取后,查看该用户的会话范围结构中存储的网站配置。

session.userInfo.configurations.cart = cart1.cfm
session.userInfo.configurations.paymentMethod = PayPal
session.userInfo.configurations.maxSubCatLevels = 2
etc.

答案 2 :(得分:1)

我宁愿将数据存储在数据库中而不是XML文件中。更新(您可以自己构建CMS)比文件加载更容易。

然后我想将所有这些存储为一个大结构,例如:

stuClient = {
  cart_url = "cart1.cfm",
  other_url = "other2.cfm",
  ...
}

我还宁愿将整个事物存储在会话范围中,因此您不必在每个页面请求上重新查询DB / XML文件。我假设你已经在使用session来存储clientID。

所以你最终可以做的是,在onApplicationStart中,获取所有客户端的所有数据,并将其存储在应用程序范围内。这部分实际上是可选的,因为在onSessionStart中,只获得与该客户端相关的数据(查询查询,如果你已经开始存储在应用程序中),并将其转换为结构。

因此,在您的代码中,您只需根据需要引用stuClient.cart_url。而不是在每个请求上初始化您的CFC,而是在每个会话上执行它。使用数据结构初始化。

然后在每个页面中,您可以使用<a href="#Client.getLink('cart_url')#">

之类的内容
 <cffunction name="getLink" returntype="String" output="false">
        <cfargument name="linktype" type="String" required="true" hint="e.g. 'cart_url'"/>

        <cfreturn VARIABLES.stuClient.CartLink>
    </cffunction>

答案 3 :(得分:0)

我将举例说明其他人的解释。这里最简单的解决方案是让您使用每个“设置”的简单方法调用替换所有这些条件。这是一种经过验证的经过验证的技术,可以处理您的情况。即使您没有使用会话,您也可以通过多种方式缓存或获取数据,而无需每次都以更基本的方式访问数据库。下面我有application.cfc,settings.cfc和模板。

//In your application.cfc onSessionStart (hopefully you are using sessions)
session.settings = createObject("component", "settings").init(clientID);

//In your settings.cfc 
//saving keystrokes with cfscript style functions
<cfscript>
function init(clientID){
   variables.clientID = arguments.clientID;
   setAllUserSettings();
   return this;
} 

function setAllUserSettings(){
   //query the database for a particular user 
   //you can keep the query in memory or set each row value to something
   //let's say you wanted to use the QoQ method
   variables.settings = qryAllUserSettings();       
}
</cfscript>

<cffunction name="getSetting">
    <cfarguments name="type" />
    //start query of query to get a particular setting
    //for our user
    <cfquery name="qry" dbtype="query">
         select value
         from variables.settings
         where setting = #arguments.type#
    </cfquery>

    <cfreturn qry.value />
</cffunction>

<cffunction name="qryAllUserSettings">
   <cfset var qry = "" />
   //we'll get every possible setting for this user
   <cfquery name="qry" datasource="dsn">
        select setting,value
        from tblSettings
        where clientID = variables.clientID
   </cfquery>
   <cfreturn qry />
</cffunction>

//In your ColdFusion Template
<a href="#session.settings.getSetting(type="cart")">Shopping Cart</a>

答案 4 :(得分:0)

重申先前已经说过的话,我会记下数据库中的信息。 ColdFusion让您可以创建独立的应用程序,可以将相同的基本代码与多个Application.cfc或.cfm文件一起使用,您需要做的就是确保每个文件都有一个唯一的this.name变量并将其放在一个单独的客户文件夹。然后在应用程序启动时读取客户端特定的设置,然后将它们缓存在应用程序范围内。

ColdFusion(特别是企业版)的设计完全是为了这个目的,网上有很多可以提供帮助的信息。搜索管理多个应用程序以及application.cfc如何帮助您完成此任务。