Critique My BLL Design

Posts   
1  /  2
 
    
Cadmium avatar
Cadmium
User
Posts: 153
Joined: 19-Sep-2003
# Posted on: 10-Jun-2004 23:58:06   

Okay, I drank the Kool-Aid and decided to try to stop using LLBLGen entities in my presenation layer, which means I had to create my own custom entity classes. This is what I've come up with so far (simplified):


public class ProjectManager
{
    //constructors and such here    

    //private members
    private int _id
    private string _name
    //...and so on

    //public properties
    public int Id
    {
        get { return _id; }
        set { _id = value; }
    }

    //public propertiess
    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }

    //...and so on

    //single entity data retreival
    public void Fetch( int projectId )
    {
        ProjectsEntity project = new ProjectsEntity(projectId);
        //Adapter code here, retrieve an entity
        
        //populate the class fields
        _id = project.Id;
        _name = project.Name;

    }
    
    public bool Save()
    {
        ProjectsEntity project = new ProjectsEntity(projectId);

        //copy the class fields into the entity
        if (_id > 0)
        {
            project.Id = _id;
            project.IsNew = false;
        }
        project.Name = _name;
        
        //...adapter code
        return adapter.SaveEntity(project);
    }

    public static DataTable FetchCollection(/*various parameters and overloads*/)
    {
        DataTable dt = new DataTable();
        //adapter, predicate, and sort code 
        adapter.FetchTypedList(new ProjectsEntity().Fields,
            dt, bucket, 0, sort, false);
        return dt;

    }


}


Note that the entities are named 'manager', but I have no idea if this is considered Manager Model anymore (or if it ever was).

-Fetch() retrieves an entity and uses it to populate the class fields with data -Save() creates and saves an entity with data from the class fields -FetchCollection() retrieves a datatable via FetchTypedList (or FetchTypedView) based on various filter, sort, or paging parameters. I always return a DataTable for consistancy (sometimes I have to use a TypedList or TypedView, so it's just easier to have it all come back as a DataTable). It's static for ease of use (though this may change so I can expose some details for paging like total rows, etc).

So my pl consumes the enties:


ProjectManager project = new ProjectManager(1234)
project.Name = myTextBox.Text;
project.Save()

or

myDataGrid.DataSoruce = ProjectManager.FetchCollection();
myDataGrid.DataBind();


Which is all fine, besides all of the extra work manually populating the entities and class fields. But I'm kind of having trouble working with related entities. I'm finding I have to write a lot of extra code to populate related entities, so I don't know if I'm on the right path.

What do you guys think (now that we have an architectural forum simple_smile )? Is this a reasonable architecture for a simple small to moderate size app? Or am I going off in the wrong direction?

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39749
Joined: 17-Aug-2003
# Posted on: 11-Jun-2004 10:42:07   

No offence, but why on earth would you create new entity classes? simple_smile That's doing the work twice. You already have entities in your hand, with databinding support and other goodies. simple_smile It's your time of course wink

If you want persistence logic free entities, use adapter, that's why that template group is created simple_smile

You're now creating a lot of overhead without adding any new functionality. Now, having abstraction between tiers is good, but that abstraction should be about abstraction of implementation of functionality, like a method to control inventory, how it works is not important for the caller of that method, only THAT it works simple_smile .

I often run into people who think that by creating a lot of overhead in an application is good because they think it solves the problem of vendor lockin. That's a myth. Only if you use plain C with just the CRT lib you might have real portable code, but that doesn't offer the functionality you need for a lot of applications. .NET development, and especially n-tier development, contains vendor lockins. Even the usage of datasets does. Or the usage of a 3rd party grid control or a 3rd party O/R mapper, heck even the database you talk to. simple_smile (because don't have the illusion you can port your Oracle db to SqlServer without hassle)

It's of course not wrong to add the overhead, if it works, fine, but more overhead means more code to maintain, more code which can contain bugs and believe it or not, more complexity in your application.

Today a large group of developers focus on the wrong things when they develop their software. Instead of focussing on describing functionality in code in a clear way and thus creating what they have to create, they focus on the myths that by complying to a pack of rules no-one can pinpoint the exact source of is bringing instant correctness and a good solid piece of software.

When I came fresh out of the university I was very focussed on using 'the right' tools/methods to produce software because I was firmly convinced that by using that right set of tools was the ONLY way to create perfect software. During the past 10 years that I now work as a professional I have come to the conclusion that that's not the only thing that's important for perfect software.

What's equally, if not more, important is the ability to maintain the software during several years to keep its initial quality or be able to improve the quality instead of downgrading it. What's NOT that important (and by the day I think it is less and less important) is the amount of compliance your software has to the rules set by academics who work solely in cleanrooms or teachers who can tackle a 10-minute problem with ease but that's it.

How do you get maintainable software? Below is a (non complete) list of points which I find myself very important. You can see that the central point of interest is functionality. It's not compliance to 'OO pureness' or 'n-tier abstraction' or other buzzwords. I also admit I'm educated in the era of Yourdon, Nijssen, Bach and Chen.

1) first create a functional research document. This document is the result of functional research and contains for example DTD's, DFD's, DSD's, E/R models, NIAM models and other drawings. It also describes solely functionality, it doesn't describe screens or other implementation / technical details. Functionality can be seen as the description of the processes the application has to represent/automate. 2) Create a technical research document from the functional research. Technical research uses the functionality described in the functional research document and describes options for various technical aspects: how many tiers? remoting/webservices? Webapplication or winforms? 3) Scrap functionality which is not going to be realized. 4) Use the technical research document to set up the software project. Use the functional document to write the software, with the technical document guiding you in the technical decisions. Because the technical document is based on the functional document, there is a tight connection between the technical and functional documents. This is essential. The implementation of the code is done with this idea in mind: describe the functionality in a computer language. That's developing software. When you do that, there will be a tight connection between functionality description in human language and functionality description in computer language.

Because of this, when you pick a random piece of code, you can make the transition back to the functionality description in the functional document. Vice versa also works: you can pick a random piece of functionality in the document and find the code back.

It is essential to get that functional description right. You will run perhaps a couple of times through 1-3. it is also essential you describe the functionality modularly, i.e.: not as a big novel but for example per process, and per process in subprocesses.

Buzzword bingo The world of software development is filled with a lot of buzzwords. Even O/R mapping is one. 'Webservices compliant', 'Pure OO', 'Pure O/R mapping', 'N-tier' and what have you. These things are not a solution on their own and most of the time the real benefits of their 'pureness', 'complianceness' are not really measureable nor interesting. Don't fall into that trap or buy the pretty marketing.

With your functionality research document and technical document in hand, you have a good overview of what you have to develop. If that means you have to talk to a database, you have to write code to work with that database. Because the functional research already gives you your datamodel (normalized, as NIAM/ORM does that!) for free (and we all like freebees wink ), what's needed is code to work with that model. What is the real question in this? Developing a layer to talk to the db and to provide functionality for the process implementations to talk to the db. DFD's, DSD's and DTD's don't contain descriptions of that layer.

That's not a surprise, it's overhead. It's the same overhead you get for free when you write a file to the filesystem: you don't manipulate NTFS records yourself nor do you do the cilinder/inode maintenance. You just write a file, using a provided function. You don't worry if that file function works, you assume it does. It should be the same with your DAL. You just call a function to persist some data, and it has to work. If it doesn't, the DAL is buggy and the DAL creator has to fix it, as it falls outside your project scope: it's overhead that just has to work.

Does it matter how the DAL works internally? For rainy sundays, it might be a good timekiller, but for your application, it's not important. The functionality to implement lies on top of that DAL. HOW you describe your functionality in computer language is really up to you: keep the connection between functional research document and description of functionality as tight as possible. That will make your application maintainable. It also will make the development of the application more transparent: the scope of what has to be implemented is clear.

When you create more overhead in your implementation, without adding functionality, thus by actually realizing described functionality with a lot of overhead, that overhead will become part of your project scope, it will not be a base your functionality relies upon, it's part of the functionality. However is there a clear link between a functional research result and your overhead code? Perhaps, perhaps not. In the case of 'perhaps not', I think it is wise to get rid of it. Remember: no-one will give you a compliment if your application complies to a 'pure OO', 'pure n-tier' law written by a person who writes books for a living. However they will give you compliments if your application does what it should do and is maintainable, not only for you but also for other developers who will work on the software within a year or so after you've moved up in the organization for example or are working on another project.

Frans Bouma | Lead developer LLBLGen Pro
wayne avatar
wayne
User
Posts: 611
Joined: 07-Apr-2004
# Posted on: 11-Jun-2004 11:33:50   

Here is an example of the classes that i would use in a BL. I make use of two type of classes - i call the one a manager and the other a record.

I might not be using your flovour - I am using self serv... but this should still give you an idea. Have a look at the code - if you find something you like, good - if you dont....let me know what you don't like and where you think i might be going wrong.simple_smile

**This is only a demo of the method that i use - so it is not complete. **

In this demo you can see how i handle: 1. field change auditing 2. validation 3. saving 4. returning of collections as datatables

I think N-tier dev has been cover very well in the other threads - i find that i am repeating myself - over and over and over.simple_smile

It's alot of code to post - but there is no other way.cry rage ... So here goes.

Base Manager Class

using System;
using SD.LLBLGen.Pro.ORMSupportClasses;

namespace Northwind.BL
{
    /// <summary>
    /// Summary description for BL_Manager.
    /// </summary>
    public class BL_Manager
    {

        protected BL_Record _Rec = null;
        protected EntityCollectionBase _Collection = null;
        

        public int RecordCount
        {
            get
            {
                if (_Collection != null) 
                {
                    return _Collection.Count;
                }
                else
                {
                    return -1;
                }
            }
        }

        public BL_Manager()
        {
            
        }
    }
}

Derived Manager Class

using System;
using System.Data; 
using Northwind.BL;
using Northwind.BL.BL_Records;
using Northwind.DL; 
using Northwind.DL.CollectionClasses; 
using SD.LLBLGen.Pro.ORMSupportClasses;
using Northwind.DL.HelperClasses; 
using Northwind.DL.FactoryClasses;  

namespace Northwind.BL.BL_Managers
{
    /// <summary>
    /// Summary description for BL_EmpMan.
    /// </summary>
    public class BL_EmpMan : BL_Manager 
    {
                    
        //Public Properties
        public BL_EmpRec Employee 
        {
            get 
            {                       
                return (BL_EmpRec)_Rec; 
            }           
        }

        //All DataTable fetches
        public DataTable Get_AllEmployees()
        {
            return EmployeesCollection.GetMultiAsDataTable(null,0,null);
        }

        public DataTable Get_Employees_By_Name(string FirstName)
        {
            PredicateExpression Filter;
            Filter = new PredicateExpression();
            Filter.Add(PredicateFactory.CompareValue(Northwind.DL.EmployeesFieldIndex.FirstName, ComparisonOperator.Equal  , FirstName));
            
            return EmployeesCollection.GetMultiAsDataTable(Filter,0,null);
        }

        public DataTable Get_AllEmployees_By_Surname(string LastName)
        {
            PredicateExpression Filter;
            Filter = new PredicateExpression();
            Filter.Add(PredicateFactory.CompareValue(Northwind.DL.EmployeesFieldIndex.LastName, ComparisonOperator.Equal  , LastName));
            
            return EmployeesCollection.GetMultiAsDataTable(Filter,0,null);
        }

        public DataTable Get_Employees_By_Country(string Country)
        {
            PredicateExpression Filter;
            Filter = new PredicateExpression();
            Filter.Add(PredicateFactory.CompareValue(Northwind.DL.EmployeesFieldIndex.Country, ComparisonOperator.Equal  , Country));
            
            return EmployeesCollection.GetMultiAsDataTable(Filter,0,null);
        }

        public DataTable Get_Employees_By_City(string City)
        {
            PredicateExpression Filter;
            Filter = new PredicateExpression();
            Filter.Add(PredicateFactory.CompareValue(Northwind.DL.EmployeesFieldIndex.City, ComparisonOperator.Equal  , City));
            
            return EmployeesCollection.GetMultiAsDataTable(Filter,0,null);
        }

        public DataTable Get_Employees_By_Region(string Region)
        {
            PredicateExpression Filter;
            Filter = new PredicateExpression();
            Filter.Add(PredicateFactory.CompareValue(Northwind.DL.EmployeesFieldIndex.Region, ComparisonOperator.Equal  , Region));

            return EmployeesCollection.GetMultiAsDataTable(Filter,0,null);
        }

        public DataTable Get_Employees_By_Postalcode(string Postalcode)
        {
            PredicateExpression Filter;
            Filter = new PredicateExpression();
            Filter.Add(PredicateFactory.CompareValue(Northwind.DL.EmployeesFieldIndex.PostalCode, ComparisonOperator.Equal  , Postalcode));

            return EmployeesCollection.GetMultiAsDataTable(Filter,0,null);
        }

        public DataTable Get_Employees_By_Location(string Country, string City, string Region, string Postalcode)
        {
            PredicateExpression Filter;
            Filter = new PredicateExpression();
            Filter.Add(PredicateFactory.CompareValue(Northwind.DL.EmployeesFieldIndex.Country, ComparisonOperator.Equal  , Country));
            Filter.AddWithAnd(PredicateFactory.CompareValue(Northwind.DL.EmployeesFieldIndex.City, ComparisonOperator.Equal  , City));
            Filter.AddWithAnd(PredicateFactory.CompareValue(Northwind.DL.EmployeesFieldIndex.Region, ComparisonOperator.Equal  , Region));
            Filter.AddWithAnd(PredicateFactory.CompareValue(Northwind.DL.EmployeesFieldIndex.PostalCode, ComparisonOperator.Equal  , Postalcode));

            return EmployeesCollection.GetMultiAsDataTable(Filter,0,null);
        }

        public BL_EmpMan()
        {
            _Rec = new BL_EmpRec();
        }

        public BL_EmpMan(int Employee_ID)
        {
            _Rec = new BL_EmpRec(Employee_ID);          
        }
    }
}

Base Record Class

using System;
using System.Data.SqlClient;
using SD.LLBLGen.Pro.ORMSupportClasses;
using System.Data;
using System.Collections;
using Northwind.DL.EntityClasses ;
using Northwind.DL.HelperClasses;
using Northwind.DL.StoredProcedureCallerClasses; 


namespace Northwind.BL
{
    /// <summary>
    /// Summary description for BL_Record.
    /// </summary>
    public class BL_Record
    {
        //Protected Variables
        private EntityBase __Entity;
        protected bool _Enable_Logging = true;
        private ArrayList Entity_Fields;

        protected EntityBase _Entity 
        {
            get 
            {
                return __Entity;                
            }

            set
            {
                __Entity = value;
                
                int i;
                Entity_Fields = new ArrayList();
                for (i = 0; i < __Entity.Fields.Count; i++)
                {
                    Entity_Fields.Add(__Entity.Fields[i].CurrentValue);
                }                       

                
            }
        }


        public bool Enable_Logging 
        {
            get 
            {
                return _Enable_Logging;
            }

            set
            {
                _Enable_Logging = value;
            }
        
        }

        //Public Methods
        public virtual bool Delete()
        {
            try  
            {           
                return _Entity.Delete();                
            }
            catch 
            {
                return false;
                    
            }
        }

        protected void Write_Audit_Memo () 
        {
            string Desc;
            string Memo;

            Desc =  "Update of " + _Entity.Fields[0].SourceObjectName;
            Memo = Get_Changed_Fields_Description();

        }

        protected virtual bool Validate(ref string Failed_Validation_Msg)
        {
            Failed_Validation_Msg = "";
            return true;
        }

        public virtual bool Save()
        {
            return Save(null);
        }

        protected string Get_Changed_Fields_Description () 
        {
            int i;
            string result;

            result = "";
            for (i = 0; i < _Entity.Fields.Count;i++)
            {
                if (_Entity.Fields[i].IsChanged) 
                {
                    result = result + _Entity.Fields[i].SourceColumnName + " value has been changed from '" +
                        Entity_Fields[i]  + "' to '" + _Entity.Fields[i].CurrentValue + ".\n";
                }
            }
            return result;
         }

        public virtual bool Save(Transaction Your_Own_Transaction)
        {
            string Validation_Error = "";
            Transaction transactionManager;
            IDbConnection DBCon;

            //Get a Transaction Object.
            if (Your_Own_Transaction == null)
            {
                transactionManager = new Transaction(IsolationLevel.ReadCommitted, "Base_Save");
            }
            else
            {
                transactionManager = Your_Own_Transaction;
            }


            //Assign the entity to Tran.

            transactionManager.Add(_Entity);

            try 
            {
                try 
                {
                    if (Validate(ref Validation_Error)) 
                    {
                        DBCon = transactionManager.ConnectionToUse;                     

                        if (_Enable_Logging) 
                        {
                            Write_Audit_Memo();
                        }

                        if (!_Entity.Save(true)) 
                        {
                            throw new Exception("Save Failed"); //Don't know why it failed!!!
                        }
                        else
                        {                           
                            //If i am using someones else's tran then don't commit.
                            if (Your_Own_Transaction == null)
                            {
                                transactionManager.Commit();
                            }
                            
                            return true; //Entity was saved.
                        }
                    }
                    else
                    {
                        throw new Exception("Validation Failed - " + Validation_Error);
                    }
                }

                catch (ORMQueryExecutionException ex)
                {
                    SqlException SqlEx;
                    SqlEx = (SqlException)ex.InnerException;

                    //Okay so the value already exists in the database - no need to tell
                    //the user this - just don't save it and continue.
                    if (SqlEx.Number == 262700) 
                    {
                        return true;
                    }
                    else
                    {
                        //Only raise the exception if i don't know what it is.
                        throw ex;
                    }
                
                }
                
            }

            //Catch all unhandled exceptions.
            catch (Exception ex)
            {
                //If i am using someones else's tran then don't Rollback.
                if (Your_Own_Transaction == null)

                {
                    transactionManager.Rollback();
                }
                
                throw ex;
            }           

            //Release the Tran.
            finally 
            {
                //If i am using someones else's tran then don't Dispose it - he might still need it.
                if (Your_Own_Transaction == null)
                {
                    transactionManager.Dispose(); 
                }               
            }
        }

        public BL_Record()
        {
        }       
    }       
    
}

Derived Record Class / Custom Entity

using System;
using Northwind.BL;
using Northwind.DL.EntityClasses;

namespace Northwind.BL.BL_Records
{
    /// <summary>
    /// Summary description for BL_EmpRec.
    /// </summary>
    public class BL_EmpRec : BL_Record
    {
        //Properties
        protected EmployeesEntity Employee
        {
            get
            {
                return (EmployeesEntity)_Entity;
            }
        }

        public string First_Name 
        {
            get 
            { 
                return Employee.FirstName; 
            }
            set 
            { 
                Employee.FirstName = value;
            }
        }

        public string LastName 
        {
            get 
            { 
                return Employee.LastName;
            }
            set 
            { 
                Employee.LastName = value;
            }
        }

        public string Title 
        {
            get 
            { 
                return Employee.TitleOfCourtesy;
            }
            set 
            { 
                Employee.TitleOfCourtesy = value;
            }
        }

        public string Country 
        {
            get 
            { 
                return Employee.Country;
            }
            set 
            { 
                Employee.Country = value;
            }
        }

        public string City 
        {
            get 
            { 
                return Employee.City;

            }
            set 
            { 
                Employee.City = value;
            }
        }

        public string Region
        {
            get 
            { 
                return Employee.Region;
            }
            set 
            { 
                Employee.Region = value;
            }
        }

        public string PostalCode
        {
            get 
            { 
                return Employee.PostalCode;
            }
            set 
            { 
                Employee.PostalCode = value;
            }
        }

        protected override bool Validate(ref string Failed_Validation_Msg)
        {
            Failed_Validation_Msg = "";

            //Place Validation Code here    

            return true;
        }

        public override bool Save()
        {

            //Place Custome Code when saving here - NOT ALWAYS REQUIRED

            return Save(null);
        }

        public BL_EmpRec()
        {           
            _Entity = new EmployeesEntity();                
        }

        public BL_EmpRec(int Employee_ID)
        {           
            _Entity = new EmployeesEntity(Employee_ID);             
        }
            
        
        //Methods
        public void Load_Employee(int Employee_ID) 
        {
            try  
            {
                _Entity = new EmployeesEntity(Employee_ID);                 
            }
            catch 
            {
                throw;          
            }
        }
        
    }
}

Dave avatar
Dave
User
Posts: 48
Joined: 28-Jan-2004
# Posted on: 11-Jun-2004 15:24:57   

Hi Cadmium,

If using LLBLGen Entities in your UI makes your application simpler and easier to maintain, use them.

I recommend you avoid a business layer if it will serve no purpose other than hiding the LLBLGen Entities. There is probably little chance of you swapping out the data layer anyway.

I don't know about you, but I purchased LLBLGen because the entities make building applications so much easier. I'd be nuts not to use them directly from my UI if it made my application simpler to write and just as easy (if not easier) to maintain.

wayne avatar
wayne
User
Posts: 611
Joined: 07-Apr-2004
# Posted on: 11-Jun-2004 16:28:09   

BL's are design by the PL requirements.

For example in my situation i know that i am going to have to support webserv.. & remoting in the future. I also know that other unknown developers are going to develop desktop & Webapps using my BL.

So in my case it is very important that i do it correctly - meaning splitting the DL from the PL because i don't actually now what the PL is going to be. As a security precusion i also need to try and control the entry points to the DB. That means that only the BL must access the DL and only the DL the DB. Currently my DB Connectionstring is stored in the main web.config but this is going to have to change aswell. My BL is going to have to control this as i am going to work with multiple DBServers.

Coming to think of this project - It's F#%^&* Huge.

If you don't need this type of func then don't develop a BL. Daja Vu? I am sure i said this before - I am repeating myself again! wink Http://www.llblgen.com/tinyforum/Messages.aspx?ThreadID=802&StartAtMessage=0

I also have to say that the implementation of a BL depends on the size and capable growth of your application. If it is a small app then you don't need to implement it. On bigger projects with large databases and many processes a BL is a must as it devines your rules for your application.

Cadmium avatar
Cadmium
User
Posts: 153
Joined: 19-Sep-2003
# Posted on: 11-Jun-2004 18:34:41   

Otis wrote:

No offence, but why on earth would you create new entity classes? simple_smile That's doing the work twice.

None taken. I had posted the bll design I which my project is currently using (which is more or less a collection of static methods) in another thread here, and a few people complained that using dal entities directly is a bad thing, so I thought I'd give it a shot and see how the other half lives. Apparently it wasn't that good of an idea simple_smile I only lost a few hours worth of playing around so it's not a big deal.

There are a few instances where having customizable entities would save some effort, but I guess the next major release of LLBLGen will alleviate some of that.

Otis wrote:

Good advice

Man you could write a book on this stuff simple_smile Unfortunatly I'm not in a good position (yet) to implement proper research design docs (small compay where things change too fast to nail it down).

Dave wrote:

I recommend you avoid a business layer if it will serve no purpose other than hiding the LLBLGen Entities. There is probably little chance of you swapping out the data layer anyway.

As I mentioned above the architecture I'm currently using is less of a bll and more of a way to group and organize common tasks (fetching/saving entity data), and even though my project is reasonably small it's still usefull. I agree I'm heading in the wrong direction with the architecture in my first post though. It's just too much work and doesnt give me anything. But I still thought I'd try it.

wayne wrote:

Example code

Thanks for your examples, they are very interesting (I haven't done much design like that, obviously wink ). Can I ask why you don't just make the Get_* methods static?

wayne avatar
wayne
User
Posts: 611
Joined: 07-Apr-2004
# Posted on: 11-Jun-2004 19:56:29   

Can I ask why you don't just make the Get_* methods static?

I propably can. I havn't used static that much.

Cadmium avatar
Cadmium
User
Posts: 153
Joined: 19-Sep-2003
# Posted on: 11-Jun-2004 21:21:55   

wayne wrote:

Can I ask why you don't just make the Get_* methods static?

I propably can. I havn't used static that much.

Okay, I just wasn't sure if I was missing out on anything simple_smile

Fishy avatar
Fishy
User
Posts: 392
Joined: 15-Apr-2004
# Posted on: 12-Jun-2004 01:12:06   

Cadmium,

Don't be afraid frowning of the Entity, embrace it's functionality flushed

Remember, the Entity (collection and single) are **_designed _**for the pl. (It's bindable, why have it bindable if you aren't using it in the pl?)

jeffreygg
User
Posts: 805
Joined: 26-Oct-2003
# Posted on: 12-Jun-2004 01:31:26   

Fishy wrote:

Cadmium,

Don't be afraid frowning of the Entity, embrace it's functionality flushed

Remember, the Entity (collection and single) are **_designed _**for the pl. (It's bindable, why have it bindable if you aren't using it in the pl?)

Fishy:

Hmmm, I might have to take exception to this, it's logic notwithstanding. Perhaps it makes no real difference here, but:

  1. LLBL = Lower Layer Business Layer, not ULPL (Upper Layer Presentation Layer simple_smile )
  2. Frans has thrashed me more than once for confusing GUI state vs. DB state.
  3. Frans has maintained that his sole focus in making decisions is to keep the object as close to the DB as possible, thus his refusal to implement in-memory/searchable object graphs (much to my chagrin), i.e. the entities/collections are merely persistence mechanisms/state brokers with some fun stuff tacked on. However, if the limitations of this architecture do not prohibit you from using it in the PL (as they do not for us), then there's really no problem using it there (design considerations notwithstanding).

I think his decision to put databinding into the entities was more of a concession than a statement. He knew there would be those who would, whether because of poor design, or simply because of small project size, want to use the entities and collections in the UI. However, I wouldn't go so far as to say that it was designed for it.

Jeff...

Cadmium avatar
Cadmium
User
Posts: 153
Joined: 19-Sep-2003
# Posted on: 12-Jun-2004 07:26:13   

jeffreygg wrote:

I think his decision to put databinding into the entities was more of a concession than a statement. He knew there would be those who would, whether because of poor design, or simply because of small project size, want to use the entities and collections in the UI. However, I wouldn't go so far as to say that it was designed for it. .

And yet Otis chastises me for not using them (at least in my small project):

Otis wrote:

No offence, but why on earth would you create new entity classes? That's doing the work twice. You already have entities in your hand, with databinding support and other goodies. It's your time of course

Unless I'm mis-understanding, use 'em if you got 'em? confused

jeffreygg
User
Posts: 805
Joined: 26-Oct-2003
# Posted on: 12-Jun-2004 08:58:33   

Hmmm...now I'm a bit confused.

Jeff...

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39749
Joined: 17-Aug-2003
# Posted on: 12-Jun-2004 09:58:42   

I think some things are mixed up simple_smile

You have a logical item, called the entity. The entity can be defined in a lot of places, with LLBLGen Pro it's the database. This is done because proven technology like NIAM/ORM, E/R model and Yourdon's vision on how to develop software uses that strategy.

This means that the definition of an entity is in the database (table), instances of entities are in that table (rows) and instances of the entity class are in your application (entity object, mirrors row in table).

An entity object in memory is thus a mirror of a real entity in the database. You can use that mirror to feed business logic rules to perform actions, you can also feed GUI logic to perform actions, that's up to you. As I tried to describe in my long posting earlier in this thread: it's not set in stone what you may or may not do, however if you follow the path of keeping your CODE close to what you have to implement (so the projection of functionality onto code is very clear, and the connection between functional design (Process A does steps 1, 2, 5 and 6. Step 1 consists of step 1a, 1b and 1c. Implement process steps 1, 2, 5, 6 with routines for example and implement 1a, 1b and 1c also with routines, in class A, or create per step a class) and code is very tight.

What I said also was that it isn't very wise to re-create code you already have, as it creates overhead, extra code to maintain and extra risks for bugs. You are however free to do it, because perhaps the reason to do so is more important to you than the extra time/overhead it takes, you are perhaps very willing to pay that price. That is what's important.

The entities are databinding aware because datasets are too. It's nothing more than conveniant to pull a bucket of entities from the database and show them in the gui with a few lines of code. That's productivity. WHERE these entities are pulled out of the database, be it the BL, or the GUI, that's up to the developer. Some developers see the GUI as the 'controller' of a process, others see the GUI as a way to retrieve / show data required for the process and a start/stop interface. There is no law that says you must not do the first but always the latter. simple_smile What's important is that you preserve the tight connection between your functional design and your code so you can maintain the application also within a year or later.

To add some to the confusion: some say everything has to be abstracted. So they create a BL tier and add methods like 'GetCustomerWithSpecialOrders'. Special orders are orders with a given property set to a given value. Because this developer needs this method more than once, he re-uses that method on these places. Everything is very clean.

Now, what happens if that special order's properties change so other orders are special? Perhaps with that change the method can't be reused on spot A and B and B requires a different method. Did the abstraction help? No, in fact it might add more confusion, as a developer maintaining the software probably changes GetCustomerWithSpecialOrders after 2 years of production and suddenly sees that after 2 days of testing at spot B something goes wrong.

I'm not saying that everything should be thrown in 1 big tier though. The example above could be solved perfectly when there was a design document which says GetCustomerWithSpecialOrders is reused at spot A and B for this and this is the reason why (the reason is important, not the fact. Design decisions are what's it all about, so when you re-look at the code and you wonder 'why is this done this way', you can look it up). The developer then could determine that with the change to be made to GetCustomerWithSpecialOrders, B requires its own routine.

In short: abstracting away stuff isn't going to bring instant goods to the table. It will give you some (abstraction, change the implementation of GetCustomerWithSpecialOrders without changing its semantical interface definition and the whole application is updated) and you lose some (re-using code can have severe consequences when that re-used component is updated).

Back to the entity usage in the GUI. Using selfservicing entities in the gui might be something that can be discouraged for the reason that the GUI programmers are perhaps not allowed to call any persistence code. You can only prevent them from doing that by using Adapter. However if that's not the case, why bother writing another entity class which mimics the already available class ? simple_smile

What I do most of the time is creating 3 tiers physically. The GUI asks the BL for everything: data, do things... The BL is logically a pack of 2 tiers: 1 serves the GUI in providing data tailored for the GUI (so the GUI code is cleaner) and to provide the api to call by the GUI to perform actions ('AddCustomer') The other one is the real BL, with classes which execute BL logic, thus use entities for input and output, using the DAL or the other BL subtier.

Business components The most important thing in this is that I can pull an entity out of the db, and use it throughout the application, eventually aggregated into another object. I never copy data over into another object just to have some abstraction. That would be redundant work.

The confusion sometimes comes from the fact that in the BL you more and more lean towards business components than entities. Business components are objects which aggregate one or more entities. Like a 'salesOrder'. It contains a Customer entity, an Order entity and the order Entity contains several OrderRow entities. You can pass that Salesorder as one unit to the GUI and other BL processes. Logic specific for the Salesorder is for example added to that Salesorder class.

This can be a way to do things, however as said before, there is no law forcing you to go that route. You can also see the world as: I have a semantical 'view' on the entities and I call that view a salesorder, by putting the 'role' salesOrder onto that set of entities currently loaded in that salesorder manager class, i.e.: that class sees that pack as the data containing the salesorder. After all, 'salesorder' is just a grouping of entities to give that group a pretty name, it's not a storable unit, as the order with orderrows separated from the customer object is perhaps also part of another business component!

So, it's up to you what to do, make the right decision based on: 'what are my priorities' and 'keep the connection between functional design and code as tight as possible'. The priorities can mess up your connection between design and code though, for various reasons.

Frans Bouma | Lead developer LLBLGen Pro
Otis avatar
Otis
LLBLGen Pro Team
Posts: 39749
Joined: 17-Aug-2003
# Posted on: 12-Jun-2004 10:21:40   

jeffreygg wrote:

I think his decision to put databinding into the entities was more of a concession than a statement. He knew there would be those who would, whether because of poor design, or simply because of small project size, want to use the entities and collections in the UI. However, I wouldn't go so far as to say that it was designed for it.

It wasn't a real concession simple_smile THE most important thing was and still is that you can use the same object throughout the application: change an entity object in the gui and pass in on to a lower tier which perhaps uses that change to start a new process or alter a running process, saving some other entities and what have you... To be able to do that, databinding had to be added. It took a great deal of effort to get it right: IEditableObject, ITypedList and IBindingList are not easy-doesit interfaces simple_smile Add to that the bizar databinding behavior sometimes and you have a great time as a developer simple_smile

Databinding is conveniant. Not always but most of the time. It is very conveniant to have an object passed up by the BL which you can bind directly to a control, if you want to, as the entity is usable throughout the application.

That's the most valuable lesson learned from Windows DNA/n-tier development: the answer to the question: "How to pass data accross tiers?".

Frans Bouma | Lead developer LLBLGen Pro
wayne avatar
wayne
User
Posts: 611
Joined: 07-Apr-2004
# Posted on: 12-Jun-2004 14:17:28   

Wow - What a lecture!!stuck_out_tongue_winking_eye

Jeremy Driver avatar
Posts: 41
Joined: 08-May-2004
# Posted on: 12-Jun-2004 20:59:23   

Thanks for sharing your opinions with us, Frans. I appreciate the time you've spent articulating your ideas.

These architectural discussions have been very useful to me as I consider how to implement a business layer of my own, so I also want to thank everyone else for sharing their own business layer designs as well.

Jeremy

Posts: 497
Joined: 08-Apr-2004
# Posted on: 14-Jun-2004 09:45:35   

Frans,

Thanks a lot for your opinions on this matter. You've provided a good sense check here for me.

I originally decided to go for custom entities (which would be gen'd by a template mod). My main reason for this was not tier abstraction (althouh I saw that as a "bonus"), it was for a simple problem we faced:

LLBL entities are bound to db tables, but in some places we need to add extra properties to these entities before we pass them to the PL. Your LLBL help manula explains how to add fields to entities, but its quite a lot of work, so I thought that the custom objects would sort this out for me (which they do!). However, your new beta allows custom properties to be added to entities, which solves the problem for us! So now I am contemplating reverting to LLBL entities.....hmmmmm

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39749
Joined: 17-Aug-2003
# Posted on: 14-Jun-2004 10:21:05   

MattWoberts wrote:

I originally decided to go for custom entities (which would be gen'd by a template mod). My main reason for this was not tier abstraction (althouh I saw that as a "bonus"), it was for a simple problem we faced:

LLBL entities are bound to db tables, but in some places we need to add extra properties to these entities before we pass them to the PL. Your LLBL help manula explains how to add fields to entities, but its quite a lot of work, so I thought that the custom objects would sort this out for me (which they do!). However, your new beta allows custom properties to be added to entities, which solves the problem for us! So now I am contemplating reverting to LLBL entities.....hmmmmm

THe custom properties are not regular properties, don't confuse the two simple_smile They're 'properties' of the entity, thus name-value pairs.

You can 'inject' the new properties you want to add yourself, with the custom included templates which are new in the upcoming update and which make it a lot easier to add these new properties.

What you're talking about are more 'business components' I think, which can contain more info than the lower level entities the lower layer talk about simple_smile

Frans Bouma | Lead developer LLBLGen Pro
wayne avatar
wayne
User
Posts: 611
Joined: 07-Apr-2004
# Posted on: 14-Jun-2004 10:34:09   

I downloaded LLBLGen 7 June.

Where do i find the 'custom properties'?

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39749
Joined: 17-Aug-2003
# Posted on: 14-Jun-2004 10:53:13   

wayne wrote:

I downloaded LLBLGen 7 June.

Where do i find the 'custom properties'?

It's in the GUI updates, (currently the RC1 is up), thus not yet released simple_smile

Frans Bouma | Lead developer LLBLGen Pro
Posts: 497
Joined: 08-Apr-2004
# Posted on: 14-Jun-2004 10:56:29   

Otis wrote:

What you're talking about are more 'business components' I think, which can contain more info than the lower level entities the lower layer talk about simple_smile

I think of objects that perform operations on entities when I think of business components. Our business components are classesd that simply expose methods for the PL, they "chat" with entities.

Posts: 497
Joined: 08-Apr-2004
# Posted on: 14-Jun-2004 11:19:32   

In case anyone is interested, this is a list of reaons why for us we deiced to go with custom entities to pass between PL<>BL:

Why “Custom Entities” rather than using the LLBLGen entities? • We can model the entity to the process, rather than the table, if required • It prevents the presentation layer from knowing about LLBLGen • It is easier to add custom properties to custom entities than modify LLBLGen entities
• Our custom entities can inherit base properties such as FullTitle, ModuleCode, etc.
• We can expose ONLY the properties we need • We can use LLBLGen to automate the creation of the custom entities • We can use generic code that uses reflection to populate a custom entity from a LLBLGen entity.

So thats why we did it. However in lots of places in our app a simple LLBL entity would have been fine - but I didn't want us passing both, so we standardized on custom ones.

Posts: 497
Joined: 08-Apr-2004
# Posted on: 21-Jun-2004 13:35:02   

Hi..

Just mulling over the old "custom entities" rather than "LLBL entites" in the PL. As I mentioned in my previous post, we decided to pass custom entity objects between BL<>PL, primarily because we wanted to easily extend the entites to include custom fields that aren't in the database - so these entities map to the business process rather than the underlying DB structure, and it makes working in the PL very nice for us....

However, on the downside, sometimes I don't need anything more the the gen'd class, so I am writing code to for example create an ArrayList from an EntityCollection to pass back to the PL, and it all seems a little "daft" in this scenario! Take a look at the code snippet:

protected  ArrayList TransformToArrayList(GeneratedGeneric.HelperClasses.EntityCollection entColl)
{
    ArrayList retList = new ArrayList();
    foreach (GeneratedGeneric.EntityClasses.System_AreaEntity entClass in entColl)
    {
        BusinessEntities.System_AreaBusEnt sysAreaEnt = new BusinessEntities.System_AreaBusEnt();
        TransformEntities(entClass, sysAreaEnt);
        retList.Add(sysAreaEnt);

    }
    return retList;

}

Something that might make life easier, although I may not have fully thought this through, is....

Ability to Define Non-mapped fields in Entities (in designer)

I got excited when I looked at the beta because this is what I thought the custom properties were, but Frans corrected me there.

Its a really common scenario that we want to add a property onto the entity (for example FullName), so this could be part of the generated entity. The new property doesn't come from the database, but is just there in the generated code ready for use (my BL can fill it with the appropriate value, or in some cases I may want to add code to determine what it returns).

I can see some issues with this which may account for its ommision:

  1. It means that the entities don't strictly map to the tables LLBL is not supposed to model the business process, it models the db (obviosly!) (and it does this very well may I add simple_smile )
  2. It opens up issues - for example if I wanted to add this code to my new generated property:
public string FullTitle
{
     get { return this.Prefix + ": " + this.Title; }
}

... then I need to add this code, and where would it go...

I'd be interested to know your thoughts on this Frans, and anyone else!

Fishy avatar
Fishy
User
Posts: 392
Joined: 15-Apr-2004
# Posted on: 21-Jun-2004 15:59:53   
 I got excited when I looked at the beta because this is what I thought the custom properties were

Me too.

The documentation does have a section on extending the entity.

Another thought would be to change the template to always create a property called "ExtendedCols" of type Object. Then, create a class that would contain your custom fields. Then, set the ExtendCols property to the custom class. You would need to iterate over the entitycollection to populate your new custom collection. This is not an ideal solution, and I would love to hear others.

Fishy

Otis avatar
Otis
LLBLGen Pro Team
Posts: 39749
Joined: 17-Aug-2003
# Posted on: 22-Jun-2004 10:06:09   

Adding your own fields will be added, don't worry. simple_smile The scope for that feature is pretty big though that's why it was postponed.

In the new version, hopefully released today otherwise tomorrow, you can 'inject' template code into the shipped templates, so you can add your own properties that way without having to alter shipped templates. Perhaps an idea.

Frans Bouma | Lead developer LLBLGen Pro
1  /  2